Ajout du support pour les images des recettes dans le frontend Angular

This commit is contained in:
FyloZ 2021-02-08 18:04:53 -05:00
parent 93bae1504b
commit 17e056544d
27 changed files with 258 additions and 135 deletions

View File

@ -1,5 +1,6 @@
<cre-header></cre-header>
<div>
<div class="dark-background"></div>
<router-outlet></router-outlet>
<div class="offline-server-card-wrapper" [hidden]="isServerOnline">

View File

@ -1,4 +1,3 @@
<div class="dark-background"></div>
<form [formGroup]="form">
<mat-card class="x-centered y-centered">
<mat-card-header>

View File

@ -16,10 +16,12 @@ import {MixEditorComponent} from './components/mix-editor/mix-editor.component';
import {UnitSelectorComponent} from './components/unit-selector/unit-selector.component';
import {MixAddComponent} from './pages/mix/mix-add/mix-add.component';
import {MixEditComponent} from './pages/mix/mix-edit/mix-edit.component';
import { ImagesEditorComponent } from './components/images-editor/images-editor.component';
import { MixesCardComponent } from './components/mixes-card/mixes-card.component';
@NgModule({
declarations: [ListComponent, AddComponent, EditComponent, ExploreComponent, RecipeInfoComponent, MixTableComponent, StepListComponent, StepTableComponent, MixEditorComponent, UnitSelectorComponent, MixAddComponent, MixEditComponent],
declarations: [ListComponent, AddComponent, EditComponent, ExploreComponent, RecipeInfoComponent, MixTableComponent, StepListComponent, StepTableComponent, MixEditorComponent, UnitSelectorComponent, MixAddComponent, MixEditComponent, ImagesEditorComponent, MixesCardComponent],
imports: [
ColorsRoutingModule,
SharedModule,

View File

@ -0,0 +1,26 @@
<mat-card>
<mat-card-header>
<mat-card-title>Images</mat-card-title>
</mat-card-header>
<mat-card-content [class.no-action]="!editionMode">
<div class="d-flex flex-row justify-content-around flex-wrap">
<div *ngFor="let imageId of (imageIds$ | async)" class="d-flex flex-column align-self-center m-3">
<div class="image-wrapper">
<img src="{{backendUrl}}/recipe/{{recipe.id}}/image/{{imageId}}" width="300px"/>
<div class="d-flex flex-row justify-content-end mt-2" [class.justify-content-between]="editionMode">
<button mat-raised-button color="primary" (click)="openImage(imageId)">Afficher</button>
<button *ngIf="editionMode" mat-raised-button color="warn" (click)="delete(imageId)">Retirer</button>
</div>
</div>
</div>
</div>
</mat-card-content>
<mat-card-actions *ngIf="editionMode">
<cre-file-button
color="accent"
label="Ajouter"
accept="image/jpeg,image/png"
(change)="submit($event)">
</cre-file-button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,9 @@
mat-card
background-color: rgba(255, 255, 255, 0.5)
max-width: 90vw !important
.image-wrapper
padding: 16px
border-radius: 4px
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12)
background-color: white

View File

@ -0,0 +1,57 @@
import {Component, Input} from '@angular/core';
import {Recipe} from "../../../shared/model/recipe.model";
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {ActivatedRoute, Router} from "@angular/router";
import {Observable} from "rxjs";
import {RecipeImageService} from "../../services/recipe-image.service";
import {environment} from "../../../../../environments/environment";
@Component({
selector: 'cre-images-editor',
templateUrl: './images-editor.component.html',
styleUrls: ['./images-editor.component.sass']
})
export class ImagesEditorComponent extends SubscribingComponent {
@Input() recipe: Recipe
@Input() editionMode = false
imageIds$: Observable<number[]>
backendUrl = environment.apiUrl
constructor(
private recipeImageService: RecipeImageService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.loadImagesIds()
}
submit(event) {
const image = event.target.files[0]
this.subscribe(
this.recipeImageService.save(image, this.recipe.id),
{next: () => this.loadImagesIds()}
)
}
openImage(imageId: number) {
window.open(`${environment.apiUrl}/recipe/${this.recipe.id}/image/${imageId}`, '_blank')
}
delete(imageId: number) {
this.subscribe(
this.recipeImageService.delete(imageId, this.recipe.id),
{next: () => this.loadImagesIds()}
)
}
private loadImagesIds() {
this.imageIds$ = this.recipeImageService.getAllIdsForRecipe(this.recipe.id)
}
}

View File

@ -4,7 +4,7 @@
</mat-expansion-panel-header>
<div class="mix-actions d-flex flex-row justify-content-between">
<ng-container *ngIf="!editing">
<ng-container *ngIf="!editionMode">
<div class="flex-grow-1">
<mat-form-field class="dark">
<mat-label>Casier</mat-label>
@ -18,7 +18,7 @@
<button mat-raised-button color="accent" (click)="deduct.emit()">Déduire</button>
</div>
</ng-container>
<ng-container *ngIf="editing">
<ng-container *ngIf="editionMode">
<div class="flex-grow-1"></div>
<button mat-raised-button color="accent" routerLink="/color/edit/mix/{{recipe.id}}/{{mix.id}}">Modifier</button>
</ng-container>
@ -92,8 +92,8 @@
<tr mat-header-row *matHeaderRowDef="mixColumns"></tr>
<tr mat-row *matRowDef="let mixMaterial; columns: mixColumns"
[class.low-quantity]="!editing && isInLowQuantity(mixMaterial.id)"></tr>
<ng-container *ngIf="!editing">
[class.low-quantity]="!editionMode && isInLowQuantity(mixMaterial.id)"></tr>
<ng-container *ngIf="!editionMode">
<tr mat-footer-row *matFooterRowDef="mixColumns"></tr>
</ng-container>
</table>

View File

@ -22,7 +22,7 @@ export class MixTableComponent extends SubscribingComponent {
@Input() recipe: Recipe
@Input() units$: Subject<string>
@Input() deductErrorBody
@Input() editing: boolean
@Input() editionMode: boolean
@Input() printingError = 2
@Output() locationChange = new EventEmitter<{ id: number, location: string }>()
@Output() quantityChange = new EventEmitter<{ id: number, materialId: number, quantity: number }>()
@ -46,7 +46,7 @@ export class MixTableComponent extends SubscribingComponent {
ngOnInit() {
super.ngOnInit();
if (this.editing) {
if (this.editionMode) {
this.mixColumns = this.COLUMNS_STATIC
}
@ -142,7 +142,7 @@ export class MixTableComponent extends SubscribingComponent {
}
private convertQuantities(newUnit: string) {
this.computedQuantities.forEach(q => convertMixMaterialQuantity(q, this.units, newUnit))
this.computedQuantities.forEach(q => q.quantity = convertMixMaterialQuantity(q, this.units, newUnit))
this.units = newUnit
}

View File

@ -1,4 +1,4 @@
<mat-card class="mt-5">
<mat-card>
<mat-card-header>
<mat-card-title>Étapes</mat-card-title>
</mat-card-header>

View File

@ -35,34 +35,21 @@
[formFields]="formFields"
[unknownError]="unknownError"
[customError]="errorMessage"
[disableButtons]="true">
[disableButtons]="true"
[noTopMargin]="true">
</cre-entity-edit>
</div>
<div class="recipe-mixes-wrapper">
<mat-card>
<mat-card-header>
<mat-card-title>Mélanges</mat-card-title>
</mat-card-header>
<mat-card-content class="no-action pl-5">
<ng-container *ngFor="let mix of recipe.mixes; let i = index">
<cre-mix-table
[class.no-top-margin]="i == 0"
[recipe]="recipe"
[mix]="mix"
[units$]="units$"
[editing]="true">
</cre-mix-table>
</ng-container>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="accent" routerLink="/color/add/mix/{{recipe.id}}">Ajouter</button>
</mat-card-actions>
</mat-card>
<cre-mixes-card [recipe]="recipe" [units$]="units$" [editionMode]="true"></cre-mixes-card>
</div>
<div class="mt-5">
<div>
<cre-step-table [steps]="recipe.steps"></cre-step-table>
</div>
<div>
<cre-images-editor #imagesEditor [recipe]="recipe" [editionMode]="true"></cre-images-editor>
</div>
</div>
</div>

View File

@ -1,17 +1,2 @@
.recipe-wrapper > div
margin: 0 3rem 3rem
mat-card
margin-top: 3rem
.recipe-mixes-wrapper mat-card
min-width: 20rem
mat-card
mat-card-content
margin-bottom: 0
padding-top: 16px !important
padding-bottom: 0 !important
mat-card-actions
margin-top: 0

View File

@ -1,14 +1,15 @@
import {Component} from '@angular/core';
import {Component, ViewChild} from '@angular/core';
import {SubscribingComponent} from "../../../shared/components/subscribing.component";
import {Recipe} from "../../../shared/model/recipe.model";
import {RecipeService} from "../../services/recipe.service";
import {ActivatedRoute, Router} from "@angular/router";
import {FormBuilder, Validators} from "@angular/forms";
import {Validators} from "@angular/forms";
import {Subject} from "rxjs";
import {UNIT_GALLON, UNIT_LITER, UNIT_MILLILITER} from "../../../shared/units";
import {AccountService} from "../../../accounts/services/account.service";
import {EmployeePermission} from "../../../shared/model/employee";
import {EntityEditComponent} from "../../../shared/components/entity-edit/entity-edit.component";
import {ImagesEditorComponent} from "../../components/images-editor/images-editor.component";
@Component({
selector: 'cre-edit',
@ -18,6 +19,8 @@ import {EntityEditComponent} from "../../../shared/components/entity-edit/entity
export class EditComponent extends SubscribingComponent {
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
@ViewChild('imagesEditor') imagesEditor: ImagesEditorComponent
recipe: Recipe | null
formFields = [
{

View File

@ -42,31 +42,28 @@
</mat-form-field>
</div>
<div class="recipe-content">
<div class="recipe-content d-flex flex-row justify-content-around align-items-start flex-wrap mt-5">
<!-- Mixes -->
<div>
<ng-container *ngFor="let mix of recipe.mixes">
<cre-mix-table
[mix]="mix"
[recipe]="recipe"
[deductErrorBody]="deductErrorBody"
[units$]="units$"
(quantityChange)="changeQuantity($event)"
(locationChange)="changeMixLocation($event)"
(deduct)="deductMixQuantities(mix.id)"
[(printingError)]="printingError">
</cre-mix-table>
</ng-container>
<cre-mixes-card
[recipe]="recipe"
[deductErrorBody]="deductErrorBody"
[units$]="units$"
(quantityChange)="changeQuantity($event)"
(locationChange)="changeMixLocation($event)"
(deduct)="deductMixQuantities($event)"
[(printingError)]="printingError">
</cre-mixes-card>
</div>
<!-- Steps -->
<div>
<cre-step-list [steps]="recipe.steps"></cre-step-list>
</div>
</div>
<!-- Images -->
<div class="recipe-images">
Images
<!-- Images -->
<div>
<cre-images-editor [recipe]="recipe" [editionMode]="false"></cre-images-editor>
</div>
</div>
</div>

View File

@ -1,21 +1,2 @@
.recipe-content
display: flex
flex-direction: column
align-items: center
div
margin-bottom: 1rem
.recipe-images
margin-top: 5rem
padding: 2rem
background-color: grey
@media screen and (min-width: 1920px)
.action-bar
padding-bottom: 5rem
.recipe-content
flex-direction: row
justify-content: space-around
align-items: start
.recipe-content > div
margin: 0 3rem 3rem

View File

@ -0,0 +1,32 @@
import {Injectable} from '@angular/core';
import {ApiService} from "../../shared/service/api.service";
import {Observable} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class RecipeImageService {
constructor(
private api: ApiService
) {
}
getAllIdsForRecipe(recipeId: number): Observable<number[]> {
return this.api.get(`/recipe/${recipeId}/image`)
}
save(image: File, recipeId: number): Observable<void> {
const body = new FormData()
body.append('image', image)
return this.api.post<void>(`/recipe/${recipeId}/image`, body, true)
}
deleteAll(imageIds: number[], recipeId: number) {
imageIds.forEach(id => this.delete(id, recipeId))
}
delete(imageId: number, recipeId: number): Observable<void> {
return this.api.delete<void>(`/recipe/${recipeId}/image/${imageId}`)
}
}

View File

@ -31,7 +31,7 @@ export class EmployeeService {
}
updatePassword(id: number, password: string): Observable<void> {
return this.api.put<void>(`/employee/${id}/password`, password, true, {headers: {contentType: 'text/plain'}})
return this.api.put<void>(`/employee/${id}/password`, password, true, {contentType: 'text/plain'})
}
delete(id: number): Observable<void> {

View File

@ -26,12 +26,11 @@
(click)="openSimdutUrl()">
Voir la fiche signalitique
</button>
<div class="edit-simdut-file-input">
<button mat-raised-button color="accent" type="button">Modifier la fiche signalitique</button>
<mat-form-field>
<mat-label>{{field.label}}</mat-label>
<ngx-mat-file-input #simdutFileInput [accept]="field.fileType" [formControl]="control"></ngx-mat-file-input>
</mat-form-field>
</div>
<cre-file-button
color="accent"
label="Modifier la fiche signalitique"
[accept]="field.accept"
[control]="control">
</cre-file-button>
</div>
</ng-template>

View File

@ -1,14 +1,3 @@
.simdut-file
button
height: 43px
.edit-simdut-file-input
width: 250px
mat-form-field
z-index: 10
margin-top: 10px
opacity: 0
button
position: absolute

View File

@ -1,6 +1,6 @@
import {Component, ViewChild} from '@angular/core';
import {FormField} from "../../../shared/components/entity-add/entity-add.component";
import {FormBuilder, Validators} from "@angular/forms";
import {Validators} from "@angular/forms";
import {map} from "rxjs/operators";
import {MaterialTypeService} from "../../../material-type/service/material-type.service";
import {MaterialService} from "../../service/material.service";

View File

@ -1,4 +1,4 @@
<mat-card *ngIf="entity" class="mt-5 x-centered">
<mat-card *ngIf="entity" class="x-centered" [class.mt-5]="!noTopMargin">
<mat-card-header>
<mat-card-title>{{title}}</mat-card-title>
</mat-card-header>

View File

@ -21,6 +21,7 @@ export class EntityEditComponent extends SubscribingComponent {
@Input() unknownError = false
@Input() customError: string | null
@Input() disableButtons: boolean
@Input() noTopMargin = false
@Output() submit = new EventEmitter<any>()
@Output() delete = new EventEmitter<void>()

View File

@ -0,0 +1,11 @@
<div class="file-button-wrapper">
<button mat-raised-button [color]="color" type="button">{{label}}</button>
<mat-form-field>
<mat-label>{{label}}</mat-label>
<ngx-mat-file-input
#fileInput
[accept]="accept"
[attr.formControl]="control">
</ngx-mat-file-input>
</mat-form-field>
</div>

View File

@ -0,0 +1,13 @@
.file-button-wrapper
width: 16rem
button
width: 16rem
mat-form-field
width: 16rem
z-index: 10
opacity: 0
button
position: absolute

View File

@ -0,0 +1,18 @@
import {Component, Input, ViewChild} from '@angular/core';
import {FormControl} from "@angular/forms";
import {ThemePalette} from "@angular/material/core";
import {MaterialFileInputModule} from "ngx-material-file-input";
@Component({
selector: 'cre-file-button',
templateUrl: './file-button.component.html',
styleUrls: ['./file-button.component.sass']
})
export class FileButtonComponent {
@ViewChild('fileInput') fileInput: MaterialFileInputModule
@Input() label: string
@Input() color: ThemePalette
@Input() accept: string
@Input() control: FormControl | null
}

View File

@ -5,6 +5,7 @@ import {environment} from "../../../../environments/environment";
import {AppState} from "../app-state";
import {Router} from "@angular/router";
import {map, share, takeUntil} from "rxjs/operators";
import {valueOr} from "../utils/optional.utils";
@Injectable({
providedIn: 'root'
@ -61,7 +62,11 @@ export class ApiService implements OnDestroy {
}
private executeHttpRequest<T>(requestFn: (httpOptions) => Observable<any>, needAuthentication = true, requestOptions: ApiRequestOptions = new ApiRequestOptions()): Observable<T> {
const httpOptions = {withCredentials: false, observe: 'response'}
const httpOptions = {
withCredentials: false,
headers: {contentType: valueOr(requestOptions.contentType, 'application/json')},
observe: 'response'
}
if (needAuthentication) {
if (this.checkAuthenticated()) {
if (httpOptions) {
@ -83,6 +88,7 @@ export class ApiService implements OnDestroy {
const errorCheckSubscription = result$.subscribe({
next: () => this.appState.isServerOnline = true,
error: err => {
console.error(err)
errorCheckSubscription.unsubscribe()
this.appState.isServerOnline = !(err.status === 0 && err.statusText === "Unknown Error");
}
@ -102,7 +108,8 @@ export class ApiService implements OnDestroy {
export class ApiRequestOptions {
constructor(
public takeFullResponse = false
public takeFullResponse?,
public contentType?
) {
}
}

View File

@ -26,35 +26,37 @@ import {EntityEditComponent} from './components/entity-edit/entity-edit.componen
import {MatSelectModule} from "@angular/material/select";
import {MatOptionModule} from "@angular/material/core";
import {MaterialFileInputModule} from "ngx-material-file-input";
import { FileButtonComponent } from './file-button/file-button.component';
@NgModule({
declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent],
exports: [
CommonModule,
HttpClientModule,
HeaderComponent,
MatCardModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatTableModule,
MatCheckboxModule,
MatListModule,
MatSelectModule,
MatOptionModule,
MaterialFileInputModule,
ReactiveFormsModule,
LabeledIconComponent,
ConfirmBoxComponent,
PermissionsListComponent,
PermissionsFieldComponent,
NavComponent,
EntityListComponent,
EntityAddComponent,
EntityEditComponent
],
declarations: [HeaderComponent, EmployeeInfoComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent],
exports: [
CommonModule,
HttpClientModule,
HeaderComponent,
MatCardModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatTableModule,
MatCheckboxModule,
MatListModule,
MatSelectModule,
MatOptionModule,
MaterialFileInputModule,
ReactiveFormsModule,
LabeledIconComponent,
ConfirmBoxComponent,
PermissionsListComponent,
PermissionsFieldComponent,
NavComponent,
EntityListComponent,
EntityAddComponent,
EntityEditComponent,
FileButtonComponent
],
imports: [
MatTabsModule,
MatIconModule,

View File

@ -0,0 +1,4 @@
/** Returns [value] if it is not null or [or]. */
export function valueOr<T>(value: T, or: T): T {
return value ? value : or
}