From e9590fc8ef7ad32f8518b635ea0b5bc16bd23898 Mon Sep 17 00:00:00 2001 From: Anton Gustafsson Date: Wed, 27 Dec 2023 20:32:08 +0100 Subject: [PATCH] feat: edit base cocktails (#461) * feat: edit base cocktails * fix * chore: added eslint rule * fix * fix * fix * chore: update version --- .eslintrc.json | 11 +- android/app/build.gradle | 4 +- cypress/e2e/cocktails.cy.ts | 8 +- cypress/e2e/edit-base-cocktail.cy.ts | 136 +++++++++++++++ .../validation-helpers/validate-cocktail.ts | 34 ++++ .../android/en-US/changelogs/14600.txt | 2 + .../copy-to-clipboard/copy-to-clipboard.ts | 2 +- .../cocktail-dialog/cocktail-dialog.html | 160 ++++++++---------- .../cocktail-dialog/cocktail-dialog.ts | 134 ++++++++++++--- .../cocktail-dialog/manage-ingredient-row.ts | 2 +- .../dialogs/cocktail-filter-dialog.ts | 4 +- src/components/dialogs/edit-tags-drawer.html | 9 +- src/components/icons/icon-image.html | 22 +++ src/components/index.ts | 3 +- src/converters/amount-format.ts | 3 +- src/data/cocktail-data.ts | 7 +- src/domain/entities/cocktail-information.ts | 18 +- src/domain/entities/cocktail.ts | 2 + src/functions/utils.ts | 9 + src/locales/en/translation.json | 4 +- .../dialogs/manage-cocktail-row-dialog.ts | 9 +- .../cocktails/filter-cocktails-helper.ts | 2 +- src/modules/user/contact/contact.ts | 2 +- .../ingredient-list-drawer.ts | 4 +- .../unshopped-count-value-converter.ts | 2 +- src/modules/user/tags/user-tag-drawer.ts | 4 +- src/services/cocktail-service.ts | 149 ++++++++++++---- src/services/dev-tools-service.ts | 2 +- tsconfig.json | 5 +- 29 files changed, 571 insertions(+), 182 deletions(-) create mode 100644 cypress/e2e/edit-base-cocktail.cy.ts create mode 100644 cypress/e2e/validation-helpers/validate-cocktail.ts create mode 100644 fastlane/metadata/android/en-US/changelogs/14600.txt create mode 100644 src/components/icons/icon-image.html diff --git a/.eslintrc.json b/.eslintrc.json index 57bf7280..5f76e0d9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,11 +9,12 @@ "prettier" ], "rules": { - "@typescript-eslint/consistent-type-definitions": [1, "type"], - "@typescript-eslint/no-unused-vars": [1], - "@typescript-eslint/no-empty-function": [1], - "@typescript-eslint/no-inferrable-types": [1], - "@typescript-eslint/no-extraneous-class": [1], + "@typescript-eslint/consistent-type-definitions": ["warn", "type"], + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-empty-function": "warn", + "@typescript-eslint/no-inferrable-types": "warn", + "@typescript-eslint/no-extraneous-class": "warn", + "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", "eqeqeq": ["error", "always", { "null": "ignore" }] }, diff --git a/android/app/build.gradle b/android/app/build.gradle index 4e3cb6fd..6a116720 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.moimob.drinkable" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 14501 - versionName "1.45.1" + versionCode 14600 + versionName "1.46.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/cypress/e2e/cocktails.cy.ts b/cypress/e2e/cocktails.cy.ts index 503c965f..7c70dfaa 100644 --- a/cypress/e2e/cocktails.cy.ts +++ b/cypress/e2e/cocktails.cy.ts @@ -13,8 +13,8 @@ describe('Cocktails', () => { cy.dataCy('save-cocktail').click(); - cy.dataCy('cocktail-name').should('have.class', 'input-error').type('Test Cocktail'); - cy.dataCy('cocktail-name').blur(); + cy.dataCy('cocktail-name-input').should('have.class', 'input-error').type('Test Cocktail'); + cy.dataCy('cocktail-name-input').blur(); cy.dataCy('cocktail-image').selectFile('static/images/balmoral.jpg', { force: true }); @@ -88,8 +88,8 @@ describe('Cocktails', () => { cy.dataCy('create-cocktail').click(); - cy.dataCy('cocktail-name').type('Test Cocktail'); - cy.dataCy('cocktail-name').blur(); + cy.dataCy('cocktail-name-input').type('Test Cocktail'); + cy.dataCy('cocktail-name-input').blur(); cy.dataCy('cocktail-image').selectFile('static/images/balmoral.jpg', { force: true }); cy.dataCy('textarea').type('Test Instructions'); cy.dataCy('save-cocktail').click(); diff --git a/cypress/e2e/edit-base-cocktail.cy.ts b/cypress/e2e/edit-base-cocktail.cy.ts new file mode 100644 index 00000000..c3f3357a --- /dev/null +++ b/cypress/e2e/edit-base-cocktail.cy.ts @@ -0,0 +1,136 @@ +import { ValidateCocktailRequest, validateCocktail } from './validation-helpers/validate-cocktail'; + +describe('Edit Base Cocktails', () => { + beforeEach(() => { + window.localStorage.clear(); + window.localStorage.setItem('CapacitorStorage.messuarement-system', 'Imperial'); + }); + + it('Edit base cocktail with new values', () => { + cy.visit('#/cocktails'); + + const cocktailId = '2'; + cy.get(`#all-cocktails-${cocktailId}`).click(); + + validateCocktail({ + name: 'Gin & Tonic', + category: 'Cocktail', + tags: [], + abv: '9.38%', + ingredients: [ + { name: 'Gin', amount: '1 1/3 fl oz' }, + { name: 'Tonic', amount: '4 fl oz' } + ], + isEditedText: false + }); + + cy.dataCy('cocktail-dialog-dropdown-content').should('not.be.visible'); + + cy.dataCy('cocktail-dialog-dropdown').click(); + cy.dataCy('cocktail-dialog-dropdown-content').should('be.visible'); + cy.dataCy('dropdown-edit-cocktail').click(); + + cy.dataCy('cocktail-dialog-dropdown-content').should('not.be.visible'); + + cy.dataCy('cocktail-category-select').select('Other'); + + cy.dataCy('edit-tags').click(); + + cy.dataCy('tag-1').click(); + cy.dataCy('edit-tags-drawer-close').click(); + + cy.dataCy(['ingredient-group-0', 'amount-input']).clear(); + cy.dataCy(['ingredient-group-0', 'amount-input']).type('2'); + cy.dataCy(['ingredient-group-0', 'amount-input']).blur(); + + cy.dataCy('save-cocktail').click(); + + const validateCocktailRequest: ValidateCocktailRequest = { + name: 'Gin & Tonic', + category: 'Other', + tags: ['IBA'], + ingredients: [ + { name: 'Gin', amount: '2 fl oz' }, + { name: 'Tonic', amount: '4 fl oz' } + ], + abv: '12.5%', + isEditedText: true + }; + + validateCocktail(validateCocktailRequest); + + // Validate that when dialog is reopened, the latest values are still there + cy.dataCy('close-dialog').click(); + cy.get(`#all-cocktails-${cocktailId}`).click(); + validateCocktail(validateCocktailRequest); + }); + + it('Restore Cocktail', () => { + window.localStorage.setItem( + 'CapacitorStorage.cocktail-information', + JSON.stringify([ + { + id: '2', + category: 2, + ingredientGroups: [ + { + amount: '2', + ingredientId: '6', + unit: 'fl oz' + }, + { + amount: '4', + ingredientId: '7', + unit: 'fl oz' + } + ], + tags: ['1'] + } + ]) + ); + + cy.visit('#/cocktails'); + + const cocktailId = '2'; + cy.get(`#all-cocktails-${cocktailId}`).click(); + + validateCocktail({ + name: 'Gin & Tonic', + category: 'Other', + tags: ['IBA'], + ingredients: [ + { name: 'Gin', amount: '2 fl oz' }, + { name: 'Tonic', amount: '4 fl oz' } + ], + abv: '12.5%', + isEditedText: true + }); + + cy.dataCy('cocktail-dialog-dropdown-content').should('not.be.visible'); + + cy.dataCy('cocktail-dialog-dropdown').click(); + cy.dataCy('cocktail-dialog-dropdown-content').should('be.visible'); + cy.dataCy('dropdown-restore-cocktail').click(); + + cy.dataCy('cocktail-dialog-dropdown-content').should('not.be.visible'); + + const validateCocktailRequest = { + name: 'Gin & Tonic', + category: 'Cocktail', + tags: [], + abv: '9.38%', + ingredients: [ + { name: 'Gin', amount: '1 1/3 fl oz' }, + { name: 'Tonic', amount: '4 fl oz' } + ], + isEditedText: false + }; + + validateCocktail(validateCocktailRequest); + + // Validate that when dialog is reopened, the latest values are still there + cy.dataCy('close-dialog').click(); + cy.get(`#all-cocktails-${cocktailId}`).click(); + validateCocktail(validateCocktailRequest); + }); +}); diff --git a/cypress/e2e/validation-helpers/validate-cocktail.ts b/cypress/e2e/validation-helpers/validate-cocktail.ts new file mode 100644 index 00000000..4258313a --- /dev/null +++ b/cypress/e2e/validation-helpers/validate-cocktail.ts @@ -0,0 +1,34 @@ +export type ValidateCocktailRequest = { + name: string; + category: string; + tags: string[]; + abv: string; + ingredients: { name: string; amount: string }[]; + isEditedText: boolean; +}; + +export function validateCocktail(cocktail: ValidateCocktailRequest) { + const { name, category, isEditedText, tags, ingredients, abv } = cocktail; + + cy.dataCy('cocktail-name').should('have.text', name); + cy.dataCy('cocktail-category').should('have.text', category); + + if (isEditedText) { + cy.dataCy('cocktail-edited-text').should('exist'); + } else { + cy.dataCy('cocktail-edited-text').should('not.exist'); + } + + cy.get('[data-cy=cocktail-tags] > p').should('have.length', tags.length); + tags.forEach(tag => { + cy.get('[data-cy=cocktail-tags] > p').should('include.text', tag); + }); + + for (let i = 0; i < ingredients.length; i++) { + const element = ingredients[i]; + cy.get(`[data-cy=ingredient-group-${i}] [data-cy=ingredient-name]`).should('include.text', element.name); + cy.get(`[data-cy=ingredient-group-${i}] [data-cy=ingredient-amount]`).should('include.text', element.amount); + } + + cy.dataCy('cocktail-abv').should('include.text', abv); +} diff --git a/fastlane/metadata/android/en-US/changelogs/14600.txt b/fastlane/metadata/android/en-US/changelogs/14600.txt new file mode 100644 index 00000000..680292c7 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/14600.txt @@ -0,0 +1,2 @@ +• You can now edit base recipes +• Updated German translations \ No newline at end of file diff --git a/src/components/copy-to-clipboard/copy-to-clipboard.ts b/src/components/copy-to-clipboard/copy-to-clipboard.ts index 90d22ee7..0d85f9d1 100644 --- a/src/components/copy-to-clipboard/copy-to-clipboard.ts +++ b/src/components/copy-to-clipboard/copy-to-clipboard.ts @@ -6,7 +6,7 @@ export class CopyToClipboard { public textCopied = false; async copyToClipboard() { - if (this.textCopied === true) { + if (this.textCopied) { return; } diff --git a/src/components/dialogs/cocktail-dialog/cocktail-dialog.html b/src/components/dialogs/cocktail-dialog/cocktail-dialog.html index 8dfaf098..40220a31 100644 --- a/src/components/dialogs/cocktail-dialog/cocktail-dialog.html +++ b/src/components/dialogs/cocktail-dialog/cocktail-dialog.html @@ -6,34 +6,56 @@ class="h-6 w-6 mr-1" data-cy="close-dialog"> -

${cocktail.name}

-
+
+ data-cy="cocktail-name-input" />
-
@@ -107,21 +107,10 @@

${cocktail.name}

- - Close - - + + @@ -132,12 +121,14 @@

${cocktail.name}

+ t.bind="cocktailCategories[cocktail.category].translation" + data-cy="cocktail-category"> + class="input w-full input-sm bg-neutral mx-1" + data-cy="amount-input" /> +
+

${cocktail.instructions}

+

+
-
-

- - Trash - - - - -
-
diff --git a/src/components/dialogs/cocktail-dialog/cocktail-dialog.ts b/src/components/dialogs/cocktail-dialog/cocktail-dialog.ts index dd66677f..e75e24ef 100644 --- a/src/components/dialogs/cocktail-dialog/cocktail-dialog.ts +++ b/src/components/dialogs/cocktail-dialog/cocktail-dialog.ts @@ -2,8 +2,8 @@ import { Cocktail, ExtendedIngredientGroup, IngredientGroup } from 'domain/entit import { DialogController, DialogService } from 'aurelia-dialog'; import { LocalStorageService } from 'services/local-storage-service'; import { DrinkCategory, getDrinkCategories } from 'domain/enums/drink-category'; -import { CocktailService } from 'services/cocktail-service'; -import { inject, NewInstance, observable } from 'aurelia-framework'; +import { CocktailService, UpdateCocktailInformationRequest } from 'services/cocktail-service'; +import { inject, NewInstance, observable, computedFrom } from 'aurelia-framework'; import { ValidationRules, ValidationController } from 'aurelia-validation'; import Compressor from 'compressorjs'; import { getUnitsForImperial, getUnitsForMetric, Unit } from 'domain/enums/unit'; @@ -15,15 +15,18 @@ import { EnumTranslationModel } from 'domain/models/enum-translation-model'; import { getTagsFromIds } from 'data/tags-data'; import { EditTagsDrawer } from './../edit-tags-drawer'; import { TagModel } from 'domain/entities/cocktail-tag'; -import { CocktailAlcoholInformation } from 'domain/cocktail-alcohol-information'; import { ManageIngredientRow } from './manage-ingredient-row'; +import { getStaticCocktailById } from 'data/cocktail-data'; +import { isEqual } from 'functions/utils'; +import { AmountFormatValueConverter } from 'converters/amount-format'; @inject( DialogController, LocalStorageService, CocktailService, NewInstance.of(ValidationController), IngredientService, - DialogService + DialogService, + AmountFormatValueConverter ) export class CocktailDialog { @observable public searchFilter: string; @@ -42,7 +45,6 @@ export class CocktailDialog { public searchElement: HTMLElement; public imageInput: HTMLInputElement; public tags: TagModel[] = []; - public alcoholInfo: CocktailAlcoholInformation; public noteState: 'none' | 'edit' | 'exists' = 'none'; public preferCl: boolean; @@ -54,6 +56,7 @@ export class CocktailDialog { private _ingredients: Ingredient[] = []; private _clickedIngredientIndex; + private _messuarementSystem: MessuarementSystem; handleInputBlur: (e: FocusEvent) => void; updateImageDisplay: (e: InputEvent) => void; @@ -65,7 +68,8 @@ export class CocktailDialog { private _cocktailService: CocktailService, private _validationController: ValidationController, private _ingredientService: IngredientService, - private _dialogService: DialogService + private _dialogService: DialogService, + private _amountFormat: AmountFormatValueConverter ) { this.controller = dialogContoller; this.handleInputBlur = () => { @@ -105,7 +109,7 @@ export class CocktailDialog { this.isEditMode = true; this.isNewCocktail = true; } else { - this.cocktail = cocktail; + this.cocktail = { ...cocktail }; } this.isUserCreatedCocktail = this.cocktail.id === undefined || this.cocktail.id?.includes('x-'); @@ -126,10 +130,10 @@ export class CocktailDialog { this.extendedIngredientGroup.push(ingredientGroup); } - const messuarementSystem = this._localStorageService.getMessuarementSystem(); + this._messuarementSystem = this._localStorageService.getMessuarementSystem(); this.ingredientUnits = - messuarementSystem === MessuarementSystem.Imperial ? getUnitsForImperial() : getUnitsForMetric(); + this._messuarementSystem === MessuarementSystem.Imperial ? getUnitsForImperial() : getUnitsForMetric(); this._ingredients = this._ingredientService.getIngredients(); this.filteredIngredientTags = this._ingredients.filter( @@ -163,6 +167,30 @@ export class CocktailDialog { }); } + @computedFrom('cocktail.isEdited') + public get isCocktailEdited(): boolean { + console.log(this.cocktail.isEdited); + return this.cocktail.isEdited; + } + + async restoreCocktail() { + this.closeDetailsElement(); + + this.cocktail = await this._cocktailService.restoreCocktail(this.cocktail); + + const ingredientIds = this._localStorageService.getIngredientIds(); + this.extendedIngredientGroup = this._ingredientService.toExtendedIngredientGroup( + this.cocktail.ingredientGroups, + ingredientIds + ); + + this.tags = getTagsFromIds(this.cocktail.tags); + } + + private closeDetailsElement() { + this.detailsElement?.attributes?.removeNamedItem('open'); + } + editTags() { this._dialogService .open({ viewModel: EditTagsDrawer, model: this.tags.map(x => x.id), lock: false }) @@ -195,9 +223,15 @@ export class CocktailDialog { this.cocktail.rating = newValue; - this.isUserCreatedCocktail - ? await this._cocktailService.updateCocktail(this.cocktail) - : await this._cocktailService.updateCocktailInformation(this.cocktail); + if (this.isUserCreatedCocktail) { + this.cocktail = await this._cocktailService.updateCocktail(this.cocktail); + return; + } + + const updateRequest = new UpdateCocktailInformationRequest(this.cocktail.id); + updateRequest.addField('rating', this.cocktail.rating !== 0 ? this.cocktail.rating : undefined); + + this.cocktail = await this._cocktailService.updateCocktailInformationByRequest(updateRequest); } clearRating() { @@ -270,20 +304,42 @@ export class CocktailDialog { this.cocktail.isFavorite = !this.cocktail.isFavorite; if (this.isUserCreatedCocktail) { - await this._cocktailService.updateCocktail(this.cocktail); + this.cocktail = await this._cocktailService.updateCocktail(this.cocktail); } else { - await this._cocktailService.updateCocktailInformation(this.cocktail); + const updateRequest = new UpdateCocktailInformationRequest(this.cocktail.id); + updateRequest.addField('isFavorite', this.cocktail.isFavorite ? this.cocktail.isFavorite : undefined); + + this.cocktail = await this._cocktailService.updateCocktailInformationByRequest(updateRequest); } } editCocktail() { - this.detailsElement?.attributes?.removeNamedItem('open'); - - this.isEditMode = true; + this.closeDetailsElement(); this.extendedIngredientGroup.forEach(element => { element.isChecked = false; }); + + if (!this.isUserCreatedCocktail && this._messuarementSystem === MessuarementSystem.Imperial) { + this.extendedIngredientGroup.forEach(element => { + if (element.amount == null) { + return; + } + + const newUnit = this._amountFormat.getUnit(element.unit as Unit, this._messuarementSystem); + const unitMultiplier = this._amountFormat.getUnitMultiplier( + element.unit as Unit, + this._messuarementSystem + ); + + const newValue = +parseFloat((Number(element.amount) * unitMultiplier).toString()).toFixed(2); + + element.unit = newUnit; + element.amount = newValue.toString(); + }); + } + + this.isEditMode = true; } async deleteCocktail() { @@ -332,15 +388,33 @@ export class CocktailDialog { return group; }); - this.cocktail.tags = this.tags.map(x => x.id); - this.cocktail.alcoholInformation = new CocktailAlcoholInformation( - this.cocktail, - this._ingredientService.getIngredients() - ); + this.cocktail.tags = this.tags?.map(x => x.id); - this.isNewCocktail - ? await this._cocktailService.createCocktail(this.cocktail) - : await this._cocktailService.updateCocktail(this.cocktail); + if (this.isUserCreatedCocktail) { + this.cocktail = this.isNewCocktail + ? await this._cocktailService.createCocktail(this.cocktail) + : await this._cocktailService.updateCocktail(this.cocktail); + } else { + const staticCocktail = getStaticCocktailById(this.cocktail.id); + + const updateRequest = new UpdateCocktailInformationRequest(this.cocktail.id); + updateRequest.addField( + 'category', + staticCocktail.category !== this.cocktail.category ? this.cocktail.category : undefined + ); + updateRequest.addField( + 'ingredientGroups', + !isEqual(staticCocktail.ingredientGroups, this.cocktail.ingredientGroups) + ? this.cocktail.ingredientGroups + : undefined + ); + updateRequest.addField( + 'tags', + !isEqual(staticCocktail.tags, this.cocktail.tags) ? this.cocktail.tags : undefined + ); + + this.cocktail = await this._cocktailService.updateCocktailInformationByRequest(updateRequest); + } this.isEditMode = false; @@ -376,9 +450,13 @@ export class CocktailDialog { if (this.isUserCreatedCocktail) { await this._cocktailService.updateCocktail(this.cocktail); } else { - console.log(this.cocktail.notes); - console.log('hej'); - await this._cocktailService.updateCocktailInformation(this.cocktail); + const updateRequest = new UpdateCocktailInformationRequest(this.cocktail.id); + updateRequest.addField( + 'notes', + this.cocktail.notes != null && this.cocktail.notes !== '' ? this.cocktail.notes : undefined + ); + + await this._cocktailService.updateCocktailInformationByRequest(updateRequest); } if (this.cocktail.notes?.length > 0) { diff --git a/src/components/dialogs/cocktail-dialog/manage-ingredient-row.ts b/src/components/dialogs/cocktail-dialog/manage-ingredient-row.ts index 40859dc7..1515d699 100644 --- a/src/components/dialogs/cocktail-dialog/manage-ingredient-row.ts +++ b/src/components/dialogs/cocktail-dialog/manage-ingredient-row.ts @@ -38,7 +38,7 @@ export class ManageIngredientRow { async toggleIngredientStorageStatus() { this.isInStorage = !this.isInStorage; - if (this.isInStorage === true) { + if (this.isInStorage) { this._savedIngredientIds.push(this._ingredientId); } else { this._savedIngredientIds = this._savedIngredientIds.filter(x => x !== this._ingredientId); diff --git a/src/components/dialogs/cocktail-filter-dialog.ts b/src/components/dialogs/cocktail-filter-dialog.ts index 29a4e181..4b85cd51 100644 --- a/src/components/dialogs/cocktail-filter-dialog.ts +++ b/src/components/dialogs/cocktail-filter-dialog.ts @@ -56,7 +56,7 @@ export class CocktailFilterDialog { this.spiritFilter !== null || this.ingredientFilter !== null || this.tags.find(x => x.isActive) !== undefined || - this.favoriteFilter === true || + this.favoriteFilter || this.alcoholFilter !== null ); } @@ -67,7 +67,7 @@ export class CocktailFilterDialog { const response: CocktailFilterDialogModel = { spiritFilter: this.spiritFilter, categoryFilter: this.categoryFilter, - favoriteFilter: this.favoriteFilter === true ? true : null, + favoriteFilter: this.favoriteFilter ? true : null, ingredientFilter: this.ingredientFilter, tagFilter: tags.length > 0 ? tags : null, alcoholFilter: this.alcoholFilter diff --git a/src/components/dialogs/edit-tags-drawer.html b/src/components/dialogs/edit-tags-drawer.html index fd87371c..aaba9403 100644 --- a/src/components/dialogs/edit-tags-drawer.html +++ b/src/components/dialogs/edit-tags-drawer.html @@ -11,11 +11,16 @@

+ tag.bind="tag" + data-cy="tag-${tag.id}">
- +
diff --git a/src/components/icons/icon-image.html b/src/components/icons/icon-image.html new file mode 100644 index 00000000..0eb0f286 --- /dev/null +++ b/src/components/icons/icon-image.html @@ -0,0 +1,22 @@ + diff --git a/src/components/index.ts b/src/components/index.ts index 124173df..ed206572 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -35,6 +35,7 @@ export function configure(config: FrameworkConfiguration) { PLATFORM.moduleName('./copy-to-clipboard/copy-to-clipboard'), PLATFORM.moduleName('./icons/icon-arrow-forward.html'), PLATFORM.moduleName('./icons/icon-chevron-forward.html'), - PLATFORM.moduleName('./icons/icon-menu.html') + PLATFORM.moduleName('./icons/icon-menu.html'), + PLATFORM.moduleName('./icons/icon-image.html') ]); } diff --git a/src/converters/amount-format.ts b/src/converters/amount-format.ts index cd478ae9..1668dd68 100644 --- a/src/converters/amount-format.ts +++ b/src/converters/amount-format.ts @@ -9,7 +9,7 @@ export class AmountFormatValueConverter { constructor(private _localStorageService: LocalStorageService) {} toView(value: string, multiplier: number, unit: Unit, preferCl: boolean) { - if (value === '' || value === undefined) { + if (value === '' || value == null) { return value; } @@ -20,6 +20,7 @@ export class AmountFormatValueConverter { const newValue = +parseFloat((Number(value) * multiplier * unitMultiplier).toString()).toFixed(2); + console.log(value, multiplier, unit, system, unitMultiplier, newUnit, newValue); const fraction = convertToFraction(newValue); if (preferCl && newUnit === Unit.ML && system === MessuarementSystem.Metric) { diff --git a/src/data/cocktail-data.ts b/src/data/cocktail-data.ts index 78eeb264..aa04b7d8 100644 --- a/src/data/cocktail-data.ts +++ b/src/data/cocktail-data.ts @@ -8,6 +8,10 @@ export function getStaticCocktails() { return [...cocktails]; } +export function getStaticCocktailById(id: string) { + return { ...cocktails.find(x => x.id === id) }; +} + export function toCocktailWithMissingIngredients( cocktail: Cocktail, ingredient: Ingredient @@ -25,7 +29,8 @@ export function toCocktailWithMissingIngredients( notes: cocktail.notes, isFavorite: cocktail.isFavorite, rating: cocktail.rating, - tags: cocktail.tags + tags: cocktail.tags, + isEdited: cocktail.isEdited }; } diff --git a/src/domain/entities/cocktail-information.ts b/src/domain/entities/cocktail-information.ts index c8b5e254..20dd983a 100644 --- a/src/domain/entities/cocktail-information.ts +++ b/src/domain/entities/cocktail-information.ts @@ -1,6 +1,12 @@ -export class CocktailInformation { - public id: string; - public rating?: number; - public isFavorite?: boolean; - public notes?: string; -} +import { DrinkCategory } from 'domain/enums/drink-category'; +import { IngredientGroup } from './cocktail'; + +export type CocktailInformation = { + id: string; + rating?: number; + isFavorite?: boolean; + notes?: string; + category?: DrinkCategory; + tags?: string[]; + ingredientGroups?: IngredientGroup[]; +}; diff --git a/src/domain/entities/cocktail.ts b/src/domain/entities/cocktail.ts index eea56ba4..3b67c29f 100644 --- a/src/domain/entities/cocktail.ts +++ b/src/domain/entities/cocktail.ts @@ -19,9 +19,11 @@ export class Cocktail extends StaticCocktail { this.ingredientGroups = []; this.tags = []; this.notes = ''; + this.isEdited = false; } name: string; notes: string; + isEdited: boolean; instructions?: string; rating?: number; isFavorite?: boolean; diff --git a/src/functions/utils.ts b/src/functions/utils.ts index a4aa0644..9fcadb73 100644 --- a/src/functions/utils.ts +++ b/src/functions/utils.ts @@ -57,3 +57,12 @@ function findClosestNumber(currentNumber: number, numbersArray: { value: number; export function formatToTwoDecimalsIfNeeded(value: number) { return value % 1 === 0 ? value : parseFloat(value.toFixed(2)); } + +export function isEqual(x, y) { + const ok = Object.keys, + tx = typeof x, + ty = typeof y; + return x && y && tx === 'object' && tx === ty + ? ok(x).length === ok(y).length && ok(x).every(key => isEqual(x[key], y[key])) + : x === y; +} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index c8954506..6c797258 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -170,5 +170,7 @@ "preferred-unit": "Preferred Unit", "christmas-title": "Christmas!", "christmas-subtitle": "Explore christmas themed cocktails", - "edit-cocktail": "Edit Cocktail" + "edit-cocktail": "Edit Cocktail", + "restore-cocktail": "Restore Cocktail", + "base-cocktail-is-edited": "You have modified this recipe" } diff --git a/src/modules/cocktails/dialogs/manage-cocktail-row-dialog.ts b/src/modules/cocktails/dialogs/manage-cocktail-row-dialog.ts index bff88d50..cd3dccb9 100644 --- a/src/modules/cocktails/dialogs/manage-cocktail-row-dialog.ts +++ b/src/modules/cocktails/dialogs/manage-cocktail-row-dialog.ts @@ -1,7 +1,7 @@ import { DialogController } from 'aurelia-dialog'; import { autoinject } from 'aurelia-framework'; import { Cocktail } from 'domain/entities/cocktail'; -import { CocktailService } from 'services/cocktail-service'; +import { CocktailService, UpdateCocktailInformationRequest } from 'services/cocktail-service'; import { LocalStorageService } from 'services/local-storage-service'; @autoinject @@ -29,9 +29,12 @@ export class ManageCocktailRowDialog { this.cocktail.isFavorite = !this.cocktail.isFavorite; if (this.cocktail.id.includes('x-')) { - await this.cocktailService.updateCocktail(this.cocktail); + this.cocktail = await this.cocktailService.updateCocktail(this.cocktail); } else { - await this.cocktailService.updateCocktailInformation(this.cocktail); + const updateRequest = new UpdateCocktailInformationRequest(this.cocktail.id); + updateRequest.addField('isFavorite', this.cocktail.isFavorite ? true : undefined); + + this.cocktail = await this.cocktailService.updateCocktailInformationByRequest(updateRequest); } } } diff --git a/src/modules/cocktails/filter-cocktails-helper.ts b/src/modules/cocktails/filter-cocktails-helper.ts index 93af3a94..f7f33a9c 100644 --- a/src/modules/cocktails/filter-cocktails-helper.ts +++ b/src/modules/cocktails/filter-cocktails-helper.ts @@ -43,7 +43,7 @@ export function filterCocktailList(request: FilterCocktailRequest) { } if (request.filterDialogModel.favoriteFilter !== null) { - cocktails = cocktails.filter(x => x.isFavorite === true); + cocktails = cocktails.filter(x => x.isFavorite); filterCount++; } diff --git a/src/modules/user/contact/contact.ts b/src/modules/user/contact/contact.ts index 57d9e42e..e50b9468 100644 --- a/src/modules/user/contact/contact.ts +++ b/src/modules/user/contact/contact.ts @@ -39,7 +39,7 @@ export class Contact { } public async submit(): Promise { - if (this.isBusy === true) { + if (this.isBusy) { return; } diff --git a/src/modules/user/ingredient-lists/ingredient-list-drawer.ts b/src/modules/user/ingredient-lists/ingredient-list-drawer.ts index 742ec8ff..008ba99e 100644 --- a/src/modules/user/ingredient-lists/ingredient-list-drawer.ts +++ b/src/modules/user/ingredient-lists/ingredient-list-drawer.ts @@ -42,7 +42,7 @@ export class IngredientListDrawer { return; } - if (this.isNew === true) { + if (this.isNew) { await this.localStorageService.createIngredientList(this.name); this._dialogController.ok(); return; @@ -60,7 +60,7 @@ export class IngredientListDrawer { } async delete() { - if (this.isNew === true) { + if (this.isNew) { return; } diff --git a/src/modules/user/shopping-list/unshopped-count-value-converter.ts b/src/modules/user/shopping-list/unshopped-count-value-converter.ts index 7b7887e5..f3b81aa6 100644 --- a/src/modules/user/shopping-list/unshopped-count-value-converter.ts +++ b/src/modules/user/shopping-list/unshopped-count-value-converter.ts @@ -2,6 +2,6 @@ import { ShoppingListIngredient } from './shopping-list-models'; export class UnshoppedCountValueConverter { toView(value: ShoppingListIngredient[]) { - return value.filter(x => x.shopped === false).length; + return value.filter(x => !x.shopped).length; } } diff --git a/src/modules/user/tags/user-tag-drawer.ts b/src/modules/user/tags/user-tag-drawer.ts index 79a309b1..aa8af436 100644 --- a/src/modules/user/tags/user-tag-drawer.ts +++ b/src/modules/user/tags/user-tag-drawer.ts @@ -41,7 +41,7 @@ export class UserTagDrawer { return; } - if (this.isNew === true) { + if (this.isNew) { await this.cocktailService.createTag(this.name); this._dialogController.ok(); return; @@ -61,7 +61,7 @@ export class UserTagDrawer { } async delete() { - if (this.isNew === true) { + if (this.isNew) { return; } diff --git a/src/services/cocktail-service.ts b/src/services/cocktail-service.ts index b6dc587d..efc9deef 100644 --- a/src/services/cocktail-service.ts +++ b/src/services/cocktail-service.ts @@ -1,7 +1,7 @@ import { LocalStorageService } from './local-storage-service'; import { autoinject } from 'aurelia-framework'; import { Cocktail, CocktailWithMissingIngredient } from 'domain/entities/cocktail'; -import { getStaticCocktails, toCocktailWithMissingIngredients } from 'data/cocktail-data'; +import { getStaticCocktailById, getStaticCocktails, toCocktailWithMissingIngredients } from 'data/cocktail-data'; import { IngredientService } from './ingredient-service'; import { CocktailInformation } from 'domain/entities/cocktail-information'; import { DrinkCategory } from 'domain/enums/drink-category'; @@ -36,16 +36,19 @@ export class CocktailService { staticCocktails.forEach(element => { const cocktail: Cocktail = { id: element.id, - category: element.category, + category: this._cocktailInformation.find(x => x.id === element.id)?.category ?? element.category, imageSrc: element.imageSrc, - ingredientGroups: element.ingredientGroups, + ingredientGroups: + this._cocktailInformation.find(x => x.id === element.id)?.ingredientGroups ?? + element.ingredientGroups, isImagePortrait: element.isImagePortrait, name: this.i18n.tr(element.translation, { ns: 'cocktails' }), notes: this._cocktailInformation.find(x => x.id === element.id)?.notes ?? '', - tags: element.tags, + tags: this._cocktailInformation.find(x => x.id === element.id)?.tags ?? element.tags, translation: element.translation, isFavorite: this._cocktailInformation.find(x => x.id === element.id)?.isFavorite ?? false, - rating: this._cocktailInformation.find(x => x.id === element.id)?.rating ?? 0 + rating: this._cocktailInformation.find(x => x.id === element.id)?.rating ?? 0, + isEdited: this.isCocktailEdited(this._cocktailInformation.find(x => x.id === element.id)) }; cocktail.alcoholInformation = new CocktailAlcoholInformation(cocktail, ingredients); @@ -62,18 +65,14 @@ export class CocktailService { x.tags = x.tags !== undefined ? x.tags : []; x.notes = x.notes !== undefined ? x.notes : ''; x.alcoholInformation = new CocktailAlcoholInformation(x, ingredients); - - // Created Cocktails saved isFavorite in CocktailInformation before so this is for backwards compatibility - if (x.isFavorite === undefined) { - x.isFavorite = this._cocktailInformation.find(x => x.id === x.id)?.isFavorite ?? false; - } + x.isEdited = false; this._cocktails.push(x); }); this._mocktails = this._cocktails.filter(x => x.category === DrinkCategory.Mocktail); - if (this._localStorageService.getSettings().showMocktails !== true) { + if (!this._localStorageService.getSettings().showMocktails) { this.hideMocktails(); } @@ -117,7 +116,7 @@ export class CocktailService { public getLatestCocktails(amount: number) { return [...this._cocktails] - .filter(x => x.id.includes('x') === false) + .filter(x => !x.id.includes('x')) .slice(amount * -1) .reverse(); } @@ -129,7 +128,7 @@ export class CocktailService { const cocktailIngredients = element.ingredientGroups.map(x => x.ingredientId); const result = [...new Set(cocktailIngredients.map(x => this.ingredientIdExists(ingredientIds, x)))]; - if (result.length === 1 && result[0] === true) { + if (result.length === 1 && result[0]) { validCocktails.push(element); } }); @@ -172,11 +171,49 @@ export class CocktailService { ); } - public async createCocktail(cocktail: Cocktail) { + public async createCocktail(cocktail: Cocktail): Promise { cocktail.id = this.setCocktailId(); + cocktail.alcoholInformation = new CocktailAlcoholInformation( + cocktail, + this._ingredientService.getIngredients() + ); + cocktail.isEdited = false; + this._createdCocktails.push(cocktail); this._cocktails.push(cocktail); await this._localStorageService.updateCocktails(this._createdCocktails); + return cocktail; + } + + public async restoreCocktail(cocktail: Cocktail): Promise { + this._cocktailInformation = this._cocktailInformation.filter(x => x.id !== cocktail.id); + this._cocktailInformation.push({ + id: cocktail.id, + rating: cocktail.rating, + isFavorite: cocktail.isFavorite, + notes: cocktail.notes, + category: undefined, + tags: undefined, + ingredientGroups: undefined + }); + + const cocktailtoUpdate = this._cocktails.find(x => x.id === cocktail.id); + const staticCocktail = getStaticCocktails().find(x => x.id === cocktail.id); + if (cocktailtoUpdate != null && staticCocktail != null) { + cocktailtoUpdate.category = staticCocktail.category; + cocktailtoUpdate.tags = staticCocktail.tags; + cocktailtoUpdate.ingredientGroups = staticCocktail.ingredientGroups; + cocktailtoUpdate.isEdited = false; + + cocktailtoUpdate.alcoholInformation = new CocktailAlcoholInformation( + cocktailtoUpdate, + this._ingredientService.getIngredients() + ); + } + + await this._localStorageService.updateCocktailInformation(this._cocktailInformation); + + return cocktailtoUpdate; } public async createTag(name: string) { @@ -193,7 +230,7 @@ export class CocktailService { this._tags.push(newTag); } - public async updateCocktail(cocktail: Cocktail) { + public async updateCocktail(cocktail: Cocktail): Promise { this._createdCocktails = this._createdCocktails.filter(x => x.id !== cocktail.id); this._createdCocktails.push(cocktail); @@ -201,6 +238,8 @@ export class CocktailService { this._cocktails = this._cocktails.filter(x => x.id !== cocktail.id); this._cocktails.push(cocktail); + + return cocktail; } public async updateTag(tag: TagModel) { @@ -213,23 +252,35 @@ export class CocktailService { this._tags.push(tag); } - public async updateCocktailInformation(cocktail: Cocktail) { - this._cocktailInformation = this._cocktailInformation.filter(x => x.id !== cocktail.id); - this._cocktailInformation.push({ - id: cocktail.id, - rating: cocktail.rating, - isFavorite: cocktail.isFavorite, - notes: cocktail.notes + public async updateCocktailInformationByRequest( + updateRequest: UpdateCocktailInformationRequest + ): Promise { + let cocktailInformation = this._cocktailInformation.find(x => x.id === updateRequest.id); + if (cocktailInformation == null) { + cocktailInformation = { + id: updateRequest.id + }; + this._cocktailInformation.push(cocktailInformation); + } + + const cocktail = this._cocktails.find(x => x.id === updateRequest.id); + const staticCocktail = getStaticCocktailById(updateRequest.id); + + updateRequest.getFields().forEach(element => { + cocktailInformation[element.key.toString()] = element.value; + cocktail[element.key.toString()] = + element.value === undefined ? staticCocktail[element.key.toString()] : element.value; }); - const cocktailtoUpdate = this._cocktails.find(x => x.id === cocktail.id); - if (cocktailtoUpdate !== undefined) { - cocktailtoUpdate.isFavorite = cocktail.isFavorite; - cocktailtoUpdate.rating = cocktail.rating; - cocktailtoUpdate.notes = cocktail.notes; - } + cocktail.isEdited = this.isCocktailEdited(cocktailInformation); + cocktail.alcoholInformation = new CocktailAlcoholInformation( + cocktail, + this._ingredientService.getIngredients() + ); await this._localStorageService.updateCocktailInformation(this._cocktailInformation); + + return cocktail; } public updateTranslation() { @@ -256,7 +307,7 @@ export class CocktailService { } public updateShowMocktails(value: boolean) { - if (value === true) { + if (value) { this._cocktails.push(...this._mocktails); } else { this.hideMocktails(); @@ -273,6 +324,18 @@ export class CocktailService { return 'x-' + this._highestTagId; } + private isCocktailEdited(cocktailInformation?: CocktailInformation): boolean { + if (cocktailInformation == null) { + return false; + } + + const result = + cocktailInformation.tags !== undefined || + cocktailInformation.ingredientGroups !== undefined || + cocktailInformation.category !== undefined; + return result; + } + private getMissingIngredientsCount( cocktails: CocktailWithMissingIngredient[], cocktail: CocktailWithMissingIngredient @@ -298,3 +361,31 @@ export class CocktailService { this._cocktails = this._cocktails.filter(x => !this._mocktails.map(y => y.id).includes(x.id)); } } + +export class UpdateCocktailInformationRequest { + public id: string; + private fields: CocktailInformationUpdateField[] = []; + + constructor(id: string) { + this.fields = []; + this.id = id; + } + + addField(key: K, value: CocktailInformation[K]) { + this.fields.push({ + key, + value + }); + } + + getFields() { + return this.fields; + } +} + +export type CocktailInformationUpdateField = { + key: K; + value: CocktailInformation[K]; +}; + +type CocktailInformationUpdateProperties = Exclude; diff --git a/src/services/dev-tools-service.ts b/src/services/dev-tools-service.ts index c7059e51..c78e7d88 100644 --- a/src/services/dev-tools-service.ts +++ b/src/services/dev-tools-service.ts @@ -5,7 +5,7 @@ export class DevToolsService { public isDevelopment = false; constructor() { - if (PRODUCTION !== true) { + if (!PRODUCTION) { document.addEventListener('keydown', (event: KeyboardEvent) => { if (event.ctrlKey && event.key === '1') { this.isDevelopment = !this.isDevelopment; diff --git a/tsconfig.json b/tsconfig.json index 66d8b69a..4db6520a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,9 +11,10 @@ "target": "ES2020", "moduleResolution": "node", "baseUrl": "src", - "allowJs": true, "allowSyntheticDefaultImports": true, - "esModuleInterop": true + "esModuleInterop": true, + // "strict": true, + "noUncheckedIndexedAccess": true }, "include": ["src", "tests", "capacitor.config.ts", "jest.config.ts", "cypress.config.ts"] }