Merge remote-tracking branch 'upstream/dev' into localization

This commit is contained in:
Florian Dupret 2021-04-28 10:32:33 +02:00
commit d104b4e6d5
69 changed files with 1464 additions and 290 deletions

40
.gitignore vendored
View file

@ -10,7 +10,6 @@ mealie/temp/api.html
.temp/ .temp/
.secret .secret
dev/data/backups/* dev/data/backups/*
dev/data/debug/* dev/data/debug/*
dev/data/img/* dev/data/img/*
@ -157,39 +156,6 @@ mealie/data/debug/last_recipe.json
*.sqlite *.sqlite
dev/data/db/test.db dev/data/db/test.db
scratch.py scratch.py
frontend/dist/favicon.ico dev/data/backups/dev_sample_data*.zip
frontend/dist/index.html dev/data/backups/dev_sample_data*.zip
frontend/dist/css/app.29fe0155.css !dev/data/backups/test*.zip
frontend/dist/css/chunk-vendors.db944396.css
frontend/dist/fonts/materialdesignicons-webfont.7a44ea19.woff2
frontend/dist/fonts/materialdesignicons-webfont.64d4cf64.eot
frontend/dist/fonts/materialdesignicons-webfont.147e3378.woff
frontend/dist/fonts/materialdesignicons-webfont.174c02fc.ttf
frontend/dist/fonts/roboto-latin-100.5cb7edfc.woff
frontend/dist/fonts/roboto-latin-100.7370c367.woff2
frontend/dist/fonts/roboto-latin-100italic.f8b1df51.woff2
frontend/dist/fonts/roboto-latin-100italic.f9e8e590.woff
frontend/dist/fonts/roboto-latin-300.b00849e0.woff
frontend/dist/fonts/roboto-latin-300.ef7c6637.woff2
frontend/dist/fonts/roboto-latin-300italic.4df32891.woff
frontend/dist/fonts/roboto-latin-300italic.14286f3b.woff2
frontend/dist/fonts/roboto-latin-400.60fa3c06.woff
frontend/dist/fonts/roboto-latin-400.479970ff.woff2
frontend/dist/fonts/roboto-latin-400italic.51521a2a.woff2
frontend/dist/fonts/roboto-latin-400italic.fe65b833.woff
frontend/dist/fonts/roboto-latin-500.020c97dc.woff2
frontend/dist/fonts/roboto-latin-500.87284894.woff
frontend/dist/fonts/roboto-latin-500italic.288ad9c6.woff
frontend/dist/fonts/roboto-latin-500italic.db4a2a23.woff2
frontend/dist/fonts/roboto-latin-700.2735a3a6.woff2
frontend/dist/fonts/roboto-latin-700.adcde98f.woff
frontend/dist/fonts/roboto-latin-700italic.81f57861.woff
frontend/dist/fonts/roboto-latin-700italic.da0e7178.woff2
frontend/dist/fonts/roboto-latin-900.9b3766ef.woff2
frontend/dist/fonts/roboto-latin-900.bb1e4dc6.woff
frontend/dist/fonts/roboto-latin-900italic.28f91510.woff
frontend/dist/fonts/roboto-latin-900italic.ebf6d164.woff2
frontend/dist/js/app.36f2760c.js
frontend/dist/js/app.36f2760c.js.map
frontend/dist/js/chunk-vendors.c93761e4.js
frontend/dist/js/chunk-vendors.c93761e4.js.map

View file

@ -21,6 +21,11 @@
- 'Dinner today' shows a warning when no meal is planned yet - 'Dinner today' shows a warning when no meal is planned yet
### General ### General
- New Toolbox Page!
- Bulk assign categories and tags by keyword search
- Title case all Categories or Tags with 1 click
- Create/Rename/Delete Operations for Tags/Categories
- Remove Unused Categories or Tags with 1 click
- More localization - More localization
- Start date for Week is now selectable - Start date for Week is now selectable
- Languages are now managed through Crowdin - Languages are now managed through Crowdin

View file

@ -46,6 +46,7 @@ export default {
this.darkModeAddEventListener(); this.darkModeAddEventListener();
this.$store.dispatch("requestAppInfo"); this.$store.dispatch("requestAppInfo");
this.$store.dispatch("requestCustomPages"); this.$store.dispatch("requestCustomPages");
this.$store.dispatch("requestSiteSettings");
}, },
methods: { methods: {

View file

@ -24,6 +24,16 @@ const apiReq = {
}); });
return response; return response;
}, },
patch: async function(url, data) {
let response = await axios.patch(url, data).catch(function(error) {
if (error.response) {
processResponse(error.response);
return response;
} else return;
});
processResponse(response);
return response;
},
get: async function(url, data) { get: async function(url, data) {
let response = await axios.get(url, data).catch(function(error) { let response = await axios.get(url, data).catch(function(error) {

View file

@ -6,8 +6,10 @@ const prefix = baseURL + "categories";
const categoryURLs = { const categoryURLs = {
getAll: `${prefix}`, getAll: `${prefix}`,
getEmpty: `${prefix}/empty`,
getCategory: category => `${prefix}/${category}`, getCategory: category => `${prefix}/${category}`,
deleteCategory: category => `${prefix}/${category}`, deleteCategory: category => `${prefix}/${category}`,
updateCategory: category => `${prefix}/${category}`,
}; };
export const categoryAPI = { export const categoryAPI = {
@ -15,6 +17,10 @@ export const categoryAPI = {
let response = await apiReq.get(categoryURLs.getAll); let response = await apiReq.get(categoryURLs.getAll);
return response.data; return response.data;
}, },
async getEmpty() {
let response = await apiReq.get(categoryURLs.getEmpty);
return response.data;
},
async create(name) { async create(name) {
let response = await apiReq.post(categoryURLs.getAll, { name: name }); let response = await apiReq.post(categoryURLs.getAll, { name: name });
store.dispatch("requestCategories"); store.dispatch("requestCategories");
@ -24,9 +30,20 @@ export const categoryAPI = {
let response = await apiReq.get(categoryURLs.getCategory(category)); let response = await apiReq.get(categoryURLs.getCategory(category));
return response.data; return response.data;
}, },
async delete(category) { async update(name, newName, overrideRequest = false) {
let response = await apiReq.put(categoryURLs.updateCategory(name), {
name: newName,
});
if (!overrideRequest) {
store.dispatch("requestCategories");
}
return response.data;
},
async delete(category, overrideRequest = false) {
let response = await apiReq.delete(categoryURLs.deleteCategory(category)); let response = await apiReq.delete(categoryURLs.deleteCategory(category));
store.dispatch("requestCategories"); if (!overrideRequest) {
store.dispatch("requestCategories");
}
return response; return response;
}, },
}; };
@ -35,8 +52,10 @@ const tagPrefix = baseURL + "tags";
const tagURLs = { const tagURLs = {
getAll: `${tagPrefix}`, getAll: `${tagPrefix}`,
getEmpty: `${tagPrefix}/empty`,
getTag: tag => `${tagPrefix}/${tag}`, getTag: tag => `${tagPrefix}/${tag}`,
deleteTag: tag => `${tagPrefix}/${tag}`, deleteTag: tag => `${tagPrefix}/${tag}`,
updateTag: tag => `${tagPrefix}/${tag}`,
}; };
export const tagAPI = { export const tagAPI = {
@ -44,6 +63,10 @@ export const tagAPI = {
let response = await apiReq.get(tagURLs.getAll); let response = await apiReq.get(tagURLs.getAll);
return response.data; return response.data;
}, },
async getEmpty() {
let response = await apiReq.get(tagURLs.getEmpty);
return response.data;
},
async create(name) { async create(name) {
let response = await apiReq.post(tagURLs.getAll, { name: name }); let response = await apiReq.post(tagURLs.getAll, { name: name });
store.dispatch("requestTags"); store.dispatch("requestTags");
@ -53,9 +76,20 @@ export const tagAPI = {
let response = await apiReq.get(tagURLs.getTag(tag)); let response = await apiReq.get(tagURLs.getTag(tag));
return response.data; return response.data;
}, },
async delete(tag) { async update(name, newName, overrideRequest = false) {
let response = await apiReq.put(tagURLs.updateTag(name), { name: newName });
if (!overrideRequest) {
store.dispatch("requestTags");
}
return response.data;
},
async delete(tag, overrideRequest = false) {
let response = await apiReq.delete(tagURLs.deleteTag(tag)); let response = await apiReq.delete(tagURLs.deleteTag(tag));
store.dispatch("requestTags"); if (!overrideRequest) {
store.dispatch("requestTags");
}
return response.data; return response.data;
}, },
}; };

View file

@ -66,7 +66,13 @@ export const recipeAPI = {
async update(data) { async update(data) {
let response = await apiReq.put(recipeURLs.update(data.slug), data); let response = await apiReq.put(recipeURLs.update(data.slug), data);
store.dispatch("requestRecentRecipes"); store.dispatch("patchRecipe", response.data);
return response.data.slug; // ! Temporary until I rewrite to refresh page without additional request
},
async patch(data) {
let response = await apiReq.patch(recipeURLs.update(data.slug), data);
store.dispatch("patchRecipe", response.data);
return response.data; return response.data;
}, },

View file

@ -3,7 +3,8 @@
<v-card-title class=" headline"> <v-card-title class=" headline">
{{ $t("meal-plan.create-a-new-meal-plan") }} {{ $t("meal-plan.create-a-new-meal-plan") }}
<v-btn color="info" class="ml-auto" @click="setQuickWeek()"> <v-btn color="info" class="ml-auto" @click="setQuickWeek()">
<v-icon left>mdi-calendar-minus</v-icon> {{$t('meal-plan.quick-week')}} <v-icon left>mdi-calendar-minus</v-icon>
{{ $t("meal-plan.quick-week") }}
</v-btn> </v-btn>
</v-card-title> </v-card-title>
@ -153,7 +154,7 @@ export default {
return recipes.length > 0 ? recipes : this.items; return recipes.length > 0 ? recipes : this.items;
}, },
allRecipes() { allRecipes() {
return this.$store.getters.getRecentRecipes; return this.$store.getters.getAllRecipes;
}, },
}, },

View file

@ -60,10 +60,10 @@
<v-row v-else dense> <v-row v-else dense>
<v-col <v-col
cols="12" cols="12"
sm="12" :sm="singleColumn ? '12' : '12'"
md="6" :md="singleColumn ? '12' : '6'"
lg="4" :lg="singleColumn ? '12' : '4'"
xl="3" :xl="singleColumn ? '12' : '3'"
v-for="recipe in recipes.slice(0, cardLimit)" v-for="recipe in recipes.slice(0, cardLimit)"
:key="recipe.name" :key="recipe.name"
> >
@ -79,14 +79,16 @@
</v-row> </v-row>
</div> </div>
<div v-intersect="bumpList" class="d-flex"> <div v-intersect="bumpList" class="d-flex">
<v-progress-circular <v-expand-x-transition>
v-if="loading" <v-progress-circular
class="mx-auto mt-1" v-if="loading"
:size="50" class="mx-auto mt-1"
:width="7" :size="50"
color="primary" :width="7"
indeterminate color="primary"
></v-progress-circular> indeterminate
></v-progress-circular>
</v-expand-x-transition>
</div> </div>
</div> </div>
</template> </template>
@ -109,6 +111,12 @@ export default {
hardLimit: { hardLimit: {
default: 99999, default: 99999,
}, },
mobileCards: {
default: false,
},
singleColumn: {
defualt: false,
},
recipes: Array, recipes: Array,
}, },
data() { data() {
@ -117,8 +125,14 @@ export default {
loading: false, loading: false,
}; };
}, },
watch: {
recipes() {
this.bumpList();
},
},
computed: { computed: {
viewScale() { viewScale() {
if (this.mobileCards) return true;
switch (this.$vuetify.breakpoint.name) { switch (this.$vuetify.breakpoint.name) {
case "xs": case "xs":
return true; return true;
@ -128,10 +142,16 @@ export default {
return false; return false;
} }
}, },
effectiveHardLimit() {
return Math.min(this.hardLimit, this.recipes.length);
},
}, },
methods: { methods: {
bumpList() { bumpList() {
const newCardLimit = Math.min(this.cardLimit + 20, this.hardLimit); const newCardLimit = Math.min(
this.cardLimit + 20,
this.effectiveHardLimit
);
if (this.loading === false && newCardLimit > this.cardLimit) { if (this.loading === false && newCardLimit > this.cardLimit) {
this.setLoader(); this.setLoader();
@ -141,7 +161,7 @@ export default {
}, },
async setLoader() { async setLoader() {
this.loading = true; this.loading = true;
await new Promise(r => setTimeout(r, 3000)); await new Promise(r => setTimeout(r, 1000));
this.loading = false; this.loading = false;
}, },
}, },

View file

@ -1,21 +1,38 @@
<template> <template>
<div> <div>
<v-dialog v-model="dialog" :width="modalWidth + 'px'"> <v-dialog
<v-app-bar dark :color="color" class="mt-n1 mb-2"> v-model="dialog"
<v-icon large left v-if="!loading"> :width="modalWidth + 'px'"
{{ titleIcon }} :content-class="top ? 'top-dialog' : undefined"
</v-icon> >
<v-progress-circular <v-card class="pb-10" height="100%">
v-else <v-app-bar dark :color="color" class="mt-n1 mb-2">
<v-icon large left>
{{ titleIcon }}
</v-icon>
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-progress-linear
v-if="loading"
indeterminate indeterminate
color="white" color="primary"
large ></v-progress-linear>
class="mr-2" <slot> </slot>
> <v-card-actions>
</v-progress-circular> <slot name="card-actions">
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title> <v-btn text color="grey" @click="dialog = false">
<v-spacer></v-spacer> Cancel
</v-app-bar> </v-btn>
<v-spacer></v-spacer>
<v-btn color="success" @click="$emit('submit')">
Submit
</v-btn>
</slot>
</v-card-actions>
<slot name="below-actions"> </slot>
</v-card>
</v-dialog> </v-dialog>
</div> </div>
</template> </template>
@ -35,6 +52,12 @@ export default {
modalWidth: { modalWidth: {
default: "500", default: "500",
}, },
loading: {
default: false,
},
top: {
default: false,
},
}, },
data() { data() {
return { return {
@ -45,9 +68,15 @@ export default {
open() { open() {
this.dialog = true; this.dialog = true;
}, },
close() {
this.dialog = false;
},
}, },
}; };
</script> </script>
<style scoped> <style scoped>
.top-dialog {
align-self: flex-start;
}
</style> </style>

View file

@ -1,8 +1,10 @@
<template> <template>
<div> <div>
<v-btn icon @click="dialog = true" class="mt-n1"> <slot>
<v-icon :color="color">mdi-plus</v-icon> <v-btn icon @click="dialog = true" class="mt-n1">
</v-btn> <v-icon :color="color">mdi-plus</v-icon>
</v-btn>
</slot>
<v-dialog v-model="dialog" width="500"> <v-dialog v-model="dialog" width="500">
<v-card> <v-card>
<v-app-bar dense dark color="primary mb-2"> <v-app-bar dense dark color="primary mb-2">
@ -80,6 +82,9 @@ export default {
}, },
methods: { methods: {
open() {
this.dialog = true;
},
async select() { async select() {
const newItem = await (async () => { const newItem = await (async () => {
if (this.tagDialog) { if (this.tagDialog) {

View file

@ -0,0 +1,79 @@
<template>
<div>
<slot> </slot>
</div>
</template>
<script>
const RESULTS_EVENT = "results";
import Fuse from "fuse.js";
export default {
props: {
search: {
default: "",
},
rawData: {
default: true,
},
/** Defaults to Show All Results */
showAll: {
default: true,
},
keys: {
type: Array,
default: () => ["name"],
},
defaultOptions: {
default: () => ({
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
findAllMatches: true,
maxPatternLength: 32,
minMatchCharLength: 2,
}),
},
},
data() {
return {
results: [],
fuseResults: [],
};
},
computed: {
options() {
return { ...this.defaultOptions, ...{ keys: this.keys } };
},
autoResults() {
return this.fuseResults.length > 1 ? this.fuseResults : this.results;
},
fuse() {
return new Fuse(this.rawData, this.options);
},
isSearching() {
return this.search && this.search.length > 0;
},
},
watch: {
search() {
try {
this.results = this.fuse.search(this.search.trim());
} catch {
this.results = this.rawData
.map(x => ({ item: x }))
.sort((a, b) => (a.name > b.name ? 1 : -1));
}
this.$emit(RESULTS_EVENT, this.results);
if (this.showResults === true) {
this.fuseResults = this.results;
}
},
},
};
</script>
<style scoped>
</style>

View file

@ -132,7 +132,7 @@ export default {
}, },
computed: { computed: {
data() { data() {
return this.$store.getters.getRecentRecipes; return this.$store.getters.getAllRecipes;
}, },
autoResults() { autoResults() {
return this.fuseResults.length > 1 ? this.fuseResults : this.results; return this.fuseResults.length > 1 ? this.fuseResults : this.results;

View file

@ -163,6 +163,11 @@ export default {
to: "/admin/settings", to: "/admin/settings",
title: this.$t("settings.site-settings"), title: this.$t("settings.site-settings"),
}, },
{
icon: "mdi-tools",
to: "/admin/toolbox",
title: this.$t("settings.toolbox.toolbox"),
},
{ {
icon: "mdi-account-group", icon: "mdi-account-group",
to: "/admin/manage-users", to: "/admin/manage-users",

View file

@ -6,6 +6,7 @@
bottom bottom
right right
offset-y offset-y
offset-overflow
open-on-hover open-on-hover
close-delay="200" close-delay="200"
> >
@ -72,7 +73,7 @@ export default {
}, },
{ {
icon: "mdi-logout", icon: "mdi-logout",
title: this.$t('user.logout'), title: this.$t("user.logout"),
restricted: true, restricted: true,
nav: "/logout", nav: "/logout",
}, },

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "Se opskrift" "view-recipe": "Se opskrift"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Webadresserne, der er anført nedenfor, modtager webhooks, der indeholder opskriftsdataene for måltidsplanen på den planlagte dag. \nWebhooks udføres i øjeblikket på <strong> {time} </strong>", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Webadresserne, der er anført nedenfor, modtager webhooks, der indeholder opskriftsdataene for måltidsplanen på den planlagte dag. \nWebhooks udføres i øjeblikket på <strong> {time} </strong>",
"webhook-url": "Webhook adresse" "webhook-url": "Webhook adresse"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Tid", "webhook-time": "Webhook Tid",
"webhooks-enabled": "Webhooks Aktiveret" "webhooks-enabled": "Webhooks Aktiveret"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "Rezept anschauen" "view-recipe": "Rezept anschauen"
}, },
"search": { "search": {
"and": "Und", "search-mealie": "Mealie durchsuchen (/ drücken)",
"search-placeholder": "Suchen...",
"max-results": "Max. Ergebnisse",
"category-filter": "Kategoriefilter", "category-filter": "Kategoriefilter",
"exclude": "Ausschließen", "exclude": "Ausschließen",
"include": "Einbeziehen", "include": "Einbeziehen",
"max-results": "Max. Ergebnisse",
"or": "Oder", "or": "Oder",
"and": "and",
"search": "Suchen", "search": "Suchen",
"search-mealie": "Mealie durchsuchen",
"search-placeholder": "Suchen...",
"tag-filter": "Schlagwortfilter" "tag-filter": "Schlagwortfilter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Teste Webhooks", "test-webhooks": "Teste Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Die unten stehenden URL's erhalten Webhooks welche die Rezeptdaten für den Menüplan am geplanten Tag enthalten. Derzeit werden die Webhooks ausgeführt um", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Die unten stehenden URL's erhalten Webhooks welche die Rezeptdaten für den Menüplan am geplanten Tag enthalten. Derzeit werden die Webhooks ausgeführt um",
"webhook-url": "Webhook-URL" "webhook-url": "Webhook-URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Zeit", "webhook-time": "Webhook Zeit",
"webhooks-enabled": "Webhooks aktiviert" "webhooks-enabled": "Webhooks aktiviert"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -180,6 +180,7 @@
"include": "Include", "include": "Include",
"max-results": "Max Results", "max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie (press /)", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...", "search-placeholder": "Search...",
@ -264,6 +265,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"this": {}, "this": {},

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -47,7 +47,7 @@
"reset": "Réinitialiser", "reset": "Réinitialiser",
"saturday": "Samedi", "saturday": "Samedi",
"save": "Sauvegarder", "save": "Sauvegarder",
"settings": "Options", "settings": "Paramètres",
"sort": "Trier", "sort": "Trier",
"sort-alphabetically": "A-Z", "sort-alphabetically": "A-Z",
"submit": "Soumettre", "submit": "Soumettre",
@ -145,15 +145,15 @@
"view-recipe": "Voir la recette" "view-recipe": "Voir la recette"
}, },
"search": { "search": {
"and": "Et", "search-mealie": "Rechercher dans Mealie (appuyez sur /)",
"search-placeholder": "Rechercher...",
"max-results": "Résultats max",
"category-filter": "Filtre par catégories", "category-filter": "Filtre par catégories",
"exclude": "Exclure", "exclude": "Exclure",
"include": "Inclure", "include": "Inclure",
"max-results": "Résultats max",
"or": "Ou", "or": "Ou",
"and": "and",
"search": "Rechercher", "search": "Rechercher",
"search-mealie": "Rechercher dans Mealie",
"search-placeholder": "Rechercher...",
"tag-filter": "Filtre par mots-clés" "tag-filter": "Filtre par mots-clés"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Tester les webhooks", "test-webhooks": "Tester les webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Les liens dans cette liste recevront les webhooks contenant les recettes pour le menu du jour. Actuellement, les webhooks se lancent à", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Les liens dans cette liste recevront les webhooks contenant les recettes pour le menu du jour. Actuellement, les webhooks se lancent à",
"webhook-url": "Lien du webhook" "webhook-url": "Lien du webhook"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Heure du Webhook", "webhook-time": "Heure du Webhook",
"webhooks-enabled": "Webhooks activés" "webhooks-enabled": "Webhooks activés"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "Bekijk Recept" "view-recipe": "Bekijk Recept"
}, },
"search": { "search": {
"and": "En", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Zoeken...",
"max-results": "Max. resultaten",
"category-filter": "Categorie Filter", "category-filter": "Categorie Filter",
"exclude": "Exclusief", "exclude": "Exclusief",
"include": "Inclusief", "include": "Inclusief",
"max-results": "Max. resultaten",
"or": "Of", "or": "Of",
"and": "and",
"search": "Zoek", "search": "Zoek",
"search-mealie": "Mealie Zoeken",
"search-placeholder": "Zoeken...",
"tag-filter": "Label Filter" "tag-filter": "Label Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Webhooks testen", "test-webhooks": "Webhooks testen",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "De onderstaande URL's ontvangen webhooks met receptgegevens voor het maaltijdplan op de geplande dag. Momenteel zullen Webhooks uitvoeren op", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "De onderstaande URL's ontvangen webhooks met receptgegevens voor het maaltijdplan op de geplande dag. Momenteel zullen Webhooks uitvoeren op",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Tijd", "webhook-time": "Webhook Tijd",
"webhooks-enabled": "Webhooks ingeschakeld" "webhooks-enabled": "Webhooks ingeschakeld"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "Wyświetl przepis" "view-recipe": "Wyświetl przepis"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Przeszukaj Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Testuj webhooki", "test-webhooks": "Testuj webhooki",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Odnośniki poniżej otrzymają webhook zawierający dane o przepisie dla danego dnia. Aktualnie webhooki zostanę wykonane o", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Odnośniki poniżej otrzymają webhook zawierający dane o przepisie dla danego dnia. Aktualnie webhooki zostanę wykonane o",
"webhook-url": "Odnośnik webhooka" "webhook-url": "Odnośnik webhooka"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "Ver Receita" "view-recipe": "Ver Receita"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Pesquisar Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Webhooks de Teste", "test-webhooks": "Webhooks de Teste",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Os URLs apresentados abaixo receberão webhooks que contêm os dados da receita para o plano de refeições no dia marcado. Atualmente, os webhooks serão executados a ", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Os URLs apresentados abaixo receberão webhooks que contêm os dados da receita para o plano de refeições no dia marcado. Atualmente, os webhooks serão executados a ",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "Visa recept" "view-recipe": "Visa recept"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Testa Webhooks", "test-webhooks": "Testa Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Följande URLer kommer att mottaga webhooks med receptdata för dagens planerade måltid. Datan kommer att skickas klockan <strong>{ time }</strong>", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Följande URLer kommer att mottaga webhooks med receptdata för dagens planerade måltid. Datan kommer att skickas klockan <strong>{ time }</strong>",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "View Recipe" "view-recipe": "View Recipe"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "Search Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "Test Webhooks", "test-webhooks": "Test Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
"webhook-url": "Webhook URL" "webhook-url": "Webhook URL"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "查看食谱" "view-recipe": "查看食谱"
}, },
"search": { "search": {
"and": "与", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "搜索...",
"max-results": "最大结果",
"category-filter": "分类筛选", "category-filter": "分类筛选",
"exclude": "排除", "exclude": "排除",
"include": "包括", "include": "包括",
"max-results": "最大结果",
"or": "或", "or": "或",
"and": "and",
"search": "搜索", "search": "搜索",
"search-mealie": "搜索Mealie",
"search-placeholder": "搜索...",
"tag-filter": "标签筛选" "tag-filter": "标签筛选"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "测试Webhooks", "test-webhooks": "测试Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "下方列出的网址将在预定日期接收到有关用餐计划的食谱资料。Webhooks执行将在", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "下方列出的网址将在预定日期接收到有关用餐计划的食谱资料。Webhooks执行将在",
"webhook-url": "Webhook网址" "webhook-url": "Webhook网址"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook时间", "webhook-time": "Webhook时间",
"webhooks-enabled": "Webhooks 启用" "webhooks-enabled": "Webhooks 启用"
} }
} }

View file

@ -145,15 +145,15 @@
"view-recipe": "查看食譜" "view-recipe": "查看食譜"
}, },
"search": { "search": {
"and": "And", "search-mealie": "Search Mealie (press /)",
"search-placeholder": "Search...",
"max-results": "Max Results",
"category-filter": "Category Filter", "category-filter": "Category Filter",
"exclude": "Exclude", "exclude": "Exclude",
"include": "Include", "include": "Include",
"max-results": "Max Results",
"or": "Or", "or": "Or",
"and": "and",
"search": "Search", "search": "Search",
"search-mealie": "搜索Mealie",
"search-placeholder": "Search...",
"tag-filter": "Tag Filter" "tag-filter": "Tag Filter"
}, },
"settings": { "settings": {
@ -218,6 +218,16 @@
"test-webhooks": "測試Webhooks", "test-webhooks": "測試Webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "下方列出的網址將在預定日期接收到有關用餐計劃的食譜資料。Webhooks將在<strong>{ time }</strong>執行", "the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "下方列出的網址將在預定日期接收到有關用餐計劃的食譜資料。Webhooks將在<strong>{ time }</strong>執行",
"webhook-url": "Webhook網址" "webhook-url": "Webhook網址"
},
"toolbox": {
"toolbox": "Toolbox",
"new-name": "New Name",
"recipes-effected": "Recipes Effected",
"title-case-all": "Title Case All",
"no-unused-items": "No Unused Items",
"remove-unused": "Remove Unused",
"assign-all": "Assign All",
"bulk-assign": "Bulk Assign"
} }
}, },
"user": { "user": {
@ -266,4 +276,4 @@
"webhook-time": "Webhook Time", "webhook-time": "Webhook Time",
"webhooks-enabled": "Webhooks Enabled" "webhooks-enabled": "Webhooks Enabled"
} }
} }

View file

@ -0,0 +1,171 @@
<template>
<div>
<base-dialog
ref="assignDialog"
title-icon="mdi-tag"
color="primary"
title="Bulk Assign"
:loading="loading"
modal-width="700"
:top="true"
>
<v-card-text>
<v-text-field
v-model="search"
autocomplete="off"
label="Keyword"
></v-text-field>
<CategoryTagSelector
:tag-selector="false"
v-model="catsToAssign"
:return-object="false"
/>
<CategoryTagSelector
:tag-selector="true"
v-model="tagsToAssign"
:return-object="false"
/>
</v-card-text>
<template slot="card-actions">
<v-btn text color="grey" @click="closeDialog">
{{ $t("general.cancel") }}
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="success"
@click="assignAll"
:loading="loading"
:disabled="results.length < 1"
>
{{ $t("settings.toolbox.assign-all") }}
</v-btn>
</template>
<template slot="below-actions">
<v-card-title class="headline"> </v-card-title>
<CardSection
class="px-2 pb-2"
:title="`${results.length || 0} Recipes Effected`"
:mobile-cards="true"
:recipes="results"
:single-column="true"
/>
</template>
</base-dialog>
<v-btn @click="openDialog" small color="success">
{{ $t("settings.toolbox.bulk-assign") }}
</v-btn>
</div>
</template>
<script>
import CardSection from "@/components/UI/CardSection";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
export default {
props: {
isTags: {
default: true,
},
},
components: {
CardSection,
BaseDialog,
CategoryTagSelector,
},
data() {
return {
results: [],
search: "",
loading: false,
assignTargetRecipes: [],
catsToAssign: [],
tagsToAssign: [],
};
},
mounted() {
this.$store.dispatch("requestAllRecipes");
},
watch: {
search() {
this.getResults();
},
},
computed: {
allRecipes() {
return this.$store.getters.getAllRecipes;
},
// results() {
// if (this.search === null || this.search === "") {
// return [];
// }
// return this.allRecipes.filter(x => {
// return (
// this.checkForKeywords(x.name) || this.checkForKeywords(x.description)
// );
// });
// },
keywords() {
const lowered = this.search.toLowerCase();
return lowered.split(" ");
},
},
methods: {
reset() {
this.search = "";
this.loading = false;
this.assignTargetRecipes = [];
this.catsToAssign = [];
this.tagsToAssign = [];
},
assignAll() {
this.loading = true;
this.results.forEach(async element => {
element.recipeCategory = element.recipeCategory.concat(
this.catsToAssign
);
element.tags = element.tags.concat(this.tagsToAssign);
await api.recipes.patch(element);
});
this.loading = false;
this.closeDialog();
},
closeDialog() {
this.$refs.assignDialog.close();
},
async openDialog() {
this.$refs.assignDialog.open();
this.reset();
},
getResults() {
this.loading = true;
// cancel pending call
clearTimeout(this._timerId);
this._timerId = setTimeout(() => {
this.results = this.filterResults();
}, 300);
this.loading = false;
// delay new call 500ms
},
filterResults() {
if (this.search === null || this.search === "") {
return [];
}
return this.allRecipes.filter(x => {
return (
this.checkForKeywords(x.name) || this.checkForKeywords(x.description)
);
});
},
checkForKeywords(str) {
const searchStr = str.toLowerCase();
return this.keywords.some(x => searchStr.includes(x));
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,100 @@
<template>
<div>
<base-dialog
ref="deleteDialog"
title-icon="mdi-tag"
color="error"
:title="
$t('general.delete') +
' ' +
(isTags ? $t('recipe.tags') : $t('recipe.categories'))
"
:loading="loading"
modal-width="400"
>
<v-list v-if="deleteList.length > 0">
<v-list-item v-for="item in deleteList" :key="item.slug">
<v-list-item-content>
{{ item.name }}
</v-list-item-content>
</v-list-item>
</v-list>
<v-card-text v-else class=" mt-4 text-center">
{{ $t("settings.toolbox.no-unused-items") }}
</v-card-text>
<template slot="card-actions">
<v-btn text color="grey" @click="closeDialog">
{{ $t("general.cancel") }}
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="error"
@click="deleteUnused"
:loading="loading"
:disabled="deleteList.length < 1"
>
{{ $t("general.delete") }}
</v-btn>
</template>
</base-dialog>
<v-btn @click="openDialog" small color="error" class="mr-1">
{{ $t("settings.toolbox.remove-unused") }}
</v-btn>
</div>
</template>
<script>
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
export default {
props: {
isTags: {
default: true,
},
},
components: {
BaseDialog,
},
data() {
return {
deleteList: [],
loading: false,
};
},
methods: {
closeDialog() {
this.$refs.deleteDialog.close();
},
async openDialog() {
this.$refs.deleteDialog.open();
console.log(this.isTags);
if (this.isTags) {
this.deleteList = await api.tags.getEmpty();
} else {
this.deleteList = await api.categories.getEmpty();
}
},
async deleteUnused() {
this.loading = true;
if (this.isTags) {
this.deleteList.forEach(async element => {
await api.tags.delete(element.slug, true);
});
this.$store.dispatch("requestTags");
} else {
this.deleteList.forEach(async element => {
await api.categories.delete(element.slug, true);
});
this.$store.dispatch("requestCategories");
}
this.loading = false;
this.closeDialog();
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,244 @@
<template>
<v-card outlined class="mt-n1">
<base-dialog
ref="renameDialog"
title-icon="mdi-tag"
:title="renameTarget.title"
modal-width="800"
@submit="renameFromDialog(renameTarget.slug, renameTarget.newName)"
>
<v-form ref="renameForm">
<v-card-text>
<v-text-field
:placeholder="$t('settings.toolbox.new-name')"
:rules="[existsRule]"
v-model="renameTarget.newName"
></v-text-field>
</v-card-text>
</v-form>
<template slot="below-actions">
<v-card-title class="headline">
{{ renameTarget.recipes.length || 0 }}
{{ $t("settings.toolbox.recipes-effected") }}
</v-card-title>
<MobileRecipeCard
class="ml-2 mr-2 mt-2 mb-2"
v-for="recipe in renameTarget.recipes"
:key="recipe.slug"
:slug="recipe.slug"
:name="recipe.name"
:description="recipe.description"
:rating="recipe.rating"
:route="false"
:tags="recipe.tags"
/>
</template>
</base-dialog>
<div class="d-flex justify-center align-center pa-2 flex-wrap">
<new-category-tag-dialog ref="newDialog" :tag-dialog="isTags">
<v-btn @click="openNewDialog" small color="success" class="mr-1 mb-1">
{{ $t("general.create") }}
</v-btn>
</new-category-tag-dialog>
<BulkAssign isTags="isTags" class="mr-1 mb-1" />
<v-btn
@click="titleCaseAll"
small
color="success"
class="mr-1 mb-1"
:loading="loadingTitleCase"
>
{{ $t("settings.toolbox.title-case-all") }}
</v-btn>
<RemoveUnused :isTags="isTags" class="mb-1" />
<v-spacer v-if="!isMobile"> </v-spacer>
<fuse-search-bar
:raw-data="allItems"
@results="filterItems"
:search="searchString"
>
<v-text-field
v-model="searchString"
clearable
solo
dense
class="mx-2"
hide-details
single-line
:placeholder="$t('search.search')"
prepend-inner-icon="mdi-magnify"
>
</v-text-field>
</fuse-search-bar>
</div>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col
cols="12"
:sm="12"
:md="6"
:lg="4"
:xl="3"
v-for="item in results"
:key="item.id"
>
<v-card>
<v-card-actions>
<v-card-title class="py-1">{{ item.name }}</v-card-title>
<v-spacer></v-spacer>
<v-btn small text color="info" @click="openEditDialog(item)">
Edit
</v-btn>
<v-btn small text color="error" @click="deleteItem(item.slug)"
>Delete
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar";
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
import RemoveUnused from "./RemoveUnused";
import BulkAssign from "./BulkAssign";
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog";
export default {
mixins: [validators],
components: {
BaseDialog,
MobileRecipeCard,
FuseSearchBar,
RemoveUnused,
NewCategoryTagDialog,
BulkAssign,
},
props: {
isTags: {
default: true,
},
},
data() {
return {
loadingTitleCase: false,
searchString: "",
searchResults: [],
renameTarget: {
title: "",
name: "",
slug: "",
newName: "",
recipes: [],
},
};
},
computed: {
isMobile() {
return this.$vuetify.breakpoint.name === "xs";
},
allItems() {
return this.isTags
? this.$store.getters.getAllTags
: this.$store.getters.getAllCategories;
},
results() {
if (this.searchString != null && this.searchString.length >= 1) {
return this.searchResults;
}
return this.allItems;
},
},
methods: {
filterItems(val) {
this.searchResults = val.map(x => x.item);
},
openNewDialog() {
this.$refs.newDialog.open();
},
async openEditDialog(item) {
let fromAPI = {};
if (this.isTags) {
fromAPI = await api.tags.getRecipesInTag(item.slug);
} else {
fromAPI = await api.categories.getRecipesInCategory(item.slug);
}
this.renameTarget = {
title: `Rename ${item.name}`,
name: item.name,
slug: item.slug,
newName: "",
recipes: fromAPI.recipes,
};
this.$refs.renameDialog.open();
},
async deleteItem(name) {
if (this.isTags) {
await api.tags.delete(name);
} else {
await api.categories.delete(name);
}
},
async renameFromDialog(name, newName) {
if (this.$refs.renameForm.validate()) {
await this.rename(name, newName);
}
this.$refs.renameDialog.close();
},
async rename(name, newName) {
if (this.isTags) {
await api.tags.update(name, newName);
} else {
await api.categories.update(name, newName);
}
},
titleCase(lowerName) {
return lowerName.replace(/(?:^|\s|-)\S/g, x => x.toUpperCase());
},
async titleCaseAll() {
this.loadingTitleCase = true;
const renameList = this.allItems.map(x => ({
slug: x.slug,
name: x.name,
newName: this.titleCase(x.name),
}));
if (this.isTags) {
renameList.forEach(async element => {
if (element.name === element.newName) return;
await api.tags.update(element.slug, element.newName, true);
});
this.$store.dispatch("requestTags");
} else {
renameList.forEach(async element => {
if (element.name === element.newName) return;
await api.categories.update(element.slug, element.newName, true);
});
this.$store.dispatch("requestCategories");
}
this.loadingTitleCase = false;
},
},
};
</script>
<style>
.overflow-fix .v-toolbar__content {
height: auto !important;
flex-wrap: wrap;
}
</style>

View file

@ -0,0 +1,47 @@
<template>
<div>
<v-card flat>
<v-tabs
v-model="tab"
background-color="primary"
centered
dark
icons-and-text
>
<v-tabs-slider></v-tabs-slider>
<v-tab>
{{ $t("recipe.categories") }}
<v-icon>mdi-tag-multiple-outline</v-icon>
</v-tab>
<v-tab>
{{ $t("recipe.tags") }}
<v-icon>mdi-tag-multiple-outline</v-icon>
</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item><CategoryTagEditor :is-tags="false"/></v-tab-item>
<v-tab-item><CategoryTagEditor :is-tags="true" /> </v-tab-item>
</v-tabs-items>
</v-card>
</div>
</template>
<script>
import CategoryTagEditor from "./CategoryTagEditor";
export default {
components: {
CategoryTagEditor,
},
data() {
return {
tab: 0,
};
},
};
</script>
<style>
</style>

View file

@ -38,8 +38,8 @@ export default {
return this.$store.getters.getSiteSettings; return this.$store.getters.getSiteSettings;
}, },
recentRecipes() { recentRecipes() {
let recipes = this.$store.getters.getRecentRecipes; return this.$store.getters.getRecentRecipes;
return recipes.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
}, },
}, },
async mounted() { async mounted() {

View file

@ -1,5 +1,10 @@
<template> <template>
<v-container> <v-container>
<v-progress-linear
v-if="loading"
indeterminate
color="primary"
></v-progress-linear>
<CardSection <CardSection
:sortable="true" :sortable="true"
:title="$t('page.all-recipes')" :title="$t('page.all-recipes')"
@ -18,14 +23,20 @@ export default {
CardSection, CardSection,
}, },
data() { data() {
return {}; return {
loading: false,
};
}, },
mounted() { async mounted() {
this.$store.dispatch("requestAllRecipes"); if (this.allRecipes.length < 1) {
this.loading = true;
}
await this.$store.dispatch("requestAllRecipes");
this.loading = false;
}, },
computed: { computed: {
allRecipes() { allRecipes() {
return this.$store.getters.getRecentRecipes; return this.$store.getters.getAllRecipes;
}, },
}, },
methods: { methods: {

View file

@ -119,7 +119,7 @@ export default {
}, },
computed: { computed: {
allRecipes() { allRecipes() {
return this.$store.getters.getRecentRecipes; return this.$store.getters.getAllRecipes;
}, },
filteredRecipes() { filteredRecipes() {
return this.allRecipes.filter(recipe => { return this.allRecipes.filter(recipe => {

View file

@ -7,9 +7,10 @@ import Profile from "@/pages/Admin/Profile";
import ManageUsers from "@/pages/Admin/ManageUsers"; import ManageUsers from "@/pages/Admin/ManageUsers";
import Settings from "@/pages/Admin/Settings"; import Settings from "@/pages/Admin/Settings";
import About from "@/pages/Admin/About"; import About from "@/pages/Admin/About";
import ToolBox from "@/pages/Admin/ToolBox";
import { store } from "../store"; import { store } from "../store";
export const adminRoutes = { export const adminRoutes = {
path: "/admin", path: "/admin",
component: Admin, component: Admin,
beforeEnter: (to, _from, next) => { beforeEnter: (to, _from, next) => {
@ -72,6 +73,13 @@ export const adminRoutes = {
title: "settings.site-settings", title: "settings.site-settings",
}, },
}, },
{
path: "toolbox",
component: ToolBox,
meta: {
title: "settings.toolbox.toolbox",
},
},
{ {
path: "about", path: "about",
component: About, component: About,

View file

@ -4,12 +4,12 @@ import { authRoutes } from "./auth";
import { recipeRoutes } from "./recipes"; import { recipeRoutes } from "./recipes";
import { mealRoutes } from "./meal"; import { mealRoutes } from "./meal";
import { generalRoutes } from "./general"; import { generalRoutes } from "./general";
import { store } from "../store"; import { store } from "@/store";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import VueI18n from "@/i18n"; import VueI18n from "@/i18n";
import Vuetify from "@/plugins/vuetify"; import Vuetify from "@/plugins/vuetify";
import Vue from "vue"; import Vue from "vue";
import i18n from '@/i18n.js'; import i18n from "@/i18n.js";
export const routes = [ export const routes = [
...generalRoutes, ...generalRoutes,

View file

@ -5,6 +5,7 @@ import createPersistedState from "vuex-persistedstate";
import userSettings from "./modules/userSettings"; import userSettings from "./modules/userSettings";
import language from "./modules/language"; import language from "./modules/language";
import siteSettings from "./modules/siteSettings"; import siteSettings from "./modules/siteSettings";
import recipes from "./modules/recipes";
import groups from "./modules/groups"; import groups from "./modules/groups";
Vue.use(Vuex); Vue.use(Vuex);
@ -20,6 +21,7 @@ const store = new Vuex.Store({
language, language,
siteSettings, siteSettings,
groups, groups,
recipes,
}, },
state: { state: {
// All Recipe Data Store // All Recipe Data Store
@ -35,9 +37,6 @@ const store = new Vuex.Store({
}, },
mutations: { mutations: {
setRecentRecipes(state, payload) {
state.recentRecipes = payload;
},
setMealPlanCategories(state, payload) { setMealPlanCategories(state, payload) {
state.mealPlanCategories = payload; state.mealPlanCategories = payload;
}, },
@ -53,18 +52,6 @@ const store = new Vuex.Store({
}, },
actions: { actions: {
async requestRecentRecipes({ getters }) {
const payload = await api.recipes.allSummary(0, 30);
const recent = getters.getRecentRecipes;
if (recent.length >= 30) return;
this.commit("setRecentRecipes", payload);
},
async requestAllRecipes({ getters }) {
const recent = getters.getRecentRecipes;
const start = recent.length + 1;
const payload = await api.recipes.allSummary(start, 9999);
this.commit("setRecentRecipes", [...recent, ...payload]);
},
async requestCategories({ commit }) { async requestCategories({ commit }) {
const categories = await api.categories.getAll(); const categories = await api.categories.getAll();
commit("setAllCategories", categories); commit("setAllCategories", categories);
@ -80,7 +67,6 @@ const store = new Vuex.Store({
}, },
getters: { getters: {
getRecentRecipes: state => state.recentRecipes,
getMealPlanCategories: state => state.mealPlanCategories, getMealPlanCategories: state => state.mealPlanCategories,
getAllCategories: state => getAllCategories: state =>
state.allCategories.sort((a, b) => (a.slug > b.slug ? 1 : -1)), state.allCategories.sort((a, b) => (a.slug > b.slug ? 1 : -1)),

View file

@ -0,0 +1,73 @@
import { api } from "@/api";
const state = {
recentRecipes: [],
allRecipes: [],
};
const mutations = {
setRecentRecipes(state, payload) {
state.recentRecipes = payload;
},
patchRecentRecipes(state, payload) {
if (state.recentRecipes[payload.id]) {
state.recentRecipes[payload.id] = payload;
}
},
dropRecentRecipes(state, payload) {
if (state.recentRecipes[payload.id]) {
delete state.recentRecipes[payload.id];
}
},
setAllRecipes(state, payload) {
state.allRecipes = payload;
},
patchAllRecipes(state, payload) {
state.allRecipes[payload.id] = payload;
},
dropAllRecipes(state, payload) {
if (state.allRecipes[payload.id]) {
delete state.allRecipes[payload.id];
}
},
};
const actions = {
async requestRecentRecipes() {
const payload = await api.recipes.allSummary(0, 30);
payload.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
console.log(payload);
const hash = Object.fromEntries(payload.map(e => [e.id, e]));
this.commit("setRecentRecipes", hash);
},
async requestAllRecipes({ getters }) {
const all = getters.getAllRecipes;
const payload = await api.recipes.allSummary(all.length, 9999);
const hash = Object.fromEntries([...all, ...payload].map(e => [e.id, e]));
console.log(hash);
this.commit("setAllRecipes", hash);
},
patchRecipe({ commit }, payload) {
commit("patchAllRecipes", payload);
commit("patchRecentRecipes", payload);
},
dropRecipe({ commit }, payload) {
commit("dropAllRecipes", payload);
commit("dropRecentRecipes", payload);
},
};
const getters = {
getAllRecipes: state => Object.values(state.allRecipes),
getAllRecipesHash: state => state.allRecipes,
getRecentRecipes: state => Object.values(state.recentRecipes),
getRecentRecipesHash: state => state.recentRecipes,
};
export default {
state,
mutations,
actions,
getters,
};

View file

@ -29,10 +29,10 @@ const actions = {
let settings = await api.siteSettings.get(); let settings = await api.siteSettings.get();
commit("setSettings", settings); commit("setSettings", settings);
}, },
async requestCustomPages({commit }) { async requestCustomPages({ commit }) {
const customPages = await api.siteSettings.getPages() const customPages = await api.siteSettings.getPages();
commit("setCustomPages", customPages) commit("setCustomPages", customPages);
} },
}; };
const getters = { const getters = {

View file

@ -1,3 +1,5 @@
from logging import getLogger
from mealie.db.db_base import BaseDocument from mealie.db.db_base import BaseDocument
from mealie.db.models.group import Group from mealie.db.models.group import Group
from mealie.db.models.mealplan import MealPlanModel from mealie.db.models.mealplan import MealPlanModel
@ -16,12 +18,13 @@ from mealie.schema.theme import SiteTheme
from mealie.schema.user import GroupInDB, UserInDB from mealie.schema.user import GroupInDB, UserInDB
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
logger = getLogger()
class _Recipes(BaseDocument): class _Recipes(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "slug" self.primary_key = "slug"
self.sql_model: RecipeModel = RecipeModel self.sql_model: RecipeModel = RecipeModel
self.orm_mode = True
self.schema: Recipe = Recipe self.schema: Recipe = Recipe
def update_image(self, session: Session, slug: str, extension: str = None) -> str: def update_image(self, session: Session, slug: str, extension: str = None) -> str:
@ -36,23 +39,26 @@ class _Categories(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "slug" self.primary_key = "slug"
self.sql_model = Category self.sql_model = Category
self.orm_mode = True
self.schema = RecipeCategoryResponse self.schema = RecipeCategoryResponse
def get_empty(self, session: Session):
return session.query(Category).filter(~Category.recipes.any()).all()
class _Tags(BaseDocument): class _Tags(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "slug" self.primary_key = "slug"
self.sql_model = Tag self.sql_model = Tag
self.orm_mode = True
self.schema = RecipeTagResponse self.schema = RecipeTagResponse
def get_empty(self, session: Session):
return session.query(Tag).filter(~Tag.recipes.any()).all()
class _Meals(BaseDocument): class _Meals(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "uid" self.primary_key = "uid"
self.sql_model = MealPlanModel self.sql_model = MealPlanModel
self.orm_mode = True
self.schema = MealPlanInDB self.schema = MealPlanInDB
@ -60,7 +66,6 @@ class _Settings(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "id" self.primary_key = "id"
self.sql_model = SiteSettings self.sql_model = SiteSettings
self.orm_mode = True
self.schema = SiteSettingsSchema self.schema = SiteSettingsSchema
@ -68,7 +73,6 @@ class _Themes(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "name" self.primary_key = "name"
self.sql_model = SiteThemeModel self.sql_model = SiteThemeModel
self.orm_mode = True
self.schema = SiteTheme self.schema = SiteTheme
@ -76,7 +80,6 @@ class _Users(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "id" self.primary_key = "id"
self.sql_model = User self.sql_model = User
self.orm_mode = True
self.schema = UserInDB self.schema = UserInDB
def update_password(self, session, id, password: str): def update_password(self, session, id, password: str):
@ -91,7 +94,6 @@ class _Groups(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "id" self.primary_key = "id"
self.sql_model = Group self.sql_model = Group
self.orm_mode = True
self.schema = GroupInDB self.schema = GroupInDB
def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanInDB]: def get_meals(self, session: Session, match_value: str, match_key: str = "name") -> list[MealPlanInDB]:
@ -116,7 +118,6 @@ class _SignUps(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "token" self.primary_key = "token"
self.sql_model = SignUp self.sql_model = SignUp
self.orm_mode = True
self.schema = SignUpOut self.schema = SignUpOut
@ -124,7 +125,6 @@ class _CustomPages(BaseDocument):
def __init__(self) -> None: def __init__(self) -> None:
self.primary_key = "id" self.primary_key = "id"
self.sql_model = CustomPage self.sql_model = CustomPage
self.orm_mode = True
self.schema = CustomPageOut self.schema = CustomPageOut

View file

@ -12,7 +12,6 @@ class BaseDocument:
self.primary_key: str self.primary_key: str
self.store: str self.store: str
self.sql_model: SqlAlchemyBase self.sql_model: SqlAlchemyBase
self.orm_mode = False
self.schema: BaseModel self.schema: BaseModel
# TODO: Improve Get All Query Functionality # TODO: Improve Get All Query Functionality
@ -138,3 +137,4 @@ class BaseDocument:
session.delete(result) session.delete(result)
session.commit() session.commit()

View file

@ -11,28 +11,28 @@ site_settings2categories = sa.Table(
"site_settings2categoories", "site_settings2categoories",
SqlAlchemyBase.metadata, SqlAlchemyBase.metadata,
sa.Column("sidebar_id", sa.Integer, sa.ForeignKey("site_settings.id")), sa.Column("sidebar_id", sa.Integer, sa.ForeignKey("site_settings.id")),
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")), sa.Column("category_id", sa.String, sa.ForeignKey("categories.id")),
) )
group2categories = sa.Table( group2categories = sa.Table(
"group2categories", "group2categories",
SqlAlchemyBase.metadata, SqlAlchemyBase.metadata,
sa.Column("group_id", sa.Integer, sa.ForeignKey("groups.id")), sa.Column("group_id", sa.Integer, sa.ForeignKey("groups.id")),
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")), sa.Column("category_id", sa.String, sa.ForeignKey("categories.id")),
) )
recipes2categories = sa.Table( recipes2categories = sa.Table(
"recipes2categories", "recipes2categories",
SqlAlchemyBase.metadata, SqlAlchemyBase.metadata,
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")), sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")), sa.Column("category_id", sa.String, sa.ForeignKey("categories.id")),
) )
custom_pages2categories = sa.Table( custom_pages2categories = sa.Table(
"custom_pages2categories", "custom_pages2categories",
SqlAlchemyBase.metadata, SqlAlchemyBase.metadata,
sa.Column("custom_page_id", sa.Integer, sa.ForeignKey("custom_pages.id")), sa.Column("custom_page_id", sa.Integer, sa.ForeignKey("custom_pages.id")),
sa.Column("category_slug", sa.String, sa.ForeignKey("categories.slug")), sa.Column("category_id", sa.String, sa.ForeignKey("categories.id")),
) )
@ -52,6 +52,9 @@ class Category(SqlAlchemyBase):
self.name = name.strip() self.name = name.strip()
self.slug = slugify(name) self.slug = slugify(name)
def update(self, name, session=None) -> None:
self.__init__(name, session)
@staticmethod @staticmethod
def get_ref(session, slug: str): def get_ref(session, slug: str):
return session.query(Category).filter(Category.slug == slug).one() return session.query(Category).filter(Category.slug == slug).one()

View file

@ -86,6 +86,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
rating: int = None, rating: int = None,
orgURL: str = None, orgURL: str = None,
extras: dict = None, extras: dict = None,
*args,
**kwargs
) -> None: ) -> None:
self.name = name self.name = name
self.description = description self.description = description
@ -139,6 +141,8 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
rating: int = None, rating: int = None,
orgURL: str = None, orgURL: str = None,
extras: dict = None, extras: dict = None,
*args,
**kwargs
): ):
"""Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions""" """Updated a database entry by removing nested rows and rebuilds the row through the __init__ functions"""

View file

@ -11,7 +11,7 @@ recipes2tags = sa.Table(
"recipes2tags", "recipes2tags",
SqlAlchemyBase.metadata, SqlAlchemyBase.metadata,
sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")), sa.Column("recipe_id", sa.Integer, sa.ForeignKey("recipes.id")),
sa.Column("tag_slug", sa.Integer, sa.ForeignKey("tags.slug")), sa.Column("tag_id", sa.Integer, sa.ForeignKey("tags.id")),
) )
@ -31,6 +31,9 @@ class Tag(SqlAlchemyBase):
self.name = name.strip() self.name = name.strip()
self.slug = slugify(self.name) self.slug = slugify(self.name)
def update(self, name, session=None) -> None:
self.__init__(name, session)
@staticmethod @staticmethod
def create_if_not_exist(session, name: str = None): def create_if_not_exist(session, name: str = None):
test_slug = slugify(name) test_slug = slugify(name)

View file

@ -17,6 +17,18 @@ async def get_all_recipe_categories(session: Session = Depends(generate_session)
return db.categories.get_all_limit_columns(session, ["slug", "name"]) return db.categories.get_all_limit_columns(session, ["slug", "name"])
@router.get("/empty")
def get_empty_categories(session: Session = Depends(generate_session)):
""" Returns a list of categories that do not contain any recipes"""
return db.categories.get_empty(session)
@router.get("/{category}", response_model=RecipeCategoryResponse)
def get_all_recipes_by_category(category: str, session: Session = Depends(generate_session)):
""" Returns a list of recipes associated with the provided category. """
return db.categories.get(session, category)
@router.post("") @router.post("")
async def create_recipe_category( async def create_recipe_category(
category: CategoryIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user) category: CategoryIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
@ -26,10 +38,16 @@ async def create_recipe_category(
return db.categories.create(session, category.dict()) return db.categories.create(session, category.dict())
@router.get("/{category}", response_model=RecipeCategoryResponse) @router.put("/{category}", response_model=RecipeCategoryResponse)
def get_all_recipes_by_category(category: str, session: Session = Depends(generate_session)): async def update_recipe_category(
""" Returns a list of recipes associated with the provided category. """ category: str,
return db.categories.get(session, category) new_category: CategoryIn,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Updates an existing Tag in the database """
return db.categories.update(session, category, new_category.dict())
@router.delete("/{category}") @router.delete("/{category}")

View file

@ -1,7 +1,5 @@
import shutil
from enum import Enum from enum import Enum
import requests
from fastapi import APIRouter, Depends, File, Form, HTTPException, status from fastapi import APIRouter, Depends, File, Form, HTTPException, status
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from mealie.db.database import db from mealie.db.database import db
@ -12,10 +10,7 @@ from mealie.services.image.image import IMG_OPTIONS, delete_image, read_image, r
from mealie.services.scraper.scraper import create_from_url from mealie.services.scraper.scraper import create_from_url
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter( router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
prefix="/api/recipes",
tags=["Recipe CRUD"],
)
@router.post("/create", status_code=201, response_model=str) @router.post("/create", status_code=201, response_model=str)
@ -65,7 +60,30 @@ def update_recipe(
if recipe_slug != recipe.slug: if recipe_slug != recipe.slug:
rename_image(original_slug=recipe_slug, new_slug=recipe.slug) rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
return recipe.slug return recipe
@router.patch("/{recipe_slug}")
def update_recipe(
recipe_slug: str,
data: dict,
session: Session = Depends(generate_session),
current_user=Depends(get_current_user),
):
""" Updates a recipe by existing slug and data. """
existing_entry: Recipe = db.recipes.get(session, recipe_slug)
entry_dict = existing_entry.dict()
entry_dict.update(data)
updated_entry = Recipe(**entry_dict) # ! Surely there's a better way?
recipe: Recipe = db.recipes.update(session, recipe_slug, updated_entry.dict())
if recipe_slug != recipe.slug:
rename_image(original_slug=recipe_slug, new_slug=recipe.slug)
return recipe
@router.delete("/{recipe_slug}") @router.delete("/{recipe_slug}")

View file

@ -19,6 +19,18 @@ async def get_all_recipe_tags(session: Session = Depends(generate_session)):
return db.tags.get_all_limit_columns(session, ["slug", "name"]) return db.tags.get_all_limit_columns(session, ["slug", "name"])
@router.get("/empty")
def get_empty_tags(session: Session = Depends(generate_session)):
""" Returns a list of tags that do not contain any recipes"""
return db.tags.get_empty(session)
@router.get("/{tag}", response_model=RecipeTagResponse)
def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)):
""" Returns a list of recipes associated with the provided tag. """
return db.tags.get(session, tag)
@router.post("") @router.post("")
async def create_recipe_tag( async def create_recipe_tag(
tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user) tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
@ -28,10 +40,13 @@ async def create_recipe_tag(
return db.tags.create(session, tag.dict()) return db.tags.create(session, tag.dict())
@router.get("/{tag}", response_model=RecipeTagResponse) @router.put("/{tag}", response_model=RecipeTagResponse)
def get_all_recipes_by_tag(tag: str, session: Session = Depends(generate_session)): async def update_recipe_tag(
""" Returns a list of recipes associated with the provided tag. """ tag: str, new_tag: TagIn, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
return db.tags.get(session, tag) ):
""" Updates an existing Tag in the database """
return db.tags.update(session, tag, new_tag.dict())
@router.delete("/{tag}") @router.delete("/{tag}")

View file

@ -2,6 +2,7 @@ from typing import List, Optional
from fastapi_camelcase import CamelModel from fastapi_camelcase import CamelModel
from mealie.schema.recipe import Recipe from mealie.schema.recipe import Recipe
from pydantic.utils import GetterDict
class CategoryIn(CamelModel): class CategoryIn(CamelModel):
@ -15,6 +16,13 @@ class CategoryBase(CategoryIn):
class Config: class Config:
orm_mode = True orm_mode = True
@classmethod
def getter_dict(_cls, name_orm):
return {
**GetterDict(name_orm),
"total_recipes": len(name_orm.recipes),
}
class RecipeCategoryResponse(CategoryBase): class RecipeCategoryResponse(CategoryBase):
recipes: Optional[List[Recipe]] recipes: Optional[List[Recipe]]

View file

@ -35,6 +35,7 @@ class Nutrition(BaseModel):
class RecipeSummary(BaseModel): class RecipeSummary(BaseModel):
id: Optional[int]
name: str name: str
slug: Optional[str] = "" slug: Optional[str] = ""
image: Optional[Any] image: Optional[Any]

View file

@ -8,18 +8,19 @@ from tests.app_routes import AppRoutes
@pytest.fixture @pytest.fixture
def backup_data(): def backup_data():
return { return {
"name": "dev_sample_data_2021-Feb-13.zip", "name": "test_backup_2021-Apr-27.zip",
"force": False, "force": True,
"recipes": True, "recipes": True,
"settings": False, # ! Broken "settings": False, # ! Broken
"themes": True, "themes": True,
"groups": True, "groups": True,
"users": True, "users": True,
"pages": True,
} }
def test_import(api_client: TestClient, api_routes: AppRoutes, backup_data, token): def test_import(api_client: TestClient, api_routes: AppRoutes, backup_data, token):
import_route = api_routes.backups_file_name_import("dev_sample_data_2021-Feb-13.zip") import_route = api_routes.backups_file_name_import("test_backup_2021-Apr-27.zip")
response = api_client.post(import_route, json=backup_data, headers=token) response = api_client.post(import_route, json=backup_data, headers=token)
assert response.status_code == 200 assert response.status_code == 200
for _, value in json.loads(response.content).items(): for _, value in json.loads(response.content).items():

View file

@ -60,7 +60,7 @@ def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data,
response = api_client.put(recipe_url, json=recipe, headers=token) response = api_client.put(recipe_url, json=recipe, headers=token)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.text) == recipe_data.expected_slug assert json.loads(response.text).get("slug") == recipe_data.expected_slug
response = api_client.get(recipe_url) response = api_client.get(recipe_url)
@ -84,7 +84,7 @@ def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data, toke
response = api_client.put(recipe_url, json=recipe, headers=token) response = api_client.put(recipe_url, json=recipe, headers=token)
assert response.status_code == 200 assert response.status_code == 200
assert json.loads(response.text) == new_slug assert json.loads(response.text).get("slug") == new_slug
recipe_data.expected_slug = new_slug recipe_data.expected_slug = new_slug