1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-05 05:25:26 +02:00

feat: duplicate recipes (#1750)

* feature/frontend: Add duplicate button to recipe

* feature/backend: Add recipe duplication endpoint

* feature/frontend: add duplication API call

* Regenerate API docs

* Fix linter errors

* Fix backend linter error

* Move recipe duplication logic to recipe service

* Add test for recipe duplication

* Improve recipe ingredients copy test

* generate types

* import type

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
Philipp 2022-12-01 06:57:26 +01:00 committed by GitHub
parent e73a72959c
commit 33dffccaa5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 258 additions and 25 deletions

View file

@ -54,6 +54,7 @@
delete: false,
edit: false,
download: true,
duplicate: true,
mealplanner: true,
shoppingList: true,
print: true,

View file

@ -13,6 +13,23 @@
{{ $t("recipe.delete-confirmation") }}
</v-card-text>
</BaseDialog>
<BaseDialog
v-model="recipeDuplicateDialog"
:title="$t('recipe.duplicate')"
color="primary"
:icon="$globals.icons.duplicate"
@confirm="duplicateRecipe()"
>
<v-card-text>
<v-text-field
v-model="recipeName"
dense
:label="$t('recipe.recipe-name')"
autofocus
@keyup.enter="duplicateRecipe()"
></v-text-field>
</v-card-text>
</BaseDialog>
<BaseDialog
v-model="mealplannerDialog"
:title="$t('recipe.add-recipe-to-mealplan')"
@ -136,6 +153,7 @@ export default defineComponent({
delete: true,
edit: true,
download: true,
duplicate: false,
mealplanner: true,
shoppingList: true,
print: true,
@ -199,6 +217,8 @@ export default defineComponent({
recipeDeleteDialog: false,
mealplannerDialog: false,
shoppingListDialog: false,
recipeDuplicateDialog: false,
recipeName: props.name,
loading: false,
menuItems: [] as ContextMenuItem[],
newMealdate: "",
@ -230,6 +250,12 @@ export default defineComponent({
color: undefined,
event: "download",
},
duplicate: {
title: i18n.tc("general.duplicate"),
icon: $globals.icons.duplicate,
color: undefined,
event: "duplicate",
},
mealplanner: {
title: i18n.tc("recipe.add-to-plan"),
icon: $globals.icons.calendar,
@ -330,6 +356,13 @@ export default defineComponent({
}
}
async function duplicateRecipe() {
const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName);
if (data && data.slug) {
router.push(`/recipe/${data.slug}`);
}
}
const { copyText } = useCopy();
// Note: Print is handled as an event in the parent component
@ -339,6 +372,9 @@ export default defineComponent({
},
edit: () => router.push(`/recipe/${props.slug}` + "?edit=true"),
download: handleDownloadEvent,
duplicate: () => {
state.recipeDuplicateDialog = true;
},
mealplanner: () => {
state.mealplannerDialog = true;
},
@ -376,6 +412,7 @@ export default defineComponent({
...toRefs(state),
shoppingLists,
addRecipeToList,
duplicateRecipe,
contextMenuEventHandler,
deleteRecipe,
addRecipeToPlan,

View file

@ -8,7 +8,7 @@ export const LOCALES = [
{
name: "简体中文 (Chinese simplified)",
value: "zh-CN",
progress: 57,
progress: 56,
},
{
name: "Tiếng Việt (Vietnamese)",
@ -23,7 +23,7 @@ export const LOCALES = [
{
name: "Türkçe (Turkish)",
value: "tr-TR",
progress: 32,
progress: 47,
},
{
name: "Svenska (Swedish)",
@ -38,12 +38,12 @@ export const LOCALES = [
{
name: "Slovenian",
value: "sl-SI",
progress: 95,
progress: 94,
},
{
name: "Slovak",
value: "sk-SK",
progress: 86,
progress: 85,
},
{
name: "Pусский (Russian)",
@ -53,7 +53,7 @@ export const LOCALES = [
{
name: "Română (Romanian)",
value: "ro-RO",
progress: 4,
progress: 3,
},
{
name: "Português (Portuguese)",
@ -63,27 +63,27 @@ export const LOCALES = [
{
name: "Português do Brasil (Brazilian Portuguese)",
value: "pt-BR",
progress: 39,
progress: 40,
},
{
name: "Polski (Polish)",
value: "pl-PL",
progress: 88,
progress: 89,
},
{
name: "Norsk (Norwegian)",
value: "no-NO",
progress: 85,
progress: 87,
},
{
name: "Nederlands (Dutch)",
value: "nl-NL",
progress: 91,
progress: 97,
},
{
name: "Lithuanian",
value: "lt-LT",
progress: 0,
progress: 64,
},
{
name: "한국어 (Korean)",
@ -98,12 +98,12 @@ export const LOCALES = [
{
name: "Italiano (Italian)",
value: "it-IT",
progress: 83,
progress: 82,
},
{
name: "Magyar (Hungarian)",
value: "hu-HU",
progress: 78,
progress: 77,
},
{
name: "עברית (Hebrew)",
@ -113,7 +113,7 @@ export const LOCALES = [
{
name: "Français (French)",
value: "fr-FR",
progress: 100,
progress: 99,
},
{
name: "French, Canada",
@ -123,12 +123,12 @@ export const LOCALES = [
{
name: "Suomi (Finnish)",
value: "fi-FI",
progress: 23,
progress: 22,
},
{
name: "Español (Spanish)",
value: "es-ES",
progress: 95,
progress: 94,
},
{
name: "American English",
@ -138,7 +138,7 @@ export const LOCALES = [
{
name: "British English",
value: "en-GB",
progress: 32,
progress: 31,
},
{
name: "Ελληνικά (Greek)",
@ -148,17 +148,17 @@ export const LOCALES = [
{
name: "Deutsch (German)",
value: "de-DE",
progress: 100,
progress: 99,
},
{
name: "Dansk (Danish)",
value: "da-DK",
progress: 100,
progress: 99,
},
{
name: "Čeština (Czech)",
value: "cs-CZ",
progress: 66,
progress: 89,
},
{
name: "Català (Catalan)",
@ -178,6 +178,6 @@ export const LOCALES = [
{
name: "Afrikaans (Afrikaans)",
value: "af-ZA",
progress: 0,
progress: 9,
},
]

View file

@ -75,6 +75,7 @@
"delete": "Löschen",
"disabled": "Deaktiviert",
"download": "Herunterladen",
"duplicate": "Duplizieren",
"edit": "Bearbeiten",
"enabled": "Aktiviert",
"exception": "Fehler",
@ -281,6 +282,8 @@
"description": "Beschreibung",
"disable-amount": "Zutatenmenge deaktivieren",
"disable-comments": "Kommentare deaktivieren",
"duplicate": "Rezept duplizieren",
"duplicate-name": "Name of the new recipe",
"edit-scale": "Maßstab ändern",
"fat-content": "Fett",
"fiber-content": "Ballaststoffe",

View file

@ -75,6 +75,7 @@
"delete": "Delete",
"disabled": "Disabled",
"download": "Download",
"duplicate": "Duplicate",
"edit": "Edit",
"enabled": "Enabled",
"exception": "Exception",
@ -282,6 +283,8 @@
"description": "Description",
"disable-amount": "Disable Ingredient Amounts",
"disable-comments": "Disable Comments",
"duplicate": "Duplicate recipe",
"duplicate-name": "Name of the new recipe",
"edit-scale": "Edit Scale",
"fat-content": "Fat",
"fiber-content": "Fiber",

View file

@ -1,3 +1,4 @@
import { Recipe } from "../types/recipe";
import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generated";
export interface CrudAPIInterface {
@ -20,8 +21,7 @@ export abstract class BaseAPI {
export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType = CreateType>
extends BaseAPI
implements CrudAPIInterface
{
implements CrudAPIInterface {
abstract baseRoute: string;
abstract itemRoute(itemId: string | number): string;
@ -50,4 +50,10 @@ export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType = CreateType>
async deleteOne(itemId: string | number) {
return await this.requests.delete<ReadType>(this.itemRoute(itemId));
}
async duplicateOne(itemId: string | number, newName: string | undefined) {
return await this.requests.post<Recipe>(`${this.itemRoute(itemId)}/duplicate`, {
name: newName,
});
}
}

View file

@ -302,6 +302,9 @@ export interface RecipeCommentUpdate {
id: string;
text: string;
}
export interface RecipeDuplicate {
name?: string;
}
export interface RecipePaginationQuery {
page?: number;
perPage?: number;

View file

@ -123,6 +123,7 @@ import {
mdiText,
mdiTextBoxOutline,
mdiChefHat,
mdiContentDuplicate,
} from "@mdi/js";
export const icons = {
@ -173,6 +174,7 @@ export const icons = {
dotsHorizontal: mdiDotsHorizontal,
dotsVertical: mdiDotsVertical,
download: mdiDownload,
duplicate: mdiContentDuplicate,
email: mdiEmail,
externalLink: mdiLinkVariant,
eye: mdiEye,

View file

@ -243,6 +243,7 @@
delete: false,
edit: false,
download: true,
duplicate: false,
mealplanner: false,
print: true,
share: false,