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

Feature/user seedable foods (#1176)

* remove odd ingredients

* UI Elements for food

* update translated percentage

* spek -> speck

* generate types

* seeder api endpoints + tests

* implement foods seeder UI

* localize some food strings
This commit is contained in:
Hayden 2022-05-01 12:45:50 -08:00 committed by GitHub
parent 67178f9b74
commit d6e2b4ab85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 478 additions and 172 deletions

View file

@ -0,0 +1,26 @@
import { BaseAPI } from "../_base";
import { SuccessResponse } from "~/types/api-types/response";
import { SeederConfig } from "~/types/api-types/group";
const prefix = "/api";
const routes = {
base: `${prefix}/groups/seeders`,
foods: `${prefix}/groups/seeders/foods`,
units: `${prefix}/groups/seeders/units`,
labels: `${prefix}/groups/seeders/labels`,
};
export class GroupDataSeederApi extends BaseAPI {
foods(payload: SeederConfig) {
return this.requests.post<SuccessResponse>(routes.foods, payload);
}
units(payload: SeederConfig) {
return this.requests.post<SuccessResponse>(routes.units, payload);
}
labels(payload: SeederConfig) {
return this.requests.post<SuccessResponse>(routes.labels, payload);
}
}

View file

@ -23,6 +23,7 @@ import { ShoppingApi } from "./class-interfaces/group-shopping-lists";
import { MultiPurposeLabelsApi } from "./class-interfaces/group-multiple-purpose-labels";
import { GroupEventNotifierApi } from "./class-interfaces/group-event-notifier";
import { MealPlanRulesApi } from "./class-interfaces/group-mealplan-rules";
import { GroupDataSeederApi } from "./class-interfaces/group-seeder";
import { ApiRequestInstance } from "~/types/api";
class Api {
@ -50,6 +51,7 @@ class Api {
public multiPurposeLabels: MultiPurposeLabelsApi;
public groupEventNotifier: GroupEventNotifierApi;
public upload: UploadFile;
public seeders: GroupDataSeederApi;
constructor(requests: ApiRequestInstance) {
// Recipes
@ -75,6 +77,7 @@ class Api {
this.groupReports = new GroupReportsApi(requests);
this.shopping = new ShoppingApi(requests);
this.multiPurposeLabels = new MultiPurposeLabelsApi(requests);
this.seeders = new GroupDataSeederApi(requests);
// Admin
this.backups = new BackupAPI(requests);

View file

@ -9,7 +9,7 @@
:fullscreen="$vuetify.breakpoint.xsOnly"
>
<v-card height="100%">
<v-app-bar dark :color="color" class="mt-n1">
<v-app-bar dark dense :color="color" class="">
<v-icon large left>
{{ icon }}
</v-icon>

View file

@ -6,7 +6,6 @@
v-model="locale"
:items="locales"
item-text="name"
menu-props="auto"
class="my-3"
hide-details
outlined

View file

@ -3,12 +3,12 @@ export const LOCALES = [
{
name: "繁體中文 (Chinese traditional)",
value: "zh-TW",
progress: 100,
progress: 90,
},
{
name: "简体中文 (Chinese simplified)",
value: "zh-CN",
progress: 100,
progress: 74,
},
{
name: "Tiếng Việt (Vietnamese)",
@ -18,17 +18,17 @@ export const LOCALES = [
{
name: "Українська (Ukrainian)",
value: "uk-UA",
progress: 100,
progress: 99,
},
{
name: "Türkçe (Turkish)",
value: "tr-TR",
progress: 7,
progress: 5,
},
{
name: "Svenska (Swedish)",
value: "sv-SE",
progress: 100,
progress: 92,
},
{
name: "српски (Serbian)",
@ -38,12 +38,12 @@ export const LOCALES = [
{
name: "Slovak",
value: "sk-SK",
progress: 100,
progress: 74,
},
{
name: "Pусский (Russian)",
value: "ru-RU",
progress: 100,
progress: 74,
},
{
name: "Română (Romanian)",
@ -53,12 +53,12 @@ export const LOCALES = [
{
name: "Português (Portugese)",
value: "pt-PT",
progress: 15,
progress: 11,
},
{
name: "Português do Brasil (Brazilian Portuguese)",
value: "pt-BR",
progress: 64,
progress: 47,
},
{
name: "Polski (Polish)",
@ -68,12 +68,12 @@ export const LOCALES = [
{
name: "Norsk (Norwegian)",
value: "no-NO",
progress: 100,
progress: 74,
},
{
name: "Nederlands (Dutch)",
value: "nl-NL",
progress: 100,
progress: 98,
},
{
name: "한국어 (Korean)",
@ -88,7 +88,7 @@ export const LOCALES = [
{
name: "Italiano (Italian)",
value: "it-IT",
progress: 99,
progress: 96,
},
{
name: "Magyar (Hungarian)",
@ -103,12 +103,12 @@ export const LOCALES = [
{
name: "Français (French)",
value: "fr-FR",
progress: 100,
progress: 99,
},
{
name: "French, Canada",
value: "fr-CA",
progress: 100,
progress: 88,
},
{
name: "Suomi (Finnish)",
@ -118,7 +118,7 @@ export const LOCALES = [
{
name: "Español (Spanish)",
value: "es-ES",
progress: 100,
progress: 74,
},
{
name: "American English",
@ -128,22 +128,22 @@ export const LOCALES = [
{
name: "British English",
value: "en-GB",
progress: 100,
progress: 74,
},
{
name: "Ελληνικά (Greek)",
value: "el-GR",
progress: 100,
progress: 86,
},
{
name: "Deutsch (German)",
value: "de-DE",
progress: 100,
progress: 99,
},
{
name: "Dansk (Danish)",
value: "da-DK",
progress: 100,
progress: 83,
},
{
name: "Čeština (Czech)",
@ -153,7 +153,7 @@ export const LOCALES = [
{
name: "Català (Catalan)",
value: "ca-ES",
progress: 100,
progress: 74,
},
{
name: "العربية (Arabic)",
@ -165,4 +165,4 @@ export const LOCALES = [
value: "af-ZA",
progress: 0,
},
];
]

View file

@ -130,7 +130,9 @@
"url": "URL",
"view": "View",
"wednesday": "Wednesday",
"yes": "Yes"
"yes": "Yes",
"foods": "Foods",
"units": "Units"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Are you sure you want to delete <b>{groupName}<b/>?",
@ -502,5 +504,14 @@
"select-description": "Choose the language for the Mealie UI. The setting only applies to you, not other users.",
"how-to-contribute-description": "Is something not translated yet, mistranslated, or your language missing from the list? {read-the-docs-link} on how to contribute!",
"read-the-docs": "Read the docs"
},
"data-pages": {
"seed-data": "Seed Data",
"foods": {
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
"merge-food-example": "Merging {food1} into {food2}",
"seed-dialog-text": "Seed the database with foods based on your local language. This will create 200+ common foods that can be used to organize your database. Foods are translated via a community effort.",
"seed-dialog-warning": "You have already have some items in your database. This action will not reconcile duplicates, you will have to manage them manually."
}
}
}

View file

@ -3,18 +3,57 @@
<!-- 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.
<div>
{{ $t("data-pages.foods.merge-dialog-text") }}
</div>
<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>
<div class="text-center">
{{ $t("data-pages.foods.merge-food-example", { food1: fromFood.name, food2: toFood.name }) }}
</div>
</template>
</v-card-text>
</BaseDialog>
<!-- Seed Dialog-->
<BaseDialog
v-model="seedDialog"
:icon="$globals.icons.foods"
:title="$tc('data-pages.seed-data')"
@confirm="seedDatabase"
>
<v-card-text>
<div class="pb-2">
{{ $t("data-pages.foods.seed-dialog-text") }}
</div>
<v-autocomplete
v-model="locale"
:items="locales"
item-text="name"
label="Select Language"
class="my-3"
hide-details
outlined
offset
>
<template #item="{ item }">
<v-list-item-content>
<v-list-item-title v-text="item.name"></v-list-item-title>
<v-list-item-subtitle>
{{ item.progress }}% {{ $tc("language-dialog.translated") }}
</v-list-item-subtitle>
</v-list-item-content>
</template>
</v-autocomplete>
<v-alert v-if="foods.length > 0" type="error" class="mb-0 text-body-2">
{{ $t("data-pages.foods.seed-dialog-warning") }}
</v-alert>
</v-card-text>
</BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="editDialog"
@ -73,6 +112,12 @@
{{ item.label.name }}
</MultiPurposeLabel>
</template>
<template #button-bottom>
<BaseButton @click="seedDialog = true">
<template #icon> {{ $globals.icons.database }} </template>
Seed
</BaseButton>
</template>
</CrudTable>
</div>
</template>
@ -80,11 +125,13 @@
<script lang="ts">
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
import { computed } from "vue-demi";
import type { LocaleObject } from "@nuxtjs/i18n";
import { validators } from "~/composables/use-validators";
import { useUserApi } from "~/composables/api";
import { IngredientFood } from "~/types/api-types/recipe";
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
import { MultiPurposeLabelSummary } from "~/types/api-types/labels";
import { useLocales } from "~/composables/use-locales";
export default defineComponent({
components: { MultiPurposeLabel },
@ -193,6 +240,30 @@ export default defineComponent({
allLabels.value = data ?? [];
}
// ============================================================
// Seed
const seedDialog = ref(false);
const locale = ref("");
const { locales: LOCALES, locale: currentLocale, i18n } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;
});
const locales = LOCALES.filter((locale) =>
(i18n.locales as LocaleObject[]).map((i18nLocale) => i18nLocale.code).includes(locale.value)
);
async function seedDatabase() {
const { data } = await userApi.seeders.foods({ locale: locale.value });
if (data) {
refreshFoods();
}
}
refreshLabels();
return {
tableConfig,
@ -215,6 +286,11 @@ export default defineComponent({
mergeDialog,
fromFood,
toFood,
// Seed Data
locale,
locales,
seedDialog,
seedDatabase,
};
},
});

View file

@ -103,13 +103,11 @@ export interface RecipeSummary {
}
export interface RecipeCategory {
id?: string;
groupId: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: string;
groupId: string;
name: string;
slug: string;
}

View file

@ -89,13 +89,11 @@ export interface RecipeSummary {
}
export interface RecipeCategory {
id?: string;
groupId: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: string;
groupId: string;
name: string;
slug: string;
}

View file

@ -258,13 +258,11 @@ export interface RecipeSummary {
}
export interface RecipeCategory {
id?: string;
groupId: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: string;
groupId: string;
name: string;
slug: string;
}
@ -307,6 +305,9 @@ export interface SaveWebhook {
time?: string;
groupId: string;
}
export interface SeederConfig {
locale: string;
}
export interface SetPermissions {
userId: string;
canManage?: boolean;

View file

@ -119,13 +119,11 @@ export interface RecipeSummary {
}
export interface RecipeCategory {
id?: string;
groupId: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: string;
groupId: string;
name: string;
slug: string;
}

View file

@ -42,6 +42,7 @@ export interface CategoryOut {
name: string;
id: string;
slug: string;
groupId: string;
}
export interface CategorySave {
name: string;
@ -68,19 +69,14 @@ export interface CreateRecipeBulk {
}
export interface RecipeCategory {
id?: string;
groupId: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: string;
groupId: string;
name: string;
slug: string;
}
export interface CreateRecipeByUrl {
url: string;
}
export interface CreateRecipeByUrlBulk {
imports: CreateRecipeBulk[];
}
@ -115,10 +111,6 @@ export interface MultiPurposeLabelSummary {
groupId: string;
id: string;
}
export interface IngredientMerge {
fromFood: string;
toFood: string;
}
/**
* A list of ingredient references.
*/
@ -140,6 +132,14 @@ export interface IngredientsRequest {
parser?: RegisteredParser & string;
ingredients: string[];
}
export interface MergeFood {
fromFood: string;
toFood: string;
}
export interface MergeUnit {
fromUnit: string;
toUnit: string;
}
export interface Nutrition {
calories?: string;
fatContent?: string;
@ -348,6 +348,13 @@ export interface SaveIngredientUnit {
abbreviation?: string;
groupId: string;
}
export interface ScrapeRecipe {
url: string;
includeTags?: boolean;
}
export interface ScrapeRecipeTest {
url: string;
}
export interface SlugResponse {}
export interface TagIn {
name: string;

View file

@ -132,13 +132,11 @@ export interface RecipeSummary {
}
export interface RecipeCategory {
id?: string;
groupId: string;
name: string;
slug: string;
}
export interface RecipeTag {
id?: string;
groupId: string;
name: string;
slug: string;
}

View file

@ -1,76 +1,74 @@
// 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 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 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";
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;
}
}