mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-25 08:09:41 +02:00
feat: merge food into another (#1143)
* setup food repository * add merge route and payloads * remove type checking * generate types * implement merge dialog * food repo tests * split install from workflow * bum dependencies * revert changes * update copy * refactor URLs to avoid incorrect template being used * stick advanced items under developer mode * use utility component for advanced feature
This commit is contained in:
parent
10784b6e24
commit
b93dae109e
21 changed files with 319 additions and 175 deletions
|
@ -6,9 +6,15 @@ const prefix = "/api";
|
|||
const routes = {
|
||||
food: `${prefix}/foods`,
|
||||
foodsFood: (tag: string) => `${prefix}/foods/${tag}`,
|
||||
merge: `${prefix}/foods/merge`,
|
||||
};
|
||||
|
||||
export class FoodAPI extends BaseCRUDAPI<IngredientFood, CreateIngredientFood> {
|
||||
baseRoute: string = routes.food;
|
||||
itemRoute = routes.foodsFood;
|
||||
|
||||
merge(fromId: string, toId: string) {
|
||||
// @ts-ignore TODO: fix this
|
||||
return this.requests.put<IngredientFood>(routes.merge, { fromFood: fromId, toFood: toId });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export default defineComponent({
|
|||
];
|
||||
|
||||
function handleRowClick(item: ReportSummary) {
|
||||
router.push("/group/data/reports/" + item.id);
|
||||
router.push("/group/reports/" + item.id);
|
||||
}
|
||||
|
||||
function capitalize(str: string) {
|
||||
|
@ -69,5 +69,4 @@ export default defineComponent({
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
:top-link="topLinks"
|
||||
:bottom-links="bottomLinks"
|
||||
:user="{ data: true }"
|
||||
:secondary-header="$t('user.admin')"
|
||||
secondary-header="Developer"
|
||||
:secondary-links="developerLinks"
|
||||
/>
|
||||
|
||||
<TheSnackbar />
|
||||
|
@ -49,11 +50,7 @@ export default defineComponent({
|
|||
to: "/admin/site-settings",
|
||||
title: i18n.t("sidebar.site-settings"),
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.wrench,
|
||||
to: "/admin/maintenance",
|
||||
title: "Maintenance",
|
||||
},
|
||||
|
||||
// {
|
||||
// icon: $globals.icons.chart,
|
||||
// to: "/admin/analytics",
|
||||
|
@ -74,6 +71,14 @@ export default defineComponent({
|
|||
to: "/admin/backups",
|
||||
title: i18n.t("sidebar.backups"),
|
||||
},
|
||||
];
|
||||
|
||||
const developerLinks: SidebarLinks = [
|
||||
{
|
||||
icon: $globals.icons.wrench,
|
||||
to: "/admin/maintenance",
|
||||
title: "Maintenance",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.check,
|
||||
to: "/admin/background-tasks",
|
||||
|
@ -98,6 +103,7 @@ export default defineComponent({
|
|||
sidebar,
|
||||
topLinks,
|
||||
bottomLinks,
|
||||
developerLinks,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
</section>
|
||||
</section>
|
||||
<v-container class="mt-4 d-flex justify-end">
|
||||
<v-btn outlined rounded to="/group/data/migrations"> Looking For Migrations? </v-btn>
|
||||
<v-btn outlined rounded to="/group/migrations"> Looking For Migrations? </v-btn>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,20 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Merge Dialog -->
|
||||
<BaseDialog v-model="mergeDialog" :icon="$globals.icons.foods" title="Combine Food" @confirm="mergeFoods">
|
||||
<v-card-text>
|
||||
Combining the selected foods will merge the Source Food and Target Food into a single food. The
|
||||
<strong> Source Food will be deleted </strong> and all of the references to the Source Food will be updated to
|
||||
point to the Target Food.
|
||||
<v-autocomplete v-model="fromFood" return-object :items="foods" item-text="name" label="Source Food" />
|
||||
<v-autocomplete v-model="toFood" return-object :items="foods" item-text="name" label="Target Food" />
|
||||
|
||||
<template v-if="canMerge && fromFood && toFood">
|
||||
<div class="text-center">Merging {{ fromFood.name }} into {{ toFood.name }}</div>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<BaseDialog
|
||||
v-model="editDialog"
|
||||
|
@ -48,7 +63,7 @@
|
|||
@edit-one="editEventHandler"
|
||||
>
|
||||
<template #button-row>
|
||||
<BaseButton :disabled="true">
|
||||
<BaseButton @click="mergeDialog = true">
|
||||
<template #icon> {{ $globals.icons.foods }} </template>
|
||||
Combine
|
||||
</BaseButton>
|
||||
|
@ -64,6 +79,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
|
||||
import { computed } from "vue-demi";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { IngredientFood } from "~/types/api-types/recipe";
|
||||
|
@ -144,6 +160,29 @@ export default defineComponent({
|
|||
deleteDialog.value = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Merge Foods
|
||||
|
||||
const mergeDialog = ref(false);
|
||||
const fromFood = ref<IngredientFood | null>(null);
|
||||
const toFood = ref<IngredientFood | null>(null);
|
||||
|
||||
const canMerge = computed(() => {
|
||||
return fromFood.value && toFood.value && fromFood.value.id !== toFood.value.id;
|
||||
});
|
||||
|
||||
async function mergeFoods() {
|
||||
if (!canMerge.value || !fromFood.value || !toFood.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await userApi.foods.merge(fromFood.value.id, toFood.value.id);
|
||||
|
||||
if (data) {
|
||||
refreshFoods();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Labels
|
||||
|
||||
|
@ -170,6 +209,12 @@ export default defineComponent({
|
|||
deleteEventHandler,
|
||||
deleteDialog,
|
||||
deleteFood,
|
||||
// Merge
|
||||
canMerge,
|
||||
mergeFoods,
|
||||
mergeDialog,
|
||||
fromFood,
|
||||
toFood,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -312,7 +312,7 @@
|
|||
|
||||
<AdvancedOnly>
|
||||
<v-container class="narrow-container d-flex justify-end">
|
||||
<v-btn outlined rounded to="/group/data/migrations"> Looking For Migrations? </v-btn>
|
||||
<v-btn outlined rounded to="/group/migrations"> Looking For Migrations? </v-btn>
|
||||
</v-container>
|
||||
</AdvancedOnly>
|
||||
</div>
|
||||
|
|
|
@ -98,15 +98,17 @@
|
|||
Manage your preferences, change your password, and update your email
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Your API Tokens', to: '/user/profile/api-tokens' }"
|
||||
:image="require('~/static/svgs/manage-api-tokens.svg')"
|
||||
>
|
||||
<template #title> API Tokens </template>
|
||||
Manage your API Tokens for access from external applications
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
<AdvancedOnly>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Your API Tokens', to: '/user/profile/api-tokens' }"
|
||||
:image="require('~/static/svgs/manage-api-tokens.svg')"
|
||||
>
|
||||
<template #title> API Tokens </template>
|
||||
Manage your API Tokens for access from external applications
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
</AdvancedOnly>
|
||||
</v-row>
|
||||
</section>
|
||||
<v-divider class="my-7"></v-divider>
|
||||
|
@ -134,24 +136,6 @@
|
|||
Manage a collection of recipe categories and generate pages for them.
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Webhooks', to: '/group/webhooks' }"
|
||||
:image="require('~/static/svgs/manage-webhooks.svg')"
|
||||
>
|
||||
<template #title> Webhooks </template>
|
||||
Setup webhooks that trigger on days that you have have mealplan scheduled.
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Notifiers', to: '/group/notifiers' }"
|
||||
:image="require('~/static/svgs/manage-notifiers.svg')"
|
||||
>
|
||||
<template #title> Notifiers </template>
|
||||
Setup email and push notifications that trigger on specific events.
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
<v-col v-if="user.canManage" cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Members', to: '/group/members' }"
|
||||
|
@ -161,33 +145,50 @@
|
|||
See who's in your group and manage their permissions.
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Recipe Data', to: '/group/data/recipes' }"
|
||||
:image="require('~/static/svgs/manage-recipes.svg')"
|
||||
>
|
||||
<template #title> Recipe Data </template>
|
||||
Manage your recipe data and make bulk changes
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Data', to: '/group/data/foods' }"
|
||||
:image="require('~/static/svgs/manage-recipes.svg')"
|
||||
>
|
||||
<template #title> Manage Data </template>
|
||||
Manage your Food and Units (more options coming soon)
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Data Migrations', to: '/group/data/migrations' }"
|
||||
:image="require('~/static/svgs/manage-data-migrations.svg')"
|
||||
>
|
||||
<template #title> Data Migrations </template>
|
||||
Migrate your existing data from other applications like Nextcloud Recipes and Chowdown
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
<AdvancedOnly>
|
||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Webhooks', to: '/group/webhooks' }"
|
||||
:image="require('~/static/svgs/manage-webhooks.svg')"
|
||||
>
|
||||
<template #title> Webhooks </template>
|
||||
Setup webhooks that trigger on days that you have have mealplan scheduled.
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
</AdvancedOnly>
|
||||
<AdvancedOnly>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Notifiers', to: '/group/notifiers' }"
|
||||
:image="require('~/static/svgs/manage-notifiers.svg')"
|
||||
>
|
||||
<template #title> Notifiers </template>
|
||||
Setup email and push notifications that trigger on specific events.
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
</AdvancedOnly>
|
||||
<AdvancedOnly>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Data', to: '/group/data/foods' }"
|
||||
:image="require('~/static/svgs/manage-recipes.svg')"
|
||||
>
|
||||
<template #title> Manage Data </template>
|
||||
Manage your Food and Units (more options coming soon)
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
</AdvancedOnly>
|
||||
<AdvancedOnly>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<UserProfileLinkCard
|
||||
:link="{ text: 'Manage Data Migrations', to: '/group/migrations' }"
|
||||
:image="require('~/static/svgs/manage-data-migrations.svg')"
|
||||
>
|
||||
<template #title> Data Migrations </template>
|
||||
Migrate your existing data from other applications like Nextcloud Recipes and Chowdown
|
||||
</UserProfileLinkCard>
|
||||
</v-col>
|
||||
</AdvancedOnly>
|
||||
</v-row>
|
||||
</section>
|
||||
</v-container>
|
||||
|
|
|
@ -113,6 +113,10 @@ export interface MultiPurposeLabelSummary {
|
|||
groupId: string;
|
||||
id: string;
|
||||
}
|
||||
export interface IngredientMerge {
|
||||
fromFood: string;
|
||||
toFood: string;
|
||||
}
|
||||
/**
|
||||
* A list of ingredient references.
|
||||
*/
|
||||
|
|
132
frontend/types/components.d.ts
vendored
132
frontend/types/components.d.ts
vendored
|
@ -1,74 +1,76 @@
|
|||
// This Code is auto generated by gen_global_components.py
|
||||
import BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue";
|
||||
import MarkdownEditor from "@/components/global/MarkdownEditor.vue";
|
||||
import AppLoader from "@/components/global/AppLoader.vue";
|
||||
import BaseOverflowButton from "@/components/global/BaseOverflowButton.vue";
|
||||
import ReportTable from "@/components/global/ReportTable.vue";
|
||||
import AppToolbar from "@/components/global/AppToolbar.vue";
|
||||
import BaseButtonGroup from "@/components/global/BaseButtonGroup.vue";
|
||||
import BaseButton from "@/components/global/BaseButton.vue";
|
||||
import BannerExperimental from "@/components/global/BannerExperimental.vue";
|
||||
import BaseDialog from "@/components/global/BaseDialog.vue";
|
||||
import RecipeJsonEditor from "@/components/global/RecipeJsonEditor.vue";
|
||||
import StatsCards from "@/components/global/StatsCards.vue";
|
||||
import HelpIcon from "@/components/global/HelpIcon.vue";
|
||||
import InputLabelType from "@/components/global/InputLabelType.vue";
|
||||
import BaseStatCard from "@/components/global/BaseStatCard.vue";
|
||||
import DevDumpJson from "@/components/global/DevDumpJson.vue";
|
||||
import LanguageDialog from "@/components/global/LanguageDialog.vue";
|
||||
import InputQuantity from "@/components/global/InputQuantity.vue";
|
||||
import ToggleState from "@/components/global/ToggleState.vue";
|
||||
import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
||||
import CrudTable from "@/components/global/CrudTable.vue";
|
||||
import InputColor from "@/components/global/InputColor.vue";
|
||||
import BaseDivider from "@/components/global/BaseDivider.vue";
|
||||
import AutoForm from "@/components/global/AutoForm.vue";
|
||||
import AppButtonUpload from "@/components/global/AppButtonUpload.vue";
|
||||
import AdvancedOnly from "@/components/global/AdvancedOnly.vue";
|
||||
import BasePageTitle from "@/components/global/BasePageTitle.vue";
|
||||
import ButtonLink from "@/components/global/ButtonLink.vue";
|
||||
import BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue";
|
||||
import MarkdownEditor from "@/components/global/MarkdownEditor.vue";
|
||||
import AppLoader from "@/components/global/AppLoader.vue";
|
||||
import BaseOverflowButton from "@/components/global/BaseOverflowButton.vue";
|
||||
import ReportTable from "@/components/global/ReportTable.vue";
|
||||
import AppToolbar from "@/components/global/AppToolbar.vue";
|
||||
import BaseButtonGroup from "@/components/global/BaseButtonGroup.vue";
|
||||
import BaseButton from "@/components/global/BaseButton.vue";
|
||||
import BannerExperimental from "@/components/global/BannerExperimental.vue";
|
||||
import BaseDialog from "@/components/global/BaseDialog.vue";
|
||||
import RecipeJsonEditor from "@/components/global/RecipeJsonEditor.vue";
|
||||
import StatsCards from "@/components/global/StatsCards.vue";
|
||||
import HelpIcon from "@/components/global/HelpIcon.vue";
|
||||
import InputLabelType from "@/components/global/InputLabelType.vue";
|
||||
import BaseStatCard from "@/components/global/BaseStatCard.vue";
|
||||
import DevDumpJson from "@/components/global/DevDumpJson.vue";
|
||||
import LanguageDialog from "@/components/global/LanguageDialog.vue";
|
||||
import InputQuantity from "@/components/global/InputQuantity.vue";
|
||||
import ToggleState from "@/components/global/ToggleState.vue";
|
||||
import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
||||
import CrudTable from "@/components/global/CrudTable.vue";
|
||||
import InputColor from "@/components/global/InputColor.vue";
|
||||
import BaseDivider from "@/components/global/BaseDivider.vue";
|
||||
import AutoForm from "@/components/global/AutoForm.vue";
|
||||
import AppButtonUpload from "@/components/global/AppButtonUpload.vue";
|
||||
import AdvancedOnly from "@/components/global/AdvancedOnly.vue";
|
||||
import BasePageTitle from "@/components/global/BasePageTitle.vue";
|
||||
import ButtonLink from "@/components/global/ButtonLink.vue";
|
||||
|
||||
import TheSnackbar from "@/components/layout/TheSnackbar.vue";
|
||||
import AppHeader from "@/components/layout/AppHeader.vue";
|
||||
import AppSidebar from "@/components/layout/AppSidebar.vue";
|
||||
import AppFooter from "@/components/layout/AppFooter.vue";
|
||||
|
||||
import TheSnackbar from "@/components/layout/TheSnackbar.vue";
|
||||
import AppHeader from "@/components/layout/AppHeader.vue";
|
||||
import AppSidebar from "@/components/layout/AppSidebar.vue";
|
||||
import AppFooter from "@/components/layout/AppFooter.vue";
|
||||
|
||||
declare module "vue" {
|
||||
export interface GlobalComponents {
|
||||
// Global Components
|
||||
BaseCardSectionTitle: typeof BaseCardSectionTitle;
|
||||
MarkdownEditor: typeof MarkdownEditor;
|
||||
AppLoader: typeof AppLoader;
|
||||
BaseOverflowButton: typeof BaseOverflowButton;
|
||||
ReportTable: typeof ReportTable;
|
||||
AppToolbar: typeof AppToolbar;
|
||||
BaseButtonGroup: typeof BaseButtonGroup;
|
||||
BaseButton: typeof BaseButton;
|
||||
BannerExperimental: typeof BannerExperimental;
|
||||
BaseDialog: typeof BaseDialog;
|
||||
RecipeJsonEditor: typeof RecipeJsonEditor;
|
||||
StatsCards: typeof StatsCards;
|
||||
HelpIcon: typeof HelpIcon;
|
||||
InputLabelType: typeof InputLabelType;
|
||||
BaseStatCard: typeof BaseStatCard;
|
||||
DevDumpJson: typeof DevDumpJson;
|
||||
LanguageDialog: typeof LanguageDialog;
|
||||
InputQuantity: typeof InputQuantity;
|
||||
ToggleState: typeof ToggleState;
|
||||
AppButtonCopy: typeof AppButtonCopy;
|
||||
CrudTable: typeof CrudTable;
|
||||
InputColor: typeof InputColor;
|
||||
BaseDivider: typeof BaseDivider;
|
||||
AutoForm: typeof AutoForm;
|
||||
AppButtonUpload: typeof AppButtonUpload;
|
||||
AdvancedOnly: typeof AdvancedOnly;
|
||||
BasePageTitle: typeof BasePageTitle;
|
||||
ButtonLink: typeof ButtonLink;
|
||||
// Layout Components
|
||||
TheSnackbar: typeof TheSnackbar;
|
||||
AppHeader: typeof AppHeader;
|
||||
AppSidebar: typeof AppSidebar;
|
||||
AppFooter: typeof AppFooter;
|
||||
BaseCardSectionTitle: typeof BaseCardSectionTitle;
|
||||
MarkdownEditor: typeof MarkdownEditor;
|
||||
AppLoader: typeof AppLoader;
|
||||
BaseOverflowButton: typeof BaseOverflowButton;
|
||||
ReportTable: typeof ReportTable;
|
||||
AppToolbar: typeof AppToolbar;
|
||||
BaseButtonGroup: typeof BaseButtonGroup;
|
||||
BaseButton: typeof BaseButton;
|
||||
BannerExperimental: typeof BannerExperimental;
|
||||
BaseDialog: typeof BaseDialog;
|
||||
RecipeJsonEditor: typeof RecipeJsonEditor;
|
||||
StatsCards: typeof StatsCards;
|
||||
HelpIcon: typeof HelpIcon;
|
||||
InputLabelType: typeof InputLabelType;
|
||||
BaseStatCard: typeof BaseStatCard;
|
||||
DevDumpJson: typeof DevDumpJson;
|
||||
LanguageDialog: typeof LanguageDialog;
|
||||
InputQuantity: typeof InputQuantity;
|
||||
ToggleState: typeof ToggleState;
|
||||
AppButtonCopy: typeof AppButtonCopy;
|
||||
CrudTable: typeof CrudTable;
|
||||
InputColor: typeof InputColor;
|
||||
BaseDivider: typeof BaseDivider;
|
||||
AutoForm: typeof AutoForm;
|
||||
AppButtonUpload: typeof AppButtonUpload;
|
||||
AdvancedOnly: typeof AdvancedOnly;
|
||||
BasePageTitle: typeof BasePageTitle;
|
||||
ButtonLink: typeof ButtonLink;
|
||||
// Layout Components
|
||||
TheSnackbar: typeof TheSnackbar;
|
||||
AppHeader: typeof AppHeader;
|
||||
AppSidebar: typeof AppSidebar;
|
||||
AppFooter: typeof AppFooter;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue