#14 Update recipe editor

This commit is contained in:
William Nolin 2021-09-14 16:19:00 -04:00
parent c35635c596
commit 521db72f5e
14 changed files with 226 additions and 334 deletions

View File

@ -4,6 +4,8 @@
</mat-card-header>
<mat-card-content [class.no-action]="!editionMode">
<div class="d-flex flex-row justify-content-around flex-wrap">
<p *ngIf="imagesUrls.length <= 0" class="light-text text-center">Aucune image n'est associée à cette couleur</p>
<div *ngFor="let imageUrl of imagesUrls" class="d-flex flex-column align-self-center m-3">
<div class="image-wrapper">
<img [src]="imageUrl" width="300px"/>

View File

@ -3,6 +3,8 @@
<mat-card-title>Mélanges</mat-card-title>
</mat-card-header>
<mat-card-content [class.no-action]="!editionMode">
<p *ngIf="recipe.mixes.length <= 0" class="light-text text-center">Il n'y a aucun mélange dans cette couleur</p>
<ng-container *ngFor="let mix of recipe.mixes; let i = index">
<cre-mix-table
[class.no-top-margin]="i == 0"

View File

@ -0,0 +1,36 @@
<div *ngIf="recipe">
<cre-action-bar>
<cre-action-group>
<cre-primary-button routerLink="/color/list">Retour</cre-primary-button>
<cre-unit-selector (unitChange)="changeUnits($event)"></cre-unit-selector>
</cre-action-group>
<cre-action-group>
<cre-warn-button (click)="deleteConfirmBox.show()">Supprimer</cre-warn-button>
<cre-accent-button (click)="submit()">Enregistrer</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<div class="recipe-wrapper d-flex flex-row justify-content-around align-items-start flex-wrap">
<section>
<recipe-form [recipe]="recipe"></recipe-form>
</section>
<section>
<cre-mixes-card [recipe]="recipe" [units$]="units$" [editionMode]="true"></cre-mixes-card>
</section>
<section>
<cre-step-table [recipe]="recipe" [groups$]="groups$" [selectedGroupId]="loggedInUserGroupId"></cre-step-table>
</section>
<section>
<cre-images-editor [recipe]="recipe" [editionMode]="true"></cre-images-editor>
</section>
</div>
</div>
<cre-confirm-box
#deleteConfirmBox
message="Voulez-vous vraiment supprimer la couleur {{recipe?.name}}?"
(confirm)="delete()">
</cre-confirm-box>

View File

@ -1,12 +1,13 @@
<div class="mt-5">
<cre-warning-alert *ngIf="!loading && !hasCompanies">
<div *ngIf="!loading && !hasCompanies" class="mt-5">
<cre-warning-alert>
<p>Il n'y a actuellement aucune bannière enregistrée dans le système.</p>
<p *ngIf="hasCompanyEditPermission">Vous pouvez en créer une <b><a routerLink="/catalog/company/add">ici</a></b>.</p>
</cre-warning-alert>
</div>
<cre-form *ngIf="hasCompanies" #form [formControls]="controls" class="mx-auto">
<cre-form-title>Ajouter une recette</cre-form-title>
<cre-form-title *ngIf="!recipe">Ajouter une couleur</cre-form-title>
<cre-form-title *ngIf="recipe">Modifier la couleur {{recipe.name}}</cre-form-title>
<cre-form-content>
<cre-input [control]="controls.name" label="Name" icon="form-textbox"></cre-input>
<cre-input [control]="controls.description" label="Description" icon="text"></cre-input>
@ -17,7 +18,7 @@
<cre-input [control]="controls.remark" label="Remarque" icon="text"></cre-input>
<cre-combo-box [control]="controls.company" label="Bannière" [entries]="companyEntries$"></cre-combo-box>
</cre-form-content>
<cre-form-actions>
<cre-form-actions *ngIf="!recipe">
<cre-primary-button routerLink="/color/list">Retour</cre-primary-button>
<cre-accent-button [disabled]="form.invalid" (click)="submit()">Enregistrer</cre-accent-button>
</cre-form-actions>

View File

@ -1,68 +0,0 @@
<div *ngIf="recipe">
<div class="action-bar backward">
<div class="d-flex flex-column">
<div class="mt-1 pb-2">
<button mat-raised-button color="primary" routerLink="/color/list">Retour</button>
<button
mat-raised-button
color="accent"
[disabled]="editComponent.form && editComponent.form.invalid"
(click)="submit(editComponent, stepTable)">
Enregistrer
</button>
<button
mat-raised-button
color="warn"
(click)="confirmBoxComponent.show()">
Supprimer
</button>
</div>
<mat-form-field>
<mat-label>Unités</mat-label>
<mat-select [value]="unitConstants.UNIT_MILLILITER" (selectionChange)="changeUnits($event.value)">
<mat-option [value]="unitConstants.UNIT_MILLILITER">Millilitres</mat-option>
<mat-option [value]="unitConstants.UNIT_LITER">Litres</mat-option>
<mat-option [value]="unitConstants.UNIT_GALLON">Gallons</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="flex-grow-1"></div>
</div>
<div class="recipe-wrapper d-flex flex-row justify-content-around align-items-start flex-wrap">
<div>
<cre-entity-edit
#editComponent
title="Modifier la couleur {{recipe.name}}"
deleteConfirmMessage="Voulez-vous vraiment supprimer la couleur {{recipe.name}}?"
[entity]="recipe"
[formFields]="formFields"
[disableButtons]="true"
[noTopMargin]="true">
</cre-entity-edit>
</div>
<div class="recipe-mixes-wrapper">
<cre-mixes-card [recipe]="recipe" [units$]="units$" [editionMode]="true"></cre-mixes-card>
</div>
<div>
<cre-step-table
#stepTable
[recipe]="recipe"
[groups$]="groups$"
[selectedGroupId]="loggedInUserGroupId">
</cre-step-table>
</div>
<div>
<cre-images-editor #imagesEditor [recipe]="recipe" [editionMode]="true"></cre-images-editor>
</div>
</div>
</div>
<cre-confirm-box
#confirmBoxComponent
message="Voulez-vous vraiment supprimer la couleur {{recipe?.name}}?"
(confirm)="delete()">
</cre-confirm-box>

View File

@ -1,2 +0,0 @@
.recipe-wrapper > div
margin: 0 3rem 3rem

View File

@ -1,186 +0,0 @@
import {Component, ViewChild} from '@angular/core'
import {ErrorHandlingComponent} from '../../../shared/components/subscribing.component'
import {Recipe, recipeMixCount, RecipeStep, recipeStepCount} from '../../../shared/model/recipe.model'
import {RecipeService} from '../../services/recipe.service'
import {ActivatedRoute, Router} from '@angular/router'
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 {EntityEditComponent} from '../../../shared/components/entity-edit/entity-edit.component'
import {ImagesEditorComponent} from '../../components/images-editor/images-editor.component'
import {ErrorHandler, ErrorService} from '../../../shared/service/error.service'
import {AlertService} from '../../../shared/service/alert.service'
import {GroupService} from '../../../groups/services/group.service'
import {AppState} from '../../../shared/app-state'
import {StepTableComponent} from '../../components/step-table/step-table.component'
@Component({
selector: 'cre-edit',
templateUrl: './edit.component.html',
styleUrls: ['./edit.component.sass']
})
export class EditComponent extends ErrorHandlingComponent {
readonly unitConstants = {UNIT_MILLILITER, UNIT_LITER, UNIT_GALLON}
@ViewChild('imagesEditor') imagesEditor: ImagesEditorComponent
recipe: Recipe | null
groups$ = this.groupService.all
formFields = [
{
name: 'name',
label: 'Nom',
icon: 'form-textbox',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un nom est requis'}
]
},
{
name: 'description',
label: 'Description',
icon: 'text',
type: 'text',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une description est requise'}
]
},
{
name: 'color',
label: 'Couleur',
icon: 'palette',
type: 'color',
required: true,
errorMessages: [
{conditionFn: errors => errors.required, message: 'Une couleur est requise'}
]
},
{
name: 'gloss',
label: 'Lustre',
type: 'slider',
min: 0,
max: 100,
validator: Validators.compose([Validators.required, Validators.min(0), Validators.max(100)]),
errorMessages: [
{conditionFn: errors => errors.required, message: 'Le lustre de la couleur est requis'}
]
},
{
name: 'sample',
label: 'Échantillon',
icon: 'pound',
type: 'number',
validator: Validators.min(0),
errorMessages: [
{conditionFn: errors => errors.required, message: 'Un numéro d\'échantillon est requis'},
{conditionFn: errors => errors.min, message: 'Le numéro d\'échantillon doit être supérieur ou égal à 0'}
]
},
{
name: 'approbationDate',
label: 'Date d\'approbation',
icon: 'calendar',
type: 'date'
},
{
name: 'remark',
label: 'Remarque',
icon: 'text',
type: 'text'
},
{
name: 'company',
label: 'Bannière',
icon: 'domain',
type: 'text',
readonly: true,
valueFn: recipe => recipe.company.name,
}
]
units$ = new Subject<string>()
submittedValues: any | null
errorHandlers: ErrorHandler[] = [{
filter: error => error.type === 'notfound-recipe-id',
consumer: error => this.urlUtils.navigateTo('/color/list')
}]
constructor(
private recipeService: RecipeService,
private groupService: GroupService,
private accountService: AccountService,
private alertService: AlertService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
}
ngOnInit() {
super.ngOnInit()
this.subscribeEntityById(
this.recipeService,
parseInt(this.activatedRoute.snapshot.paramMap.get('id')),
recipe => {
this.recipe = recipe
this.appState.title = `${recipe.name} (Modifications)`
if (recipeMixCount(this.recipe) == 0) {
this.alertService.pushWarning('Il n\'y a aucun mélange dans cette recette')
}
if (recipeStepCount(this.recipe) == 0) {
this.alertService.pushWarning('Il n\'y a aucune étape dans cette recette')
}
}
)
}
changeUnits(unit: string) {
this.units$.next(unit)
}
submit(editComponent: EntityEditComponent, stepTable: StepTableComponent) {
const values = editComponent.values
this.submittedValues = values
const steps = stepTable.mappedUpdatedSteps
if (!this.stepsPositionsAreValid(steps)) {
this.alertService.pushError('Les étapes ne peuvent pas avoir une position inférieure à 1')
return
}
this.subscribeAndNavigate(
this.recipeService.update(this.recipe.id, values.name, values.description, values.color, values.gloss, values.sample, values.approbationDate, values.remark, steps),
'/color/list'
)
}
delete() {
this.subscribeAndNavigate(
this.recipeService.delete(this.recipe.id),
'/color/list'
)
}
get loggedInUserGroupId(): number {
return this.appState.authenticatedUser.group?.id
}
private stepsPositionsAreValid(steps: Map<number, RecipeStep[]>): boolean {
let valid = true
steps.forEach((steps, _) => {
if (steps.find(s => s.position === 0)) {
valid = false
return
}
})
return valid
}
}

View File

@ -1,11 +1,10 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ListComponent} from "./pages/list/list.component";
import {EditComponent} from "./pages/edit/edit.component";
import {ExploreComponent} from "./pages/explore/explore.component";
import {MixEditComponent} from "./pages/mix/mix-edit/mix-edit.component";
import {MixAddComponent} from "./pages/mix/mix-add/mix-add.component";
import {RecipeAdd} from './recipes';
import {ListComponent} from './pages/list/list.component';
import {ExploreComponent} from './pages/explore/explore.component';
import {MixEditComponent} from './pages/mix/mix-edit/mix-edit.component';
import {MixAddComponent} from './pages/mix/mix-add/mix-add.component';
import {RecipeAdd, RecipeEdit} from './recipes';
const routes: Routes = [{
path: 'list',
@ -15,7 +14,7 @@ const routes: Routes = [{
component: RecipeAdd
}, {
path: 'edit/:id',
component: EditComponent
component: RecipeEdit
}, {
path: 'add/mix/:recipeId',
component: MixAddComponent

View File

@ -3,7 +3,6 @@ import {NgModule} from '@angular/core'
import {RecipesRoutingModule} from './recipes-routing.module'
import {SharedModule} from '../shared/shared.module'
import {ListComponent} from './pages/list/list.component'
import {EditComponent} from './pages/edit/edit.component'
import {MatExpansionModule} from '@angular/material/expansion'
import {FormsModule} from '@angular/forms'
import {ExploreComponent} from './pages/explore/explore.component'
@ -20,13 +19,13 @@ import {MixesCardComponent} from './components/mixes-card/mixes-card.component'
import {MatSortModule} from '@angular/material/sort'
import {CreInputsModule} from '../shared/components/inputs/inputs.module';
import {CreButtonsModule} from '../shared/components/buttons/buttons.module';
import {RecipeAdd, RecipeForm} from './recipes';
import {RecipeAdd, RecipeEdit, RecipeForm} from './recipes';
import {CreActionBarModule} from '../shared/components/action-bar/action-bar.module';
@NgModule({
declarations: [
ListComponent,
EditComponent,
ExploreComponent,
RecipeInfoComponent,
MixTableComponent,
@ -39,7 +38,8 @@ import {RecipeAdd, RecipeForm} from './recipes';
ImagesEditorComponent,
MixesCardComponent,
RecipeForm,
RecipeAdd
RecipeAdd,
RecipeEdit
],
exports: [
UnitSelectorComponent
@ -51,7 +51,8 @@ import {RecipeAdd, RecipeForm} from './recipes';
FormsModule,
MatSortModule,
CreInputsModule,
CreButtonsModule
CreButtonsModule,
CreActionBarModule
]
})
export class RecipesModule {

View File

@ -0,0 +1,6 @@
.recipe-wrapper > section
margin: 0 3rem 3rem
cre-form
margin-top: 0 !important

View File

@ -1,56 +1,27 @@
import {ErrorHandlingComponent, SubscribingComponent} from '../shared/components/subscribing.component';
import {Observable} from 'rxjs';
import {Observable, Subject} from 'rxjs';
import {ComboBoxEntry} from '../shared/components/inputs/inputs';
import {map, tap} from 'rxjs/operators';
import {RecipeService} from './services/recipe.service';
import {CompanyService} from '../company/service/company.service';
import {AppState} from '../shared/app-state';
import {ErrorService} from '../shared/service/error.service';
import {ErrorHandler, ErrorService} from '../shared/service/error.service';
import {ActivatedRoute, Router} from '@angular/router';
import {FormControl, Validators} from '@angular/forms';
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Recipe} from '../shared/model/recipe.model';
import {Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation} from '@angular/core';
import {Recipe, recipeMixCount, RecipeStep, recipeStepCount} from '../shared/model/recipe.model';
import {AccountService} from '../accounts/services/account.service';
import {Permission} from '../shared/model/user';
@Component({
selector: 'cre-recipe-add',
templateUrl: 'add.html'
})
export class RecipeAdd extends ErrorHandlingComponent {
controls: any
companyEntries$: Observable<ComboBoxEntry[]> = this.companyService.all.pipe(
map(companies => companies.map(c => new ComboBoxEntry(c.id, c.name))),
)
errorHandlers = [{
filter: error => error.type === `exists-recipe-company-name`,
messageProducer: error => `Une couleur avec le nom ${error.name} existe déjà pour la bannière ${error.company}`
}]
constructor(
private recipeService: RecipeService,
private companyService: CompanyService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
this.appState.title = 'Nouvelle couleur'
}
submit(recipe: Recipe) {
this.subscribe(
this.recipeService.save(recipe),
recipe => this.urlUtils.navigateTo(`/color/edit/${recipe.id}`)
)
}
}
import {AlertService} from '../shared/service/alert.service';
import {GroupService} from '../groups/services/group.service';
import {StepTableComponent} from './components/step-table/step-table.component';
import {anyMap} from '../shared/utils/map.utils';
@Component({
selector: 'recipe-form',
templateUrl: 'form.html'
templateUrl: 'form.html',
styleUrls: ['recipes.sass'],
encapsulation: ViewEncapsulation.None
})
export class RecipeForm extends SubscribingComponent {
@Input() recipe: Recipe | null
@ -77,14 +48,14 @@ export class RecipeForm extends SubscribingComponent {
this.fetchCompanies()
this.controls = {
name: new FormControl(null, Validators.required),
description: new FormControl(null, Validators.required),
color: new FormControl('#ffffff', Validators.required),
gloss: new FormControl(0, Validators.compose([Validators.required, Validators.min(0), Validators.max(100)])),
sample: new FormControl(null, Validators.compose([Validators.required, Validators.min(0)])),
approbationDate: new FormControl(null),
remark: new FormControl(null),
company: new FormControl(null, Validators.required)
name: new FormControl(this.recipe?.name, Validators.required),
description: new FormControl(this.recipe?.description, Validators.required),
color: new FormControl(this.recipe?.color ?? '#ffffff', Validators.required),
gloss: new FormControl(this.recipe?.gloss ?? 0, Validators.compose([Validators.required, Validators.min(0), Validators.max(100)])),
sample: new FormControl(this.recipe?.sample, Validators.compose([Validators.required, Validators.min(0)])),
approbationDate: new FormControl(this.recipe?.approbationDate),
remark: new FormControl(this.recipe?.remark),
company: new FormControl({value: this.recipe?.company.id, disabled: !!this.recipe}, Validators.required)
}
}
@ -96,8 +67,12 @@ export class RecipeForm extends SubscribingComponent {
}
submit() {
this.submitForm.emit({
id: this.recipe?.id,
this.submitForm.emit(this.updatedRecipe)
}
get updatedRecipe(): Recipe {
return {
...this.recipe,
name: this.controls.name.value,
description: this.controls.description.value,
color: this.controls.color.value,
@ -106,14 +81,128 @@ export class RecipeForm extends SubscribingComponent {
approbationDate: this.controls.approbationDate.value,
remark: this.controls.remark.value,
company: this.controls.company.value,
mixes: this.recipe?.mixes,
approbationExpired: this.recipe?.approbationExpired,
groupsInformation: this.recipe?.groupsInformation,
imagesUrls: this.recipe?.imagesUrls
})
}
}
get hasCompanyEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_COMPANIES)
}
}
@Component({
selector: 'cre-recipe-add',
templateUrl: 'add.html'
})
export class RecipeAdd extends ErrorHandlingComponent {
errorHandlers = [{
filter: error => error.type === `exists-recipe-company-name`,
messageProducer: error => `Une couleur avec le nom ${error.name} existe déjà pour la bannière ${error.company}`
}]
constructor(
private recipeService: RecipeService,
private companyService: CompanyService,
private appState: AppState,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
this.appState.title = 'Nouvelle couleur'
}
submit(recipe: Recipe) {
this.subscribe(
this.recipeService.save(recipe),
recipe => this.urlUtils.navigateTo(`/color/edit/${recipe.id}`)
)
}
}
@Component({
selector: 'cre-recipe-edit',
templateUrl: 'edit.html',
styleUrls: ['recipes.sass']
})
export class RecipeEdit extends ErrorHandlingComponent {
@ViewChild(StepTableComponent) stepTable: StepTableComponent
@ViewChild(RecipeForm) form: RecipeForm
recipe: Recipe
groups$ = this.groupService.all
units$ = new Subject<string>()
errorHandlers: ErrorHandler[] = [{
filter: error => error.type === 'notfound-recipe-id',
consumer: _ => this.urlUtils.navigateTo('/color/list')
}]
constructor(
private recipeService: RecipeService,
private companyService: CompanyService,
private groupService: GroupService,
private appState: AppState,
private alertService: AlertService,
errorService: ErrorService,
router: Router,
activatedRoute: ActivatedRoute
) {
super(errorService, activatedRoute, router)
this.fetchRecipe()
}
private fetchRecipe() {
const recipeId = this.urlUtils.parseIntUrlParam('id')
this.subscribe(
this.recipeService.getById(recipeId),
recipe => {
this.recipe = recipe
this.appState.title = `${recipe.name} (Modifications)`
if (recipeMixCount(this.recipe) == 0) {
this.alertService.pushWarning('Il n\'y a aucun mélange dans cette recette')
}
if (recipeStepCount(this.recipe) == 0) {
this.alertService.pushWarning('Il n\'y a aucune étape dans cette recette')
}
},
true,
1
)
}
changeUnits(unit: string) {
this.units$.next(unit)
}
submit() {
const recipe = this.form.updatedRecipe
const steps = this.stepTable.mappedUpdatedSteps
if (!this.stepsPositionsAreValid(steps)) {
this.alertService.pushError('Les étapes ne peuvent pas avoir une position inférieure à 1')
return
}
this.subscribeAndNavigate(
this.recipeService.update(recipe, steps),
'/color/list'
)
}
delete() {
this.subscribeAndNavigate(
this.recipeService.delete(this.recipe.id),
'/color/list'
)
}
get loggedInUserGroupId(): number {
return this.appState.authenticatedUser.group?.id
}
private stepsPositionsAreValid(steps: Map<number, RecipeStep[]>): boolean {
return !anyMap(steps, (groupId, steps) => !!steps.find(s => s.position === 0))
}
}

View File

@ -48,11 +48,10 @@ export class RecipeService {
return this.api.post<Recipe>('/recipe', body)
}
update(id: number, name: string, description: string, color: string, gloss: number, sample: number, approbationDate: string, remark: string, steps: Map<number, RecipeStep[]>) {
const body = {id, name, description, color, gloss, sample, remark, steps: []}
if (approbationDate) {
// @ts-ignore
body.approbationDate = approbationDate
update(recipe: Recipe, steps: Map<number, RecipeStep[]>) {
const body = {
...recipe,
steps: []
}
steps.forEach((groupSteps, groupId) => {

View File

@ -156,11 +156,20 @@ export class CreComboBoxComponent implements OnInit {
next: entries => {
this._entries = entries
this.internalControl.setValue(this.findEntryByKey(this.control.value)?.value)
if (this.control.value) {
this.internalControl.setValue(this.findEntryByKey(this.control.value)?.value)
}
if (this.internalControl.disabled) {
this.internalControl.disable()
}
}
})
this.internalControl = new FormControl(null, Validators.compose([this.control.validator, this.valueValidator()]))
this.internalControl = new FormControl({
value: null,
disabled: true
}, Validators.compose([this.control.validator, this.valueValidator()]))
this.internalControl.valueChanges.pipe(takeUntil(this._destroy$))
.subscribe({
next: value => {

View File

@ -1,3 +1,7 @@
export function anyMap<K, V>(map: Map<K, V>, predicate: (key: K, value: V) => boolean): boolean {
return filterMap(map, predicate).size > 0
}
export function filterMap<K, V>(map: Map<K, V>, predicate: (key: K, value: V) => boolean): Map<K, V> {
const filteredMap = new Map<K, V>()
map.forEach((value, key) => {