Refactor/user database models (#775)
* fix build error
* drop frontend.old
* improve auto_init decorator
* purge depreciated site settings
* formatting
* update init function
* fix(backend): 🐛 Fix password reset bug
Co-authored-by: Hayden <hay-kot@pm.me>
|
@ -1,2 +0,0 @@
|
|||
VUE_APP_API_BASE_URL=http://localhost:9000
|
||||
PREVIEW_BUNDLE=true
|
|
@ -1,24 +0,0 @@
|
|||
# frontend
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
presets: ["@vue/cli-plugin-babel/preset"],
|
||||
};
|
|
@ -1,173 +0,0 @@
|
|||
<template>
|
||||
<v-row>
|
||||
<SearchDialog ref="mealselect" @selected="setSlug" />
|
||||
<BaseDialog
|
||||
ref="customMealDialog"
|
||||
title="Custom Meal"
|
||||
:title-icon="$globals.icons.primary"
|
||||
:submit-text="$t('general.save')"
|
||||
:top="true"
|
||||
@submit="pushCustomMeal"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="customMeal.name" autofocus :label="$t('general.name')"> </v-text-field>
|
||||
<v-textarea v-model="customMeal.description" :label="$t('recipe.description')"> </v-textarea>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<v-col v-for="(planDay, index) in value" :key="index" cols="12" sm="12" md="6" lg="4" xl="3">
|
||||
<v-hover v-slot="{ hover }" :open-delay="50">
|
||||
<v-card :class="{ 'on-hover': hover }" :elevation="hover ? 12 : 2">
|
||||
<CardImage large :slug="planDay.meals[0].slug" icon-size="200" @click="openSearch(index, modes.primary)">
|
||||
<div>
|
||||
<v-fade-transition>
|
||||
<v-btn v-if="hover" small color="info" class="ma-1" @click.stop="addCustomItem(index, modes.primary)">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
{{ $t("reicpe.no-recipe") }}
|
||||
</v-btn>
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
</CardImage>
|
||||
|
||||
<v-card-title class="my-n3 mb-n6">
|
||||
{{ $d(new Date(planDay.date.replaceAll("-", "/")), "short") }}
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="mb-0 pb-0"> {{ planDay.meals[0].name }}</v-card-subtitle>
|
||||
<v-hover v-slot="{ hover }">
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-fade-transition>
|
||||
<v-btn v-if="hover" small color="info" text @click.stop="addCustomItem(index, modes.sides)">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
{{ $t("reicpe.no-recipe") }}
|
||||
</v-btn>
|
||||
</v-fade-transition>
|
||||
<v-btn color="info" outlined small @click="openSearch(index, modes.sides)">
|
||||
<v-icon small class="mr-1">
|
||||
{{ $globals.icons.create }}
|
||||
</v-icon>
|
||||
{{ $t("meal-plan.side") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-hover>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-list dense>
|
||||
<v-list-item v-for="(recipe, i) in planDay.meals.slice(1)" :key="i">
|
||||
<v-list-item-avatar color="accent">
|
||||
<v-img :alt="recipe.slug" :src="getImage(recipe.slug)"></v-img>
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="recipe.name"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-icon>
|
||||
<v-btn icon @click="removeSide(index, i + 1)">
|
||||
<v-icon color="error">
|
||||
{{ $globals.icons.delete }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-icon>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||
import { api } from "@/api";
|
||||
import SearchDialog from "../UI/Dialogs/SearchDialog";
|
||||
import CardImage from "../Recipe/CardImage.vue";
|
||||
export default {
|
||||
components: {
|
||||
SearchDialog,
|
||||
CardImage,
|
||||
BaseDialog,
|
||||
},
|
||||
props: {
|
||||
value: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeIndex: 0,
|
||||
mode: "PRIMARY",
|
||||
modes: {
|
||||
primary: "PRIMARY",
|
||||
sides: "SIDES",
|
||||
},
|
||||
customMeal: {
|
||||
slug: null,
|
||||
name: "",
|
||||
description: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
getImage(slug) {
|
||||
if (slug) {
|
||||
return api.recipes.recipeSmallImage(slug);
|
||||
}
|
||||
},
|
||||
setSide(name, slug = null, description = "") {
|
||||
const meal = { name, slug, description };
|
||||
this.value[this.activeIndex].meals.push(meal);
|
||||
},
|
||||
setPrimary(name, slug, description = "") {
|
||||
this.value[this.activeIndex].meals[0].slug = slug;
|
||||
this.value[this.activeIndex].meals[0].name = name;
|
||||
this.value[this.activeIndex].meals[0].description = description;
|
||||
},
|
||||
setSlug(recipe) {
|
||||
switch (this.mode) {
|
||||
case this.modes.primary:
|
||||
this.setPrimary(recipe.name, recipe.slug);
|
||||
break;
|
||||
default:
|
||||
this.setSide(recipe.name, recipe.slug);
|
||||
break;
|
||||
}
|
||||
},
|
||||
openSearch(index, mode) {
|
||||
this.mode = mode;
|
||||
this.activeIndex = index;
|
||||
this.$refs.mealselect.open();
|
||||
},
|
||||
removeSide(dayIndex, sideIndex) {
|
||||
this.value[dayIndex].meals.splice(sideIndex, 1);
|
||||
},
|
||||
addCustomItem(index, mode) {
|
||||
this.mode = mode;
|
||||
this.activeIndex = index;
|
||||
this.$refs.customMealDialog.open();
|
||||
},
|
||||
pushCustomMeal() {
|
||||
switch (this.mode) {
|
||||
case this.modes.primary:
|
||||
this.setPrimary(this.customMeal.name, this.customMeal.slug, this.customMeal.description);
|
||||
break;
|
||||
default:
|
||||
this.setSide(this.customMeal.name, this.customMeal.slug, this.customMeal.description);
|
||||
break;
|
||||
}
|
||||
this.customMeal = { name: "", slug: null, description: "" };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.relative-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.custom-button {
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
|
@ -1,46 +0,0 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
{{ $t("meal-plan.edit-meal-plan") }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<MealPlanCard v-model="mealPlan.planDays" />
|
||||
<v-row align="center" justify="end">
|
||||
<v-card-actions>
|
||||
<TheButton update @click="update" />
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import { utils } from "@/utils";
|
||||
import MealPlanCard from "./MealPlanCard";
|
||||
export default {
|
||||
components: {
|
||||
MealPlanCard,
|
||||
},
|
||||
props: {
|
||||
mealPlan: Object,
|
||||
},
|
||||
|
||||
methods: {
|
||||
formatDate(timestamp) {
|
||||
const dateObject = new Date(timestamp);
|
||||
return utils.getDateAsPythonDate(dateObject);
|
||||
},
|
||||
async update() {
|
||||
if (await api.mealPlans.update(this.mealPlan.uid, this.mealPlan)) {
|
||||
this.$emit("updated");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,227 +0,0 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class=" headline">
|
||||
{{ $t("meal-plan.create-a-new-meal-plan") }}
|
||||
<v-btn color="info" class="ml-auto" @click="setQuickWeek()">
|
||||
<v-icon left> {{ $globals.icons.calendarMinus }} </v-icon>
|
||||
{{ $t("meal-plan.quick-week") }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<v-col cols="12" lg="6" md="6" sm="12">
|
||||
<v-menu
|
||||
ref="menu1"
|
||||
v-model="menu1"
|
||||
:close-on-content-click="true"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
max-width="290px"
|
||||
min-width="290px"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="startComputedDateFormatted"
|
||||
:label="$t('meal-plan.start-date')"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendarMinus"
|
||||
readonly
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<DatePicker v-model="startDate" no-title @input="menu2 = false" />
|
||||
</v-menu>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="6" md="6" sm="12">
|
||||
<v-menu
|
||||
ref="menu2"
|
||||
v-model="menu2"
|
||||
:close-on-content-click="true"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
max-width="290px"
|
||||
min-width="290px"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="endComputedDateFormatted"
|
||||
:label="$t('meal-plan.end-date')"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendarMinus"
|
||||
readonly
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<DatePicker v-model="endDate" no-title @input="menu2 = false" />
|
||||
</v-menu>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-if="startDate">
|
||||
<MealPlanCard v-model="planDays" />
|
||||
</v-card-text>
|
||||
<v-row align="center" justify="end">
|
||||
<v-card-actions class="mr-5">
|
||||
<TheButton v-if="planDays.length > 0" edit text @click="random">
|
||||
<template #icon>
|
||||
{{ $globals.icons.diceMultiple }}
|
||||
</template>
|
||||
{{ $t("general.random") }}
|
||||
</TheButton>
|
||||
<TheButton create :disabled="planDays.length == 0" @click="save" />
|
||||
</v-card-actions>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DatePicker from "@/components/FormHelpers/DatePicker";
|
||||
import { api } from "@/api";
|
||||
import { utils } from "@/utils";
|
||||
import MealPlanCard from "./MealPlanCard";
|
||||
const CREATE_EVENT = "created";
|
||||
export default {
|
||||
components: {
|
||||
MealPlanCard,
|
||||
DatePicker,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
planDays: [],
|
||||
items: [],
|
||||
|
||||
// Dates
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
menu1: false,
|
||||
menu2: false,
|
||||
usedRecipes: [1],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
groupSettings() {
|
||||
return this.$store.getters.getCurrentGroup;
|
||||
},
|
||||
actualStartDate() {
|
||||
if (!this.startDate) return null;
|
||||
return Date.parse(this.startDate.replaceAll("-", "/"));
|
||||
},
|
||||
actualEndDate() {
|
||||
if (!this.endDate) return null;
|
||||
return Date.parse(this.endDate.replaceAll("-", "/"));
|
||||
},
|
||||
dateDif() {
|
||||
if (!this.actualEndDate || !this.actualStartDate) return null;
|
||||
const dateDif = (this.actualEndDate - this.actualStartDate) / (1000 * 3600 * 24) + 1;
|
||||
if (dateDif < 1) {
|
||||
return null;
|
||||
}
|
||||
return dateDif;
|
||||
},
|
||||
startComputedDateFormatted() {
|
||||
return this.formatDate(this.actualStartDate);
|
||||
},
|
||||
endComputedDateFormatted() {
|
||||
return this.formatDate(this.actualEndDate);
|
||||
},
|
||||
filteredRecipes() {
|
||||
const recipes = this.items.filter(x => !this.usedRecipes.includes(x));
|
||||
return recipes.length > 0 ? recipes : this.items;
|
||||
},
|
||||
allRecipes() {
|
||||
return this.$store.getters.getAllRecipes;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dateDif() {
|
||||
this.planDays = [];
|
||||
for (let i = 0; i < this.dateDif; i++) {
|
||||
this.planDays.push({
|
||||
date: this.getDate(i),
|
||||
meals: [
|
||||
{
|
||||
name: "",
|
||||
slug: "empty",
|
||||
description: "empty",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.$store.dispatch("requestCurrentGroup");
|
||||
await this.$store.dispatch("requestAllRecipes");
|
||||
await this.buildMealStore();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async buildMealStore() {
|
||||
const categories = Array.from(this.groupSettings.categories, x => x.name);
|
||||
this.items = await api.recipes.getAllByCategory(categories);
|
||||
|
||||
if (this.items.length === 0) {
|
||||
this.items = this.allRecipes;
|
||||
}
|
||||
},
|
||||
getRandom(list) {
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
},
|
||||
random() {
|
||||
this.usedRecipes = [1];
|
||||
this.planDays.forEach((_, index) => {
|
||||
const recipe = this.getRandom(this.filteredRecipes);
|
||||
this.planDays[index].meals[0].slug = recipe.slug;
|
||||
this.planDays[index].meals[0].name = recipe.name;
|
||||
this.usedRecipes.push(recipe);
|
||||
});
|
||||
},
|
||||
getDate(index) {
|
||||
const dateObj = new Date(this.actualStartDate.valueOf() + 1000 * 3600 * 24 * index);
|
||||
return utils.getDateAsPythonDate(dateObj);
|
||||
},
|
||||
async save() {
|
||||
const mealBody = {
|
||||
group: this.groupSettings.name,
|
||||
startDate: this.startDate,
|
||||
endDate: this.endDate,
|
||||
planDays: this.planDays,
|
||||
};
|
||||
if (await api.mealPlans.create(mealBody)) {
|
||||
this.$emit(CREATE_EVENT);
|
||||
this.planDays = [];
|
||||
this.startDate = null;
|
||||
this.endDate = null;
|
||||
}
|
||||
},
|
||||
formatDate(date) {
|
||||
if (!date) return null;
|
||||
|
||||
return this.$d(date);
|
||||
},
|
||||
getNextDayOfTheWeek(dayName, excludeToday = true, refDate = new Date()) {
|
||||
const dayOfWeek = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"].indexOf(dayName.slice(0, 3).toLowerCase());
|
||||
if (dayOfWeek < 0) return;
|
||||
refDate.setUTCHours(0, 0, 0, 0);
|
||||
refDate.setDate(refDate.getDate() + +!!excludeToday + ((dayOfWeek + 7 - refDate.getDay() - +!!excludeToday) % 7));
|
||||
return refDate;
|
||||
},
|
||||
setQuickWeek() {
|
||||
const nextMonday = this.getNextDayOfTheWeek("Monday", false);
|
||||
const nextEndDate = new Date(nextMonday);
|
||||
nextEndDate.setDate(nextEndDate.getDate() + 4);
|
||||
|
||||
this.startDate = utils.getDateAsPythonDate(nextMonday);
|
||||
this.endDate = utils.getDateAsPythonDate(nextEndDate);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,61 +0,0 @@
|
|||
<template>
|
||||
<v-list-item two-line to="/admin/profile">
|
||||
<v-list-item-avatar color="accent" class="white--text">
|
||||
<v-img v-if="!noImage" :src="profileImage" />
|
||||
<div v-else>
|
||||
{{ initials }}
|
||||
</div>
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> {{ user.fullName }}</v-list-item-title>
|
||||
<v-list-item-subtitle> {{ user.admin ? $t("user.admin") : $t("user.user") }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { initials } from "@/mixins/initials";
|
||||
import axios from "axios";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
mixins: [initials],
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
noImage: false,
|
||||
profileImage: "",
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
async user() {
|
||||
this.setImage();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async setImage() {
|
||||
const userImageURL = api.users.userProfileImage(this.user.id);
|
||||
if (await this.imageExists(userImageURL)) {
|
||||
this.noImage = false;
|
||||
this.profileImage = userImageURL;
|
||||
} else {
|
||||
this.noImage = true;
|
||||
}
|
||||
},
|
||||
async imageExists(url) {
|
||||
const response = await axios.get(url).catch(() => {
|
||||
this.noImage = true;
|
||||
return { status: 404 };
|
||||
});
|
||||
return response.status !== 404;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -1,153 +0,0 @@
|
|||
<template>
|
||||
<v-form ref="form">
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<ImageUploadBtn class="my-1" :slug="value.slug" @upload="uploadImage" @refresh="$emit('upload')" />
|
||||
<SettingsMenu class="my-1 mx-1" :value="value.settings" @upload="uploadImage" />
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col>
|
||||
<v-text-field v-model="value.totalTime" :label="$t('recipe.total-time')"></v-text-field>
|
||||
</v-col>
|
||||
<v-col><v-text-field v-model="value.prepTime" :label="$t('recipe.prep-time')"></v-text-field></v-col>
|
||||
<v-col><v-text-field v-model="value.performTime" :label="$t('recipe.perform-time')"></v-text-field></v-col>
|
||||
</v-row>
|
||||
<v-text-field v-model="value.name" class="my-3" :label="$t('recipe.recipe-name')" :rules="[existsRule]">
|
||||
</v-text-field>
|
||||
<v-textarea v-model="value.description" auto-grow min-height="100" :label="$t('recipe.description')">
|
||||
</v-textarea>
|
||||
<div class="my-2"></div>
|
||||
<v-row dense disabled>
|
||||
<v-col sm="4">
|
||||
<v-text-field v-model="value.recipeYield" :label="$t('recipe.servings')" class="rounded-sm"> </v-text-field>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<Rating v-model="value.rating" :emit-only="true" />
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="4" lg="4">
|
||||
<Ingredients v-model="value.recipeIngredient" :edit="true" />
|
||||
<v-card class="mt-6">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("recipe.categories") }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<CategoryTagSelector
|
||||
v-model="value.recipeCategory"
|
||||
:return-object="false"
|
||||
:show-add="true"
|
||||
:show-label="false"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mt-2">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("tag.tags") }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<CategoryTagSelector
|
||||
v-model="value.tags"
|
||||
:return-object="false"
|
||||
:show-add="true"
|
||||
:tag-selector="true"
|
||||
:show-label="false"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<Nutrition v-model="value.nutrition" :edit="true" />
|
||||
<Assets v-model="value.assets" :edit="true" :slug="value.slug" />
|
||||
<ExtrasEditor :extras="value.extras" @save="saveExtras" />
|
||||
</v-col>
|
||||
|
||||
<v-divider class="my-divider" :vertical="true"></v-divider>
|
||||
|
||||
<v-col cols="12" sm="12" md="8" lg="8">
|
||||
<Instructions v-model="value.recipeInstructions" :edit="true" />
|
||||
<div class="d-flex row justify-end mt-2">
|
||||
<BulkAdd class="mr-2" @bulk-data="appendSteps" />
|
||||
<v-btn color="secondary" dark class="mr-4" @click="addStep">
|
||||
<v-icon>{{ $globals.icons.create }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<Notes v-model="value.notes" :edit="true" />
|
||||
|
||||
<v-text-field v-model="value.orgURL" class="mt-10" :label="$t('recipe.original-url')"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd";
|
||||
import ExtrasEditor from "@/components/Recipe/Parts/Helpers/ExtrasEditor";
|
||||
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
|
||||
import ImageUploadBtn from "@/components/Recipe/Parts/Helpers/ImageUploadBtn";
|
||||
import { validators } from "@/mixins/validators";
|
||||
import Nutrition from "@/components/Recipe/Parts/Nutrition";
|
||||
import Instructions from "@/components/Recipe/Parts/Instructions";
|
||||
import Ingredients from "@/components/Recipe/Parts/Ingredients";
|
||||
import Assets from "@/components/Recipe/Parts/Assets.vue";
|
||||
import Notes from "@/components/Recipe/Parts/Notes.vue";
|
||||
import SettingsMenu from "@/components/Recipe/Parts/Helpers/SettingsMenu.vue";
|
||||
import Rating from "@/components/Recipe/Parts/Rating";
|
||||
const UPLOAD_EVENT = "upload";
|
||||
export default {
|
||||
components: {
|
||||
BulkAdd,
|
||||
ExtrasEditor,
|
||||
CategoryTagSelector,
|
||||
Nutrition,
|
||||
ImageUploadBtn,
|
||||
Instructions,
|
||||
Ingredients,
|
||||
Assets,
|
||||
Notes,
|
||||
SettingsMenu,
|
||||
Rating,
|
||||
},
|
||||
mixins: [validators],
|
||||
props: {
|
||||
value: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fileObject: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
uploadImage(fileObject) {
|
||||
this.$emit(UPLOAD_EVENT, fileObject);
|
||||
},
|
||||
appendSteps(steps) {
|
||||
this.value.recipeInstructions.push(
|
||||
...steps.map(x => ({
|
||||
title: "",
|
||||
text: x,
|
||||
}))
|
||||
);
|
||||
},
|
||||
addStep() {
|
||||
this.value.recipeInstructions.push({ title: "", text: "" });
|
||||
},
|
||||
saveExtras(extras) {
|
||||
this.value.extras = extras;
|
||||
},
|
||||
validateRecipe() {
|
||||
return this.$refs.form.validate();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.disabled-card {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.my-divider {
|
||||
margin: 0 -1px;
|
||||
}
|
||||
</style>
|
|
@ -1,142 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card-title class="headline">
|
||||
{{ recipe.name }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<VueMarkdown :source="recipe.description"> </VueMarkdown>
|
||||
<v-row dense disabled>
|
||||
<v-col>
|
||||
<v-btn
|
||||
v-if="recipe.recipeYield"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ recipe.recipeYield }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<Rating :key="recipe.slug" :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="4" lg="4">
|
||||
<Ingredients :value="recipe.recipeIngredient" :edit="false" />
|
||||
<div v-if="medium">
|
||||
<v-card v-if="recipe.recipeCategory.length > 0" class="mt-2">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("recipe.categories") }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeChips :items="recipe.recipeCategory" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card v-if="recipe.tags.length > 0" class="mt-2">
|
||||
<v-card-title class="py-2">
|
||||
{{ $t("tag.tags") }}
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeChips :items="recipe.tags" :is-category="false" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<Nutrition v-if="recipe.settings.showNutrition" :value="recipe.nutrition" :edit="false" />
|
||||
<Assets v-if="recipe.settings.showAssets" :value="recipe.assets" :edit="false" :slug="recipe.slug" />
|
||||
</div>
|
||||
</v-col>
|
||||
<v-divider v-if="medium" class="my-divider" :vertical="true"></v-divider>
|
||||
|
||||
<v-col cols="12" sm="12" md="8" lg="8">
|
||||
<Instructions :value="recipe.recipeInstructions" :edit="false" />
|
||||
<Notes :value="recipe.notes" :edit="false" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div v-if="!medium">
|
||||
<RecipeChips :title="$t('recipe.categories')" :items="recipe.recipeCategory" />
|
||||
<RecipeChips :title="$t('tag.tags')" :items="recipe.tags" />
|
||||
<Nutrition v-if="recipe.settings.showNutrition" :value="recipe.nutrition" :edit="false" />
|
||||
<Assets v-if="recipe.settings.showAssets" :value="recipe.assets" :edit="false" :slug="recipe.slug" />
|
||||
</div>
|
||||
<v-row class="mt-2 mb-1">
|
||||
<v-col></v-col>
|
||||
<v-btn
|
||||
v-if="recipe.orgURL"
|
||||
dense
|
||||
small
|
||||
:hover="false"
|
||||
type="label"
|
||||
:ripple="false"
|
||||
elevation="0"
|
||||
:href="recipe.orgURL"
|
||||
color="secondary darken-1"
|
||||
target="_blank"
|
||||
class="rounded-sm mr-4"
|
||||
>
|
||||
{{ $t("recipe.original-url") }}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Nutrition from "@/components/Recipe/Parts/Nutrition";
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import { utils } from "@/utils";
|
||||
import Rating from "@/components/Recipe/Parts/Rating";
|
||||
import Notes from "@/components/Recipe/Parts/Notes";
|
||||
import Ingredients from "@/components/Recipe/Parts/Ingredients";
|
||||
import Instructions from "@/components/Recipe/Parts/Instructions.vue";
|
||||
import Assets from "../../../../frontend.old/src/components/Recipe/Parts/Assets.vue";
|
||||
import RecipeChips from "./RecipeChips";
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
RecipeChips,
|
||||
Notes,
|
||||
Ingredients,
|
||||
Nutrition,
|
||||
Instructions,
|
||||
Assets,
|
||||
Rating,
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
disabledSteps: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
medium() {
|
||||
return this.$vuetify.breakpoint.mdAndUp;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.static {
|
||||
pointer-events: none;
|
||||
}
|
||||
.my-divider {
|
||||
margin: 0 -1px;
|
||||
}
|
||||
</style>
|
|
@ -1,22 +0,0 @@
|
|||
FROM node:lts-alpine
|
||||
|
||||
# # install simple http server for serving static content
|
||||
# RUN npm install -g http-server
|
||||
|
||||
# make the 'app' folder the current working directory
|
||||
WORKDIR /app
|
||||
|
||||
# copy both 'package.json' and 'package-lock.json' (if available)
|
||||
COPY package*.json ./
|
||||
|
||||
# install project dependencies
|
||||
RUN npm install
|
||||
|
||||
# copy project files and folders to the current working directory (i.e. 'app' folder)
|
||||
# COPY . .
|
||||
|
||||
# build app for production with minification
|
||||
# RUN npm run build
|
||||
|
||||
EXPOSE 8080
|
||||
CMD [ "npm", "run", "serve" ]
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adapttive/vue-markdown": "^4.0.1",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.14.0",
|
||||
"fuse.js": "^6.4.6",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"v-jsoneditor": "^1.4.4",
|
||||
"vue": "^2.6.14",
|
||||
"vue-i18n": "^8.24.1",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuetify": "^2.5.3",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^4.0.0-beta.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/vue-i18n-loader": "^1.1.0",
|
||||
"@mdi/font": "^5.9.55",
|
||||
"@mdi/js": "^5.9.55",
|
||||
"@vue/cli-plugin-babel": "^4.5.13",
|
||||
"@vue/cli-plugin-eslint": "^4.5.13",
|
||||
"@vue/cli-plugin-pwa": "~4.5.0",
|
||||
"@vue/cli-service": "^4.5.13",
|
||||
"@vue/preload-webpack-plugin": "^2.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"preload-webpack-plugin": "^2.3.0",
|
||||
"sass": "^1.34.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"vue-cli-plugin-i18n": "~1.0.1",
|
||||
"vue-cli-plugin-vuetify": "^2.4.1",
|
||||
"vue-cli-plugin-webpack-bundle-analyzer": "^4.0.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vuetify-loader": "^1.7.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"printWidth": 120
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 574 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 10 KiB |
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" version="1.1">
|
||||
<path d="M 162.083 54.642 C 148.745 68.272, 137.170 80.703, 136.362 82.266 C 133.689 87.435, 133.522 94.130, 135.929 99.573 C 137.122 102.269, 139.070 105.510, 140.258 106.775 L 142.418 109.074 90.974 160.526 L 39.529 211.979 46.999 219.499 L 54.470 227.020 91.235 190.265 L 128 153.510 164.765 190.265 L 201.530 227.020 209 219.500 L 216.470 211.980 179.725 175.225 L 142.980 138.470 150.320 131.178 C 156.858 124.685, 157.808 124.063, 159.001 125.501 C 162.066 129.195, 168.873 132.163, 174.392 132.213 C 183.508 132.295, 186.374 130.174, 212.477 104.038 L 236.454 80.030 231.501 75.001 L 226.548 69.973 209.288 87.212 L 192.027 104.452 187 99.500 L 181.973 94.548 199.212 77.288 L 216.452 60.027 211.500 55 L 206.548 49.973 189.288 67.212 L 172.027 84.452 167 79.500 L 161.973 74.548 179.225 57.275 L 196.477 40.001 191.406 34.930 L 186.335 29.859 162.083 54.642 M 38.429 41.250 C 31.557 49.376, 28.011 62.815, 29.835 73.824 C 31.955 86.615, 34.508 90.093, 61.720 117.253 L 86.520 142.005 101.501 126.999 L 116.482 111.993 79.496 74.996 C 59.154 54.648, 42.210 38, 41.844 38 C 41.478 38, 39.941 39.462, 38.429 41.250" stroke="none" fill="black" fill-rule="evenodd"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1,22 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="Mealie is a self hosted recipe manager and meal planner.">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title> Mealie </title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
||||
Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,138 +0,0 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<!-- Dummpy Comment -->
|
||||
<TheAppBar />
|
||||
<v-main>
|
||||
<v-banner v-if="demo" sticky>
|
||||
<div class="text-center">
|
||||
<b> This is a Demo of the v0.5.0 (BETA) </b> | Username: changeme@email.com | Password: demo
|
||||
</div>
|
||||
</v-banner>
|
||||
<GlobalSnackbar />
|
||||
<v-snackbar v-model="snackWithButtons" bottom left timeout="-1">
|
||||
{{ snackWithBtnText }}
|
||||
<template v-slot:action="{ attrs }">
|
||||
<v-btn text color="primary" v-bind="attrs" @click.stop="refreshApp">
|
||||
{{ snackBtnText }}
|
||||
</v-btn>
|
||||
<v-btn icon class="ml-4" @click="snackWithButtons = false">
|
||||
<v-icon>{{ $globals.icons.close }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TheAppBar from "@/components/UI/TheAppBar";
|
||||
import GlobalSnackbar from "@/components/UI/GlobalSnackbar";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
import { user } from "@/mixins/user";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
|
||||
components: {
|
||||
TheAppBar,
|
||||
GlobalSnackbar,
|
||||
},
|
||||
|
||||
mixins: [user],
|
||||
|
||||
computed: {
|
||||
demo() {
|
||||
const appInfo = this.$store.getters.getAppInfo;
|
||||
return appInfo.demoStatus;
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
// Initial API Requests
|
||||
this.$store.dispatch("initTheme");
|
||||
this.$store.dispatch("refreshToken");
|
||||
this.$store.dispatch("requestUserData");
|
||||
this.$store.dispatch("requestCurrentGroup");
|
||||
this.$store.dispatch("requestTags");
|
||||
this.$store.dispatch("requestAppInfo");
|
||||
this.$store.dispatch("requestSiteSettings");
|
||||
|
||||
// Listen for swUpdated event and display refresh snackbar as required.
|
||||
document.addEventListener("swUpdated", this.showRefreshUI, { once: true });
|
||||
// Refresh all open app tabs when a new service worker is installed.
|
||||
if (navigator.serviceWorker) {
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
if (this.refreshing) return;
|
||||
this.refreshing = true;
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.darkModeSystemCheck();
|
||||
this.darkModeAddEventListener();
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
refreshing: false,
|
||||
registration: null,
|
||||
snackBtnText: "",
|
||||
snackWithBtnText: "",
|
||||
snackWithButtons: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
// For Later!
|
||||
|
||||
/**
|
||||
* Checks if 'system' is set for dark mode and then sets the corrisponding value for vuetify
|
||||
*/
|
||||
darkModeSystemCheck() {
|
||||
if (this.$store.getters.getDarkMode === "system")
|
||||
Vuetify.framework.theme.dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
},
|
||||
/**
|
||||
* This will monitor the OS level darkmode and call to update dark mode.
|
||||
*/
|
||||
darkModeAddEventListener() {
|
||||
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
darkMediaQuery.addEventListener("change", () => {
|
||||
this.darkModeSystemCheck();
|
||||
});
|
||||
},
|
||||
|
||||
showRefreshUI(e) {
|
||||
// Display a snackbar inviting the user to refresh/reload the app due
|
||||
// to an app update being available.
|
||||
// The new service worker is installed, but not yet active.
|
||||
// Store the ServiceWorkerRegistration instance for later use.
|
||||
this.registration = e.detail;
|
||||
this.snackBtnText = this.$t("events.refresh");
|
||||
this.snackWithBtnText = this.$t("events.new-version");
|
||||
this.snackWithButtons = true;
|
||||
},
|
||||
refreshApp() {
|
||||
this.snackWithButtons = false;
|
||||
// Protect against missing registration.waiting.
|
||||
if (!this.registration || !this.registration.waiting) {
|
||||
return;
|
||||
}
|
||||
this.registration.waiting.postMessage("skipWaiting");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.top-dialog {
|
||||
align-self: flex-start;
|
||||
}
|
||||
:root {
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import i18n from "@/i18n.js";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const aboutAPI = {
|
||||
async getEvents() {
|
||||
const resposne = await apiReq.get(API_ROUTES.aboutEvents);
|
||||
return resposne.data;
|
||||
},
|
||||
async deleteEvent(id) {
|
||||
const resposne = await apiReq.delete(API_ROUTES.aboutEventsId(id));
|
||||
return resposne.data;
|
||||
},
|
||||
async deleteAllEvents() {
|
||||
const resposne = await apiReq.delete(API_ROUTES.aboutEvents);
|
||||
return resposne.data;
|
||||
},
|
||||
|
||||
async allEventNotifications() {
|
||||
const response = await apiReq.get(API_ROUTES.aboutEventsNotifications);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createNotification(data) {
|
||||
const response = await apiReq.post(API_ROUTES.aboutEventsNotifications, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteNotification(id) {
|
||||
const response = await apiReq.delete(API_ROUTES.aboutEventsNotificationsId(id));
|
||||
return response.data;
|
||||
},
|
||||
async testNotificationByID(id) {
|
||||
const response = await apiReq.post(
|
||||
API_ROUTES.aboutEventsNotificationsTest,
|
||||
{ id: id },
|
||||
() => i18n.t("events.something-went-wrong"),
|
||||
() => i18n.t("events.test-message-sent")
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
async testNotificationByURL(url) {
|
||||
const response = await apiReq.post(
|
||||
API_ROUTES.aboutEventsNotificationsTest,
|
||||
{ test_url: url },
|
||||
() => i18n.t("events.something-went-wrong"),
|
||||
() => i18n.t("events.test-message-sent")
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
// async getAppInfo() {
|
||||
// const response = await apiReq.get(aboutURLs.version);
|
||||
// return response.data;
|
||||
// },
|
||||
|
||||
// async getDebugInfo() {
|
||||
// const response = await apiReq.get(aboutURLs.debug);
|
||||
// return response.data;
|
||||
// },
|
||||
|
||||
// async getLogText(num) {
|
||||
// const response = await apiReq.get(aboutURLs.log(num));
|
||||
// return response.data;
|
||||
// },
|
||||
|
||||
// async getLastJson() {
|
||||
// const response = await apiReq.get(aboutURLs.lastRecipe);
|
||||
// return response.data;
|
||||
// },
|
||||
|
||||
// async getIsDemo() {
|
||||
// const response = await apiReq.get(aboutURLs.demo);
|
||||
// return response.data;
|
||||
// },
|
||||
|
||||
// async getStatistics() {
|
||||
// const response = await apiReq.get(aboutURLs.statistics);
|
||||
// return response.data;
|
||||
// },
|
||||
};
|
|
@ -1,121 +0,0 @@
|
|||
import { prefix } from "./apiRoutes";
|
||||
import axios from "axios";
|
||||
import { store } from "../store";
|
||||
import { utils } from "@/utils";
|
||||
|
||||
axios.defaults.headers.common["Authorization"] = `Bearer ${store.getters.getToken}`;
|
||||
|
||||
function handleError(error, getText) {
|
||||
if (getText) {
|
||||
utils.notify.error(getText(error.response));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function handleResponse(response, getText) {
|
||||
if (response && getText) {
|
||||
const successText = getText(response);
|
||||
utils.notify.success(successText);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
function defaultErrorText(response) {
|
||||
return response.statusText;
|
||||
}
|
||||
|
||||
function defaultSuccessText(response) {
|
||||
return response.statusText;
|
||||
}
|
||||
|
||||
const requests = {
|
||||
/**
|
||||
*
|
||||
* @param {*} funcCall Callable Axios Function
|
||||
* @param {*} url Destination url
|
||||
* @param {*} data Request Data
|
||||
* @param {*} getErrorText Error Text Function
|
||||
* @param {*} getSuccessText Success Text Function
|
||||
* @returns Object response
|
||||
*/
|
||||
unsafe: async function(funcCall, url, data, getErrorText = defaultErrorText, getSuccessText) {
|
||||
const response = await funcCall(url, data).catch(function(error) {
|
||||
handleError(error, getErrorText);
|
||||
});
|
||||
return handleResponse(response, getSuccessText);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} funcCall Callable Axios Function
|
||||
* @param {*} url Destination url
|
||||
* @param {*} data Request Data
|
||||
* @param {*} getErrorText Error Text Function
|
||||
* @param {*} getSuccessText Success Text Function
|
||||
* @returns Array [response, error]
|
||||
*/
|
||||
safe: async function(funcCall, url, data, getErrorText = defaultErrorText, getSuccessText) {
|
||||
const response = await funcCall(url, data).catch(function(error) {
|
||||
handleError(error, getErrorText);
|
||||
return [null, error];
|
||||
});
|
||||
return [handleResponse(response, getSuccessText), null];
|
||||
},
|
||||
};
|
||||
|
||||
const apiReq = {
|
||||
get: async function(url, getErrorText = defaultErrorText) {
|
||||
return axios.get(url).catch(function(error) {
|
||||
handleError(error, getErrorText);
|
||||
});
|
||||
},
|
||||
|
||||
getSafe: async function(url) {
|
||||
let error = null;
|
||||
const response = await axios.get(url).catch(e => {
|
||||
error = e;
|
||||
});
|
||||
return [response, error];
|
||||
},
|
||||
|
||||
post: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
|
||||
return await requests.unsafe(axios.post, url, data, getErrorText, getSuccessText);
|
||||
},
|
||||
|
||||
postSafe: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
|
||||
return await requests.safe(axios.post, url, data, getErrorText, getSuccessText);
|
||||
},
|
||||
|
||||
put: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
|
||||
return await requests.unsafe(axios.put, url, data, getErrorText, getSuccessText);
|
||||
},
|
||||
|
||||
putSafe: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
|
||||
return await requests.safe(axios.put, url, data, getErrorText, getSuccessText);
|
||||
},
|
||||
|
||||
patch: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
|
||||
return await requests.unsafe(axios.patch, url, data, getErrorText, getSuccessText);
|
||||
},
|
||||
|
||||
patchSafe: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
|
||||
return await requests.safe(axios.patch, url, data, getErrorText, getSuccessText);
|
||||
},
|
||||
|
||||
delete: async function(url, data, getErrorText = defaultErrorText, getSuccessText = defaultSuccessText) {
|
||||
return await requests.unsafe(axios.delete, url, data, getErrorText, getSuccessText);
|
||||
},
|
||||
|
||||
deleteSafe: async function(url, data, getErrorText = defaultErrorText, getSuccessText = defaultSuccessText) {
|
||||
return await requests.unsafe(axios.delete, url, data, getErrorText, getSuccessText);
|
||||
},
|
||||
|
||||
download: async function(url) {
|
||||
const response = await this.get(url);
|
||||
const token = response.data.fileToken;
|
||||
|
||||
const tokenURL = prefix + "/utils/download?token=" + token;
|
||||
window.open(tokenURL, "_blank");
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export { apiReq };
|
|
@ -1,90 +0,0 @@
|
|||
// This Content is Auto Generated
|
||||
export const prefix = "/api";
|
||||
export const API_ROUTES = {
|
||||
aboutEvents: `${prefix}/about/events`,
|
||||
aboutEventsNotifications: `${prefix}/about/events/notifications`,
|
||||
aboutEventsNotificationsTest: `${prefix}/about/events/notifications/test`,
|
||||
aboutRecipesDefaults: `${prefix}/about/recipes/defaults`,
|
||||
authRefresh: `${prefix}/auth/refresh`,
|
||||
authToken: `${prefix}/auth/token`,
|
||||
authTokenLong: `${prefix}/auth/token/long`,
|
||||
backupsAvailable: `${prefix}/backups/available`,
|
||||
backupsExportDatabase: `${prefix}/backups/export/database`,
|
||||
backupsUpload: `${prefix}/backups/upload`,
|
||||
categories: `${prefix}/categories`,
|
||||
categoriesEmpty: `${prefix}/categories/empty`,
|
||||
debug: `${prefix}/debug`,
|
||||
debugLastRecipeJson: `${prefix}/debug/last-recipe-json`,
|
||||
debugLog: `${prefix}/debug/log`,
|
||||
debugStatistics: `${prefix}/debug/statistics`,
|
||||
debugVersion: `${prefix}/debug/version`,
|
||||
groups: `${prefix}/groups`,
|
||||
groupsSelf: `${prefix}/groups/self`,
|
||||
mealPlansAll: `${prefix}/meal-plans/all`,
|
||||
mealPlansCreate: `${prefix}/meal-plans/create`,
|
||||
mealPlansThisWeek: `${prefix}/meal-plans/this-week`,
|
||||
mealPlansToday: `${prefix}/meal-plans/today`,
|
||||
mealPlansTodayImage: `${prefix}/meal-plans/today/image`,
|
||||
migrations: `${prefix}/migrations`,
|
||||
recipesCategory: `${prefix}/recipes/category`,
|
||||
recipesCreate: `${prefix}/recipes/create`,
|
||||
recipesCreateFromZip: `${prefix}/recipes/create-from-zip`,
|
||||
recipesCreateUrl: `${prefix}/recipes/create-url`,
|
||||
recipesSummary: `${prefix}/recipes/summary`,
|
||||
recipesSummaryUncategorized: `${prefix}/recipes/summary/uncategorized`,
|
||||
recipesSummaryUntagged: `${prefix}/recipes/summary/untagged`,
|
||||
recipesTag: `${prefix}/recipes/tag`,
|
||||
recipesTestScrapeUrl: `${prefix}/recipes/test-scrape-url`,
|
||||
shoppingLists: `${prefix}/shopping-lists`,
|
||||
siteSettings: `${prefix}/site-settings`,
|
||||
siteSettingsCustomPages: `${prefix}/site-settings/custom-pages`,
|
||||
siteSettingsWebhooksTest: `${prefix}/site-settings/webhooks/test`,
|
||||
tags: `${prefix}/tags`,
|
||||
tagsEmpty: `${prefix}/tags/empty`,
|
||||
themes: `${prefix}/themes`,
|
||||
themesCreate: `${prefix}/themes/create`,
|
||||
users: `${prefix}/users`,
|
||||
usersApiTokens: `${prefix}/users/api-tokens`,
|
||||
usersSelf: `${prefix}/users/self`,
|
||||
usersSignUps: `${prefix}/users/sign-ups`,
|
||||
utilsDownload: `${prefix}/utils/download`,
|
||||
|
||||
aboutEventsId: id => `${prefix}/about/events/${id}`,
|
||||
aboutEventsNotificationsId: id => `${prefix}/about/events/notifications/${id}`,
|
||||
backupsFileNameDelete: file_name => `${prefix}/backups/${file_name}/delete`,
|
||||
backupsFileNameDownload: file_name => `${prefix}/backups/${file_name}/download`,
|
||||
backupsFileNameImport: file_name => `${prefix}/backups/${file_name}/import`,
|
||||
categoriesCategory: category => `${prefix}/categories/${category}`,
|
||||
debugLogNum: num => `${prefix}/debug/log/${num}`,
|
||||
groupsId: id => `${prefix}/groups/${id}`,
|
||||
mealPlansId: id => `${prefix}/meal-plans/${id}`,
|
||||
mealPlansIdShoppingList: id => `${prefix}/meal-plans/${id}/shopping-list`,
|
||||
mealPlansPlanId: plan_id => `${prefix}/meal-plans/${plan_id}`,
|
||||
mediaRecipesRecipeSlugAssetsFileName: (recipe_slug, file_name) =>
|
||||
`${prefix}/media/recipes/${recipe_slug}/assets/${file_name}`,
|
||||
mediaRecipesRecipeSlugImagesFileName: (recipe_slug, file_name) =>
|
||||
`${prefix}/media/recipes/${recipe_slug}/images/${file_name}`,
|
||||
migrationsImportTypeFileNameDelete: (import_type, file_name) =>
|
||||
`${prefix}/migrations/${import_type}/${file_name}/delete`,
|
||||
migrationsImportTypeFileNameImport: (import_type, file_name) =>
|
||||
`${prefix}/migrations/${import_type}/${file_name}/import`,
|
||||
migrationsImportTypeUpload: import_type => `${prefix}/migrations/${import_type}/upload`,
|
||||
recipesRecipeSlug: recipe_slug => `${prefix}/recipes/${recipe_slug}`,
|
||||
recipesRecipeSlugAssets: recipe_slug => `${prefix}/recipes/${recipe_slug}/assets`,
|
||||
recipesRecipeSlugImage: recipe_slug => `${prefix}/recipes/${recipe_slug}/image`,
|
||||
recipesRecipeSlugZip: recipe_slug => `${prefix}/recipes/${recipe_slug}/zip`,
|
||||
recipesSlugComments: slug => `${prefix}/recipes/${slug}/comments`,
|
||||
recipesSlugCommentsId: (slug, id) => `${prefix}/recipes/${slug}/comments/${id}`,
|
||||
shoppingListsId: id => `${prefix}/shopping-lists/${id}`,
|
||||
siteSettingsCustomPagesId: id => `${prefix}/site-settings/custom-pages/${id}`,
|
||||
tagsTag: tag => `${prefix}/tags/${tag}`,
|
||||
themesId: id => `${prefix}/themes/${id}`,
|
||||
usersApiTokensTokenId: token_id => `${prefix}/users/api-tokens/${token_id}`,
|
||||
usersId: id => `${prefix}/users/${id}`,
|
||||
usersIdFavorites: id => `${prefix}/users/${id}/favorites`,
|
||||
usersIdFavoritesSlug: (id, slug) => `${prefix}/users/${id}/favorites/${slug}`,
|
||||
usersIdImage: id => `${prefix}/users/${id}/image`,
|
||||
usersIdPassword: id => `${prefix}/users/${id}/password`,
|
||||
usersIdResetPassword: id => `${prefix}/users/${id}/reset-password`,
|
||||
usersSignUpsToken: token => `${prefix}/users/sign-ups/${token}`,
|
||||
};
|
|
@ -1,62 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import { store } from "@/store";
|
||||
import i18n from "@/i18n.js";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const backupAPI = {
|
||||
/**
|
||||
* Request all backups available on the server
|
||||
* @returns {Array} List of Available Backups
|
||||
*/
|
||||
async requestAvailable() {
|
||||
let response = await apiReq.get(API_ROUTES.backupsAvailable);
|
||||
return response.data;
|
||||
},
|
||||
/**
|
||||
* Calls for importing a file on the server
|
||||
* @param {string} fileName
|
||||
* @param {object} data
|
||||
* @returns A report containing status of imported items
|
||||
*/
|
||||
async import(fileName, data) {
|
||||
let response = await apiReq.post(API_ROUTES.backupsFileNameImport(fileName), data);
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response;
|
||||
},
|
||||
/**
|
||||
* Removes a file from the server
|
||||
* @param {string} fileName
|
||||
*/
|
||||
async delete(fileName) {
|
||||
return apiReq.delete(
|
||||
API_ROUTES.backupsFileNameDelete(fileName),
|
||||
null,
|
||||
() => i18n.t("settings.backup.unable-to-delete-backup"),
|
||||
() => i18n.t("settings.backup.backup-deleted")
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Creates a backup on the serve given a set of options
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
async create(options) {
|
||||
return apiReq.post(
|
||||
API_ROUTES.backupsExportDatabase,
|
||||
options,
|
||||
() => i18n.t("settings.backup.error-creating-backup-see-log-file"),
|
||||
response => {
|
||||
return i18n.t("settings.backup.backup-created-at-response-export_path", { path: response.data.export_path });
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Downloads a file from the server. I don't actually think this is used?
|
||||
* @param {string} fileName
|
||||
* @returns Download URL
|
||||
*/
|
||||
async download(fileName) {
|
||||
const url = API_ROUTES.backupsFileNameDownload(fileName);
|
||||
apiReq.download(url);
|
||||
},
|
||||
};
|
|
@ -1,111 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import { store } from "@/store";
|
||||
import i18n from "@/i18n.js";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const categoryAPI = {
|
||||
async getAll() {
|
||||
let response = await apiReq.get(API_ROUTES.categories);
|
||||
return response.data;
|
||||
},
|
||||
async getEmpty() {
|
||||
let response = await apiReq.get(API_ROUTES.categoriesEmpty);
|
||||
return response.data;
|
||||
},
|
||||
async create(name) {
|
||||
const response = await apiReq.post(
|
||||
API_ROUTES.categories,
|
||||
{ name: name },
|
||||
() => i18n.t("category.category-creation-failed"),
|
||||
() => i18n.t("category.category-created")
|
||||
);
|
||||
if (response) {
|
||||
store.dispatch("requestCategories");
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
async getRecipesInCategory(category) {
|
||||
let response = await apiReq.get(API_ROUTES.categoriesCategory(category));
|
||||
return response.data;
|
||||
},
|
||||
async update(name, newName, overrideRequest = false) {
|
||||
const response = await apiReq.put(
|
||||
API_ROUTES.categoriesCategory(name),
|
||||
{ name: newName },
|
||||
() => i18n.t("category.category-update-failed"),
|
||||
() => i18n.t("category.category-updated")
|
||||
);
|
||||
if (response && !overrideRequest) {
|
||||
store.dispatch("requestCategories");
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
async delete(category, overrideRequest = false) {
|
||||
const response = await apiReq.delete(
|
||||
API_ROUTES.categoriesCategory(category),
|
||||
null,
|
||||
() => i18n.t("category.category-deletion-failed"),
|
||||
() => i18n.t("category.category-deleted")
|
||||
);
|
||||
if (response && !overrideRequest) {
|
||||
store.dispatch("requestCategories");
|
||||
}
|
||||
return response;
|
||||
},
|
||||
};
|
||||
|
||||
export const tagAPI = {
|
||||
async getAll() {
|
||||
let response = await apiReq.get(API_ROUTES.tags);
|
||||
return response.data;
|
||||
},
|
||||
async getEmpty() {
|
||||
let response = await apiReq.get(API_ROUTES.tagsEmpty);
|
||||
return response.data;
|
||||
},
|
||||
async create(name) {
|
||||
const response = await apiReq.post(
|
||||
API_ROUTES.tags,
|
||||
{ name: name },
|
||||
() => i18n.t("tag.tag-creation-failed"),
|
||||
() => i18n.t("tag.tag-created")
|
||||
);
|
||||
if (response) {
|
||||
store.dispatch("requestTags");
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
async getRecipesInTag(tag) {
|
||||
let response = await apiReq.get(API_ROUTES.tagsTag(tag));
|
||||
return response.data;
|
||||
},
|
||||
async update(name, newName, overrideRequest = false) {
|
||||
const response = await apiReq.put(
|
||||
API_ROUTES.tagsTag(name),
|
||||
{ name: newName },
|
||||
() => i18n.t("tag.tag-update-failed"),
|
||||
() => i18n.t("tag.tag-updated")
|
||||
);
|
||||
|
||||
if (response) {
|
||||
if (!overrideRequest) {
|
||||
store.dispatch("requestTags");
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
async delete(tag, overrideRequest = false) {
|
||||
const response = await apiReq.delete(
|
||||
API_ROUTES.tagsTag(tag),
|
||||
null,
|
||||
() => i18n.t("tag.tag-deletion-failed"),
|
||||
() => i18n.t("tag.tag-deleted")
|
||||
);
|
||||
if (response) {
|
||||
if (!overrideRequest) {
|
||||
store.dispatch("requestTags");
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,53 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import i18n from "@/i18n.js";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
function deleteErrorText(response) {
|
||||
switch (response.data.detail) {
|
||||
case "GROUP_WITH_USERS":
|
||||
return i18n.t("group.cannot-delete-group-with-users");
|
||||
|
||||
case "GROUP_NOT_FOUND":
|
||||
return i18n.t("group.group-not-found");
|
||||
|
||||
case "DEFAULT_GROUP":
|
||||
return i18n.t("group.cannot-delete-default-group");
|
||||
|
||||
default:
|
||||
return i18n.t("group.group-deletion-failed");
|
||||
}
|
||||
}
|
||||
|
||||
export const groupAPI = {
|
||||
async allGroups() {
|
||||
let response = await apiReq.get(API_ROUTES.groups);
|
||||
return response.data;
|
||||
},
|
||||
create(name) {
|
||||
return apiReq.post(
|
||||
API_ROUTES.groups,
|
||||
{ name: name },
|
||||
() => i18n.t("group.user-group-creation-failed"),
|
||||
() => i18n.t("group.user-group-created")
|
||||
);
|
||||
},
|
||||
delete(id) {
|
||||
return apiReq.delete(API_ROUTES.groupsId(id), null, deleteErrorText, function() {
|
||||
return i18n.t("group.group-deleted");
|
||||
});
|
||||
},
|
||||
async current() {
|
||||
const response = await apiReq.get(API_ROUTES.groupsSelf, null, null);
|
||||
if (response) {
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
update(data) {
|
||||
return apiReq.put(
|
||||
API_ROUTES.groupsId(data.id),
|
||||
data,
|
||||
() => i18n.t("group.error-updating-group"),
|
||||
() => i18n.t("settings.group-settings-updated")
|
||||
);
|
||||
},
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
import { backupAPI } from "./backup";
|
||||
import { recipeAPI } from "./recipe";
|
||||
import { mealplanAPI } from "./mealplan";
|
||||
import { settingsAPI } from "./settings";
|
||||
import { themeAPI } from "./themes";
|
||||
import { migrationAPI } from "./migration";
|
||||
import { utilsAPI } from "./upload";
|
||||
import { categoryAPI, tagAPI } from "./category";
|
||||
import { metaAPI } from "./meta";
|
||||
import { userAPI } from "./users";
|
||||
import { signupAPI } from "./signUps";
|
||||
import { groupAPI } from "./groups";
|
||||
import { siteSettingsAPI } from "./siteSettings";
|
||||
import { aboutAPI } from "./about";
|
||||
import { shoppingListsAPI } from "./shoppingLists";
|
||||
|
||||
/**
|
||||
* The main object namespace for interacting with the backend database
|
||||
*/
|
||||
export const api = {
|
||||
recipes: recipeAPI,
|
||||
siteSettings: siteSettingsAPI,
|
||||
backups: backupAPI,
|
||||
mealPlans: mealplanAPI,
|
||||
settings: settingsAPI,
|
||||
themes: themeAPI,
|
||||
migrations: migrationAPI,
|
||||
utils: utilsAPI,
|
||||
categories: categoryAPI,
|
||||
tags: tagAPI,
|
||||
meta: metaAPI,
|
||||
users: userAPI,
|
||||
signUps: signupAPI,
|
||||
groups: groupAPI,
|
||||
about: aboutAPI,
|
||||
shoppingLists: shoppingListsAPI,
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import i18n from "@/i18n.js";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const mealplanAPI = {
|
||||
create(postBody) {
|
||||
return apiReq.post(
|
||||
API_ROUTES.mealPlansCreate,
|
||||
postBody,
|
||||
() => i18n.t("meal-plan.mealplan-creation-failed"),
|
||||
() => i18n.t("meal-plan.mealplan-created")
|
||||
);
|
||||
},
|
||||
|
||||
async all() {
|
||||
let response = await apiReq.get(API_ROUTES.mealPlansAll);
|
||||
return response;
|
||||
},
|
||||
|
||||
async thisWeek() {
|
||||
let response = await apiReq.get(API_ROUTES.mealPlansThisWeek);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async today() {
|
||||
let response = await apiReq.get(API_ROUTES.mealPlansToday);
|
||||
return response;
|
||||
},
|
||||
|
||||
async getById(id) {
|
||||
let response = await apiReq.get(API_ROUTES.mealPlansId(id));
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
return apiReq.delete(
|
||||
API_ROUTES.mealPlansId(id),
|
||||
null,
|
||||
() => i18n.t("meal-plan.mealplan-deletion-failed"),
|
||||
() => i18n.t("meal-plan.mealplan-deleted")
|
||||
);
|
||||
},
|
||||
|
||||
update(id, body) {
|
||||
return apiReq.put(
|
||||
API_ROUTES.mealPlansId(id),
|
||||
body,
|
||||
() => i18n.t("meal-plan.mealplan-update-failed"),
|
||||
() => i18n.t("meal-plan.mealplan-updated")
|
||||
);
|
||||
},
|
||||
|
||||
async shoppingList(id) {
|
||||
let response = await apiReq.get(API_ROUTES.mealPlansIdShoppingList(id));
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const metaAPI = {
|
||||
async getAppInfo() {
|
||||
const response = await apiReq.get(API_ROUTES.debugVersion);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getDebugInfo() {
|
||||
const response = await apiReq.get(API_ROUTES.debug);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getLogText(num) {
|
||||
const response = await apiReq.get(API_ROUTES.debugLogNum(num));
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getLastJson() {
|
||||
const response = await apiReq.get(API_ROUTES.debugLastRecipeJson);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getStatistics() {
|
||||
const response = await apiReq.get(API_ROUTES.debugStatistics);
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import { store } from "../store";
|
||||
import i18n from "@/i18n.js";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const migrationAPI = {
|
||||
async getMigrations() {
|
||||
let response = await apiReq.get(API_ROUTES.migrations);
|
||||
return response.data;
|
||||
},
|
||||
async delete(folder, file) {
|
||||
const response = await apiReq.delete(
|
||||
API_ROUTES.migrationsImportTypeFileNameDelete(folder, file),
|
||||
null,
|
||||
() => i18n.t("general.file-folder-not-found"),
|
||||
() => i18n.t("migration.migration-data-removed")
|
||||
);
|
||||
return response;
|
||||
},
|
||||
async import(folder, file) {
|
||||
let response = await apiReq.post(API_ROUTES.migrationsImportTypeFileNameImport(folder, file));
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -1,181 +0,0 @@
|
|||
import { API_ROUTES } from "./apiRoutes";
|
||||
import { apiReq } from "./api-utils";
|
||||
import { store } from "../store";
|
||||
import i18n from "@/i18n.js";
|
||||
|
||||
export const recipeAPI = {
|
||||
/**
|
||||
* Returns the Default Recipe Settings for the Site
|
||||
* @returns {AxoisResponse} Axois Response Object
|
||||
*/
|
||||
async getDefaultSettings() {
|
||||
const response = await apiReq.get(API_ROUTES.aboutRecipesDefaults);
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a Recipe by URL
|
||||
* @param {string} recipeURL
|
||||
* @returns {string} Recipe Slug
|
||||
*/
|
||||
async createByURL(recipeURL) {
|
||||
const response = await apiReq.post(API_ROUTES.recipesCreateUrl, { url: recipeURL }, false, () =>
|
||||
i18n.t("recipe.recipe-created")
|
||||
);
|
||||
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response;
|
||||
},
|
||||
|
||||
async getAllByCategory(categories) {
|
||||
let response = await apiReq.post(API_ROUTES.recipesCategory, categories);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async create(recipeData) {
|
||||
const response = await apiReq.post(
|
||||
API_ROUTES.recipesCreate,
|
||||
recipeData,
|
||||
() => i18n.t("recipe.recipe-creation-failed"),
|
||||
() => i18n.t("recipe.recipe-created")
|
||||
);
|
||||
store.dispatch("requestRecentRecipes");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async requestDetails(recipeSlug) {
|
||||
const response = await apiReq.getSafe(API_ROUTES.recipesRecipeSlug(recipeSlug));
|
||||
return response;
|
||||
},
|
||||
|
||||
updateImage(recipeSlug, fileObject, overrideSuccessMsg = false) {
|
||||
const formData = new FormData();
|
||||
formData.append("image", fileObject);
|
||||
formData.append("extension", fileObject.name.split(".").pop());
|
||||
|
||||
let successMessage = null;
|
||||
if (!overrideSuccessMsg) {
|
||||
successMessage = function() {
|
||||
return overrideSuccessMsg ? null : i18n.t("recipe.recipe-image-updated");
|
||||
};
|
||||
}
|
||||
|
||||
return apiReq.put(
|
||||
API_ROUTES.recipesRecipeSlugImage(recipeSlug),
|
||||
formData,
|
||||
() => i18n.t("general.image-upload-failed"),
|
||||
successMessage
|
||||
);
|
||||
},
|
||||
|
||||
async createAsset(recipeSlug, fileObject, name, icon) {
|
||||
const fd = new FormData();
|
||||
fd.append("file", fileObject);
|
||||
fd.append("extension", fileObject.name.split(".").pop());
|
||||
fd.append("name", name);
|
||||
fd.append("icon", icon);
|
||||
const response = apiReq.post(API_ROUTES.recipesRecipeSlugAssets(recipeSlug), fd);
|
||||
return response;
|
||||
},
|
||||
|
||||
updateImagebyURL(slug, url) {
|
||||
return apiReq.post(
|
||||
API_ROUTES.recipesRecipeSlugImage(slug),
|
||||
{ url: url },
|
||||
() => i18n.t("general.image-upload-failed"),
|
||||
() => i18n.t("recipe.recipe-image-updated")
|
||||
);
|
||||
},
|
||||
|
||||
async update(data) {
|
||||
let response = await apiReq.put(
|
||||
API_ROUTES.recipesRecipeSlug(data.slug),
|
||||
data,
|
||||
() => i18n.t("recipe.recipe-update-failed"),
|
||||
() => i18n.t("recipe.recipe-updated")
|
||||
);
|
||||
if (response) {
|
||||
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(API_ROUTES.recipesRecipeSlug(data.slug), data);
|
||||
store.dispatch("patchRecipe", response.data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async delete(recipeSlug) {
|
||||
const response = await apiReq.delete(
|
||||
API_ROUTES.recipesRecipeSlug(recipeSlug),
|
||||
null,
|
||||
() => i18n.t("recipe.unable-to-delete-recipe"),
|
||||
() => i18n.t("recipe.recipe-deleted")
|
||||
);
|
||||
store.dispatch("dropRecipe", response.data);
|
||||
return response;
|
||||
},
|
||||
|
||||
async allSummary(start = 0, limit = 9999) {
|
||||
const response = await apiReq.get(API_ROUTES.recipesSummary, {
|
||||
params: { start: start, limit: limit },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async allUntagged() {
|
||||
const response = await apiReq.get(API_ROUTES.recipesSummaryUntagged);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async allUnategorized() {
|
||||
const response = await apiReq.get(API_ROUTES.recipesSummaryUncategorized);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
recipeImage(recipeSlug, version = null, key = null) {
|
||||
return `/api/media/recipes/${recipeSlug}/images/original.webp?&rnd=${key}&version=${version}`;
|
||||
},
|
||||
|
||||
recipeSmallImage(recipeSlug, version = null, key = null) {
|
||||
return `/api/media/recipes/${recipeSlug}/images/min-original.webp?&rnd=${key}&version=${version}`;
|
||||
},
|
||||
|
||||
recipeTinyImage(recipeSlug, version = null, key = null) {
|
||||
return `/api/media/recipes/${recipeSlug}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
|
||||
},
|
||||
|
||||
recipeAssetPath(recipeSlug, assetName) {
|
||||
return `/api/media/recipes/${recipeSlug}/assets/${assetName}`;
|
||||
},
|
||||
|
||||
/** Create comment in the Database
|
||||
* @param slug
|
||||
*/
|
||||
async createComment(slug, data) {
|
||||
const response = await apiReq.post(API_ROUTES.recipesSlugComments(slug), data);
|
||||
return response.data;
|
||||
},
|
||||
/** Update comment in the Database
|
||||
* @param slug
|
||||
* @param id
|
||||
*/
|
||||
async updateComment(slug, id, data) {
|
||||
const response = await apiReq.put(API_ROUTES.recipesSlugCommentsId(slug, id), data);
|
||||
return response.data;
|
||||
},
|
||||
/** Delete comment from the Database
|
||||
* @param slug
|
||||
* @param id
|
||||
*/
|
||||
async deleteComment(slug, id) {
|
||||
const response = await apiReq.delete(API_ROUTES.recipesSlugCommentsId(slug, id));
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async testScrapeURL(url) {
|
||||
const response = await apiReq.post(API_ROUTES.recipesTestScrapeUrl, { url: url });
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const settingsAPI = {
|
||||
async requestAll() {
|
||||
let response = await apiReq.get(API_ROUTES.siteSettings);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async testWebhooks() {
|
||||
let response = await apiReq.post(API_ROUTES.siteSettingsWebhooksTest);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async update(body) {
|
||||
let response = await apiReq.put(API_ROUTES.siteSettings, body);
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
// This Content is Auto Generated
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
import { apiReq } from "./api-utils";
|
||||
|
||||
export const shoppingListsAPI = {
|
||||
/** Create Shopping List in the Database
|
||||
*/
|
||||
async createShoppingList(data) {
|
||||
const response = await apiReq.post(API_ROUTES.shoppingLists, data);
|
||||
return response.data;
|
||||
},
|
||||
/** Get Shopping List from the Database
|
||||
* @param id
|
||||
*/
|
||||
async getShoppingList(id) {
|
||||
const response = await apiReq.get(API_ROUTES.shoppingListsId(id));
|
||||
return response.data;
|
||||
},
|
||||
/** Update Shopping List in the Database
|
||||
* @param id
|
||||
*/
|
||||
async updateShoppingList(id, data) {
|
||||
const response = await apiReq.put(API_ROUTES.shoppingListsId(id), data);
|
||||
return response.data;
|
||||
},
|
||||
/** Delete Shopping List from the Database
|
||||
* @param id
|
||||
*/
|
||||
async deleteShoppingList(id) {
|
||||
const response = await apiReq.delete(API_ROUTES.shoppingListsId(id));
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import i18n from "@/i18n.js";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const signupAPI = {
|
||||
async getAll() {
|
||||
let response = await apiReq.get(API_ROUTES.usersSignUps);
|
||||
return response.data;
|
||||
},
|
||||
async createToken(data) {
|
||||
let response = await apiReq.post(
|
||||
API_ROUTES.usersSignUps,
|
||||
data,
|
||||
() => i18n.t("signup.sign-up-link-creation-failed"),
|
||||
() => i18n.t("signup.sign-up-link-created")
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
async deleteToken(token) {
|
||||
return await apiReq.delete(
|
||||
API_ROUTES.usersSignUpsToken(token),
|
||||
null,
|
||||
() => i18n.t("signup.sign-up-token-deletion-failed"),
|
||||
() => i18n.t("signup.sign-up-token-deleted")
|
||||
);
|
||||
},
|
||||
async createUser(token, data) {
|
||||
return apiReq.post(
|
||||
API_ROUTES.usersSignUpsToken(token),
|
||||
data,
|
||||
() => i18n.t("user.you-are-not-allowed-to-create-a-user"),
|
||||
() => i18n.t("user.user-created")
|
||||
);
|
||||
},
|
||||
};
|
|
@ -1,71 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import { store } from "@/store";
|
||||
import i18n from "@/i18n.js";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const siteSettingsAPI = {
|
||||
async get() {
|
||||
let response = await apiReq.get(API_ROUTES.siteSettings);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async update(body) {
|
||||
const response = await apiReq.put(
|
||||
API_ROUTES.siteSettings,
|
||||
body,
|
||||
() => i18n.t("settings.settings-update-failed"),
|
||||
() => i18n.t("settings.settings-updated")
|
||||
);
|
||||
if (response) {
|
||||
store.dispatch("requestSiteSettings");
|
||||
}
|
||||
return response;
|
||||
},
|
||||
|
||||
async getPages() {
|
||||
let response = await apiReq.get(API_ROUTES.siteSettingsCustomPages);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getPage(id) {
|
||||
let response = await apiReq.get(API_ROUTES.siteSettingsCustomPagesId(id));
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createPage(body) {
|
||||
return apiReq.post(
|
||||
API_ROUTES.siteSettingsCustomPages,
|
||||
body,
|
||||
() => i18n.t("page.page-creation-failed"),
|
||||
() => i18n.t("page.new-page-created")
|
||||
);
|
||||
},
|
||||
|
||||
async deletePage(id) {
|
||||
return await apiReq.delete(
|
||||
API_ROUTES.siteSettingsCustomPagesId(id),
|
||||
null,
|
||||
() => i18n.t("page.page-deletion-failed"),
|
||||
() => i18n.t("page.page-deleted")
|
||||
);
|
||||
},
|
||||
|
||||
updatePage(body) {
|
||||
return apiReq.put(
|
||||
API_ROUTES.siteSettingsCustomPagesId(body.id),
|
||||
body,
|
||||
() => i18n.t("page.page-update-failed"),
|
||||
() => i18n.t("page.page-updated")
|
||||
);
|
||||
},
|
||||
|
||||
async updateAllPages(allPages) {
|
||||
let response = await apiReq.put(
|
||||
API_ROUTES.siteSettingsCustomPages,
|
||||
allPages,
|
||||
() => i18n.t("page.pages-update-failed"),
|
||||
() => i18n.t("page.pages-updated")
|
||||
);
|
||||
return response;
|
||||
},
|
||||
};
|
|
@ -1,42 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import i18n from "@/i18n.js";
|
||||
import { API_ROUTES } from "./apiRoutes";
|
||||
|
||||
export const themeAPI = {
|
||||
async requestAll() {
|
||||
let response = await apiReq.get(API_ROUTES.themes);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async requestByName(name) {
|
||||
let response = await apiReq.get(API_ROUTES.themesId(name));
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async create(postBody) {
|
||||
return await apiReq.post(
|
||||
API_ROUTES.themesCreate,
|
||||
postBody,
|
||||
() => i18n.t("settings.theme.error-creating-theme-see-log-file"),
|
||||
() => i18n.t("settings.theme.theme-saved")
|
||||
);
|
||||
},
|
||||
|
||||
update(data) {
|
||||
return apiReq.put(
|
||||
API_ROUTES.themesId(data.id),
|
||||
data,
|
||||
() => i18n.t("settings.theme.error-updating-theme"),
|
||||
() => i18n.t("settings.theme.theme-updated")
|
||||
);
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
return apiReq.delete(
|
||||
API_ROUTES.themesId(id),
|
||||
null,
|
||||
() => i18n.t("settings.theme.error-deleting-theme"),
|
||||
() => i18n.t("settings.theme.theme-deleted")
|
||||
);
|
||||
},
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import { apiReq } from "./api-utils";
|
||||
import i18n from "@/i18n.js";
|
||||
|
||||
export const utilsAPI = {
|
||||
// import { api } from "@/api";
|
||||
uploadFile(url, fileObject) {
|
||||
return apiReq.post(
|
||||
url,
|
||||
fileObject,
|
||||
() => i18n.t("general.failure-uploading-file"),
|
||||
() => i18n.t("general.file-uploaded")
|
||||
);
|
||||
},
|
||||
};
|
|
@ -1,107 +0,0 @@
|
|||
import { API_ROUTES } from "./apiRoutes";
|
||||
import { apiReq } from "./api-utils";
|
||||
import i18n from "@/i18n.js";
|
||||
|
||||
export const userAPI = {
|
||||
async login(formData) {
|
||||
let response = await apiReq.post(API_ROUTES.authToken, formData, null, () => {
|
||||
return i18n.t("user.user-successfully-logged-in");
|
||||
});
|
||||
return response;
|
||||
},
|
||||
async refresh() {
|
||||
return apiReq.getSafe(API_ROUTES.authRefresh);
|
||||
},
|
||||
async allUsers() {
|
||||
let response = await apiReq.get(API_ROUTES.users);
|
||||
return response.data;
|
||||
},
|
||||
create(user) {
|
||||
return apiReq.post(
|
||||
API_ROUTES.users,
|
||||
user,
|
||||
() => i18n.t("user.user-creation-failed"),
|
||||
() => i18n.t("user.user-created")
|
||||
);
|
||||
},
|
||||
async self() {
|
||||
return apiReq.getSafe(API_ROUTES.usersSelf);
|
||||
},
|
||||
async byID(id) {
|
||||
let response = await apiReq.get(API_ROUTES.usersId(id));
|
||||
return response.data;
|
||||
},
|
||||
update(user) {
|
||||
return apiReq.put(
|
||||
API_ROUTES.usersId(user.id),
|
||||
user,
|
||||
() => i18n.t("user.user-update-failed"),
|
||||
() => i18n.t("user.user-updated")
|
||||
);
|
||||
},
|
||||
changePassword(id, password) {
|
||||
return apiReq.put(
|
||||
API_ROUTES.usersIdPassword(id),
|
||||
password,
|
||||
() => i18n.t("user.existing-password-does-not-match"),
|
||||
() => i18n.t("user.password-updated")
|
||||
);
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
return apiReq.delete(API_ROUTES.usersId(id), null, deleteErrorText, () => {
|
||||
return i18n.t("user.user-deleted");
|
||||
});
|
||||
},
|
||||
resetPassword(id) {
|
||||
return apiReq.put(
|
||||
API_ROUTES.usersIdResetPassword(id),
|
||||
null,
|
||||
() => i18n.t("user.password-reset-failed"),
|
||||
() => i18n.t("user.password-has-been-reset-to-the-default-password")
|
||||
);
|
||||
},
|
||||
async createAPIToken(name) {
|
||||
const response = await apiReq.post(API_ROUTES.usersApiTokens, { name });
|
||||
return response.data;
|
||||
},
|
||||
async deleteAPIToken(id) {
|
||||
const response = await apiReq.delete(API_ROUTES.usersApiTokensTokenId(id));
|
||||
return response.data;
|
||||
},
|
||||
/** Adds a Recipe to the users favorites
|
||||
* @param id
|
||||
*/
|
||||
async getFavorites(id) {
|
||||
const response = await apiReq.get(API_ROUTES.usersIdFavorites(id));
|
||||
return response.data;
|
||||
},
|
||||
/** Adds a Recipe to the users favorites
|
||||
* @param id
|
||||
*/
|
||||
async addFavorite(id, slug) {
|
||||
const response = await apiReq.post(API_ROUTES.usersIdFavoritesSlug(id, slug));
|
||||
return response.data;
|
||||
},
|
||||
/** Adds a Recipe to the users favorites
|
||||
* @param id
|
||||
*/
|
||||
async removeFavorite(id, slug) {
|
||||
const response = await apiReq.delete(API_ROUTES.usersIdFavoritesSlug(id, slug));
|
||||
return response.data;
|
||||
},
|
||||
|
||||
userProfileImage(id) {
|
||||
if (!id || id === undefined) return;
|
||||
return `/api/users/${id}/image`;
|
||||
},
|
||||
};
|
||||
|
||||
const deleteErrorText = response => {
|
||||
switch (response.data.detail) {
|
||||
case "SUPER_USER":
|
||||
return i18n.t("user.error-cannot-delete-super-user");
|
||||
default:
|
||||
return i18n.t("user.you-are-not-allowed-to-delete-this-user");
|
||||
}
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<The404>
|
||||
<h1 class="mx-auto">{{ $t('general.no-recipe-found') }}</h1>
|
||||
</The404>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import The404 from "./The404.vue";
|
||||
export default {
|
||||
components: { The404 },
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card-title>
|
||||
<slot>
|
||||
<h1 class="mx-auto">{{ $t('page.404-page-not-found') }}</h1>
|
||||
</slot>
|
||||
</v-card-title>
|
||||
<div class="d-flex justify-space-around">
|
||||
<div class="d-flex">
|
||||
<p>4</p>
|
||||
<v-icon color="primary" class="mx-auto" size="200">
|
||||
{{ $globals.icons.primary }}
|
||||
</v-icon>
|
||||
<p>4</p>
|
||||
</div>
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<slot name="actions">
|
||||
<v-btn v-for="(button, index) in buttons" :key="index" :to="button.to" color="primary">
|
||||
<v-icon left> {{ button.icon }} </v-icon>
|
||||
{{ button.text }}
|
||||
</v-btn>
|
||||
</slot>
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
buttons() {
|
||||
return[
|
||||
{ icon: this.$globals.icons.home, to: "/", text: this.$t('general.home') },
|
||||
{ icon: this.$globals.icons.primary, to: "/recipes/all", text: this.$t('page.all-recipes') },
|
||||
{ icon: this.$globals.icons.search, to: "/search", text: this.$t('search.search') },
|
||||
];
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
p {
|
||||
padding-bottom: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
color: var(--v-primary-base);
|
||||
font-size: 200px;
|
||||
}
|
||||
</style>
|
|
@ -1,134 +0,0 @@
|
|||
<template>
|
||||
<v-autocomplete
|
||||
:items="activeItems"
|
||||
v-model="selected"
|
||||
:value="value"
|
||||
:label="inputLabel"
|
||||
chips
|
||||
deletable-chips
|
||||
:dense="dense"
|
||||
item-text="name"
|
||||
persistent-hint
|
||||
multiple
|
||||
:hint="hint"
|
||||
:solo="solo"
|
||||
:return-object="returnObject"
|
||||
:flat="flat"
|
||||
@input="emitChange"
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
@click:close="removeByIndex(data.index)"
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
:key="data.index"
|
||||
>
|
||||
{{ data.item.name || data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:append-outer="">
|
||||
<NewCategoryTagDialog v-if="showAdd" :tag-dialog="tagSelector" @created-item="pushToItem" />
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog";
|
||||
const MOUNTED_EVENT = "mounted";
|
||||
export default {
|
||||
components: {
|
||||
NewCategoryTagDialog,
|
||||
},
|
||||
props: {
|
||||
value: Array,
|
||||
solo: {
|
||||
default: false,
|
||||
},
|
||||
dense: {
|
||||
default: true,
|
||||
},
|
||||
returnObject: {
|
||||
default: true,
|
||||
},
|
||||
tagSelector: {
|
||||
default: false,
|
||||
},
|
||||
hint: {
|
||||
default: null,
|
||||
},
|
||||
showAdd: {
|
||||
default: false,
|
||||
},
|
||||
showLabel: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: [],
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
if (this.tagSelector) {
|
||||
this.$store.dispatch("requestTags");
|
||||
} else {
|
||||
this.$store.dispatch("requestCategories");
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$emit(MOUNTED_EVENT);
|
||||
this.setInit(this.value);
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(val) {
|
||||
this.selected = val;
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
inputLabel() {
|
||||
if (!this.showLabel) return null;
|
||||
return this.tagSelector ? this.$t("tag.tags") : this.$t("recipe.categories");
|
||||
},
|
||||
activeItems() {
|
||||
let ItemObjects = [];
|
||||
if (this.tagSelector) ItemObjects = this.$store.getters.getAllTags;
|
||||
else {
|
||||
ItemObjects = this.$store.getters.getAllCategories;
|
||||
}
|
||||
if (this.returnObject) return ItemObjects;
|
||||
else {
|
||||
return ItemObjects.map(x => x.name);
|
||||
}
|
||||
},
|
||||
flat() {
|
||||
if (this.selected) {
|
||||
return this.selected.length > 0 && this.solo;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
emitChange() {
|
||||
this.$emit("input", this.selected);
|
||||
},
|
||||
setInit(val) {
|
||||
this.selected = val;
|
||||
},
|
||||
removeByIndex(index) {
|
||||
this.selected.splice(index, 1);
|
||||
},
|
||||
pushToItem(createdItem) {
|
||||
createdItem = this.returnObject ? createdItem : createdItem.name;
|
||||
this.selected.push(createdItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -1,65 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<h3>{{ buttonText }}</h3>
|
||||
</div>
|
||||
<v-text-field v-model="color" hide-details class="ma-0 pa-0" solo >
|
||||
<template v-slot:append>
|
||||
<v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false">
|
||||
<template v-slot:activator="{ on }">
|
||||
<div :style="swatchStyle" v-on="on" swatches-max-height="300" />
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-text class="pa-0">
|
||||
<v-color-picker v-model="color" flat mode="hexa" show-swatches />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
buttonText: String,
|
||||
value: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
swatches: false,
|
||||
color: this.value || "#1976D2",
|
||||
mask: "!#XXXXXXXX",
|
||||
menu: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
swatchStyle() {
|
||||
const { value, menu } = this;
|
||||
return {
|
||||
backgroundColor: value,
|
||||
cursor: "pointer",
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
borderRadius: menu ? "50%" : "4px",
|
||||
transition: "border-radius 200ms ease-in-out",
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
color() {
|
||||
this.updateColor();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateColor() {
|
||||
this.$emit("input", this.color);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,27 +0,0 @@
|
|||
<template>
|
||||
<v-date-picker :first-day-of-week="firstDayOfWeek" v-on="$listeners"></v-date-picker>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
firstDayOfWeek: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getOptions();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getOptions() {
|
||||
const settings = await api.siteSettings.get();
|
||||
this.firstDayOfWeek = settings.firstDayOfWeek;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,69 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-checkbox
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="mb-n4 mt-n3"
|
||||
dense
|
||||
:label="option.text"
|
||||
v-model="option.value"
|
||||
@change="emitValue()"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const UPDATE_EVENT = "update-options";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
options: {
|
||||
recipes: {
|
||||
value: true,
|
||||
text: this.$t("general.recipes"),
|
||||
},
|
||||
settings: {
|
||||
value: true,
|
||||
text: this.$t("general.settings"),
|
||||
},
|
||||
pages: {
|
||||
value: true,
|
||||
text: this.$t("settings.pages"),
|
||||
},
|
||||
themes: {
|
||||
value: true,
|
||||
text: this.$t("general.themes"),
|
||||
},
|
||||
users: {
|
||||
value: true,
|
||||
text: this.$t("user.users"),
|
||||
},
|
||||
groups: {
|
||||
value: true,
|
||||
text: this.$t("group.groups"),
|
||||
},
|
||||
notifications: {
|
||||
value: true,
|
||||
text: this.$t("events.notification"),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.emitValue();
|
||||
},
|
||||
methods: {
|
||||
emitValue() {
|
||||
this.$emit(UPDATE_EVENT, {
|
||||
recipes: this.options.recipes.value,
|
||||
settings: this.options.settings.value,
|
||||
themes: this.options.themes.value,
|
||||
pages: this.options.pages.value,
|
||||
users: this.options.users.value,
|
||||
groups: this.options.groups.value,
|
||||
notifications: this.options.notifications.value,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,48 +0,0 @@
|
|||
<template>
|
||||
<v-select
|
||||
dense
|
||||
:items="allLanguages"
|
||||
item-text="name"
|
||||
:label="$t('settings.language')"
|
||||
:prepend-icon="$globals.icons.translate"
|
||||
:value="selectedItem"
|
||||
@input="setLanguage"
|
||||
>
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const SELECT_EVENT = "select-lang";
|
||||
export default {
|
||||
props: {
|
||||
siteSettings: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
selectedItem: 0,
|
||||
items: [
|
||||
{
|
||||
name: "English",
|
||||
value: "en-US",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.selectedItem = this.$store.getters.getActiveLang;
|
||||
},
|
||||
computed: {
|
||||
allLanguages() {
|
||||
return this.$store.getters.getAllLangs;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
setLanguage(selectedLanguage) {
|
||||
this.$emit(SELECT_EVENT, selectedLanguage);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,42 +0,0 @@
|
|||
<template>
|
||||
<v-dialog ref="dialog" v-model="modal2" :return-value.sync="time" persistent width="290px">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="time"
|
||||
:label="$t('settings.set-new-time')"
|
||||
:prepend-icon="$globals.icons.clockOutline"
|
||||
readonly
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
></v-text-field>
|
||||
</template>
|
||||
<v-time-picker v-if="modal2" v-model="time" full-width>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="primary" @click="modal2 = false"> {{ $t("general.cancel") }} </v-btn>
|
||||
<v-btn text color="primary" @click="saveTime"> {{ $t("general.ok") }} </v-btn>
|
||||
</v-time-picker>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
time: null,
|
||||
modal2: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
saveTime() {
|
||||
this.$refs.dialog.save(this.time);
|
||||
this.$emit("save-time", this.time);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-text-field {
|
||||
max-width: 300px;
|
||||
}
|
||||
</style>
|
|
@ -1,46 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-data-table
|
||||
dense
|
||||
:headers="dataHeaders"
|
||||
:items="dataSet"
|
||||
item-key="name"
|
||||
class="elevation-1 mt-2"
|
||||
show-expand
|
||||
:expanded.sync="expanded"
|
||||
:footer-props="{
|
||||
'items-per-page-options': [100, 200, 300, 400, -1],
|
||||
}"
|
||||
:items-per-page="100"
|
||||
>
|
||||
<template v-slot:item.status="{ item }">
|
||||
<div :class="item.status ? 'success--text' : 'error--text'">
|
||||
{{ item.status ? "Imported" : "Failed" }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:expanded-item="{ headers, item }">
|
||||
<td :colspan="headers.length">
|
||||
<div class="ma-2">
|
||||
{{ item.exception }}
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
dataSet: Array,
|
||||
dataHeaders: Array,
|
||||
},
|
||||
data: () => ({
|
||||
singleExpand: false,
|
||||
expanded: [],
|
||||
}),
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,142 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="70%">
|
||||
<v-card>
|
||||
<v-app-bar dark color="primary mb-2">
|
||||
<v-icon large left>
|
||||
{{ $globals.icons.import }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline">
|
||||
{{ $t("settings.backup.import-summary") }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card-text class="mb-n4">
|
||||
<v-row>
|
||||
<div v-for="values in allNumbers" :key="values.title">
|
||||
<v-card-text>
|
||||
<div>
|
||||
<h3>{{ values.title }}</h3>
|
||||
</div>
|
||||
<div class="success--text">{{ $t("general.success-count", { count: values.success }) }}</div>
|
||||
<div class="error--text">{{ $t("general.failed-count", { count: values.failure }) }}</div>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-tabs v-model="tab" show-arrows="">
|
||||
<v-tab>{{ $t("general.recipes") }}</v-tab>
|
||||
<v-tab>{{ $t("general.themes") }}</v-tab>
|
||||
<v-tab>{{ $t("general.settings") }}</v-tab>
|
||||
<v-tab> {{ $t("settings.pages") }} </v-tab>
|
||||
<v-tab>{{ $t("user.users") }}</v-tab>
|
||||
<v-tab>{{ $t("group.groups") }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-items v-model="tab">
|
||||
<v-tab-item v-for="(table, index) in allTables" :key="index">
|
||||
<v-card flat>
|
||||
<DataTable :data-headers="importHeaders" :data-set="table" />
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataTable from "@/components/ImportSummaryDialog/DataTable";
|
||||
export default {
|
||||
components: {
|
||||
DataTable,
|
||||
},
|
||||
data: () => ({
|
||||
tab: null,
|
||||
dialog: false,
|
||||
recipeData: [],
|
||||
themeData: [],
|
||||
settingsData: [],
|
||||
userData: [],
|
||||
groupData: [],
|
||||
pageData: [],
|
||||
allDataTables: [],
|
||||
}),
|
||||
|
||||
computed: {
|
||||
importHeaders() {
|
||||
return [
|
||||
{
|
||||
text: this.$t("general.status"),
|
||||
value: "status",
|
||||
},
|
||||
{
|
||||
text: this.$t("general.name"),
|
||||
align: "start",
|
||||
sortable: true,
|
||||
value: "name",
|
||||
},
|
||||
{
|
||||
text: this.$t("general.exception"),
|
||||
value: "data-table-expand",
|
||||
align: "center",
|
||||
},
|
||||
];
|
||||
},
|
||||
recipeNumbers() {
|
||||
return this.calculateNumbers(this.$t("general.recipes"), this.recipeData);
|
||||
},
|
||||
settingsNumbers() {
|
||||
return this.calculateNumbers(this.$t("general.settings"), this.settingsData);
|
||||
},
|
||||
themeNumbers() {
|
||||
return this.calculateNumbers(this.$t("general.themes"), this.themeData);
|
||||
},
|
||||
userNumbers() {
|
||||
return this.calculateNumbers(this.$t("user.users"), this.userData);
|
||||
},
|
||||
groupNumbers() {
|
||||
return this.calculateNumbers(this.$t("group.groups"), this.groupData);
|
||||
},
|
||||
pageNumbers() {
|
||||
return this.calculateNumbers(this.$t("settings.pages"), this.pageData);
|
||||
},
|
||||
allNumbers() {
|
||||
return [
|
||||
this.recipeNumbers,
|
||||
this.themeNumbers,
|
||||
this.settingsNumbers,
|
||||
this.pageNumbers,
|
||||
this.userNumbers,
|
||||
this.groupNumbers,
|
||||
];
|
||||
},
|
||||
allTables() {
|
||||
return [this.recipeData, this.themeData, this.settingsData, this.pageData, this.userData, this.groupData];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
calculateNumbers(title, list_array) {
|
||||
if (!list_array) return;
|
||||
let numbers = { title: title, success: 0, failure: 0 };
|
||||
list_array.forEach(element => {
|
||||
if (element.status) {
|
||||
numbers.success++;
|
||||
} else numbers.failure++;
|
||||
});
|
||||
return numbers;
|
||||
},
|
||||
open(importData) {
|
||||
this.recipeData = importData.recipeImports;
|
||||
this.themeData = importData.themeImports;
|
||||
this.settingsData = importData.settingsImports;
|
||||
this.userData = importData.userImports;
|
||||
this.groupData = importData.groupImports;
|
||||
this.pageData = importData.pageImports;
|
||||
this.dialog = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,28 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="500px">
|
||||
<LoginForm @logged-in="dialog = false" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoginForm from "./LoginForm";
|
||||
export default {
|
||||
components: {
|
||||
LoginForm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,94 +0,0 @@
|
|||
<template>
|
||||
<v-card width="500px">
|
||||
<v-divider></v-divider>
|
||||
<v-app-bar dark color="primary" class="mt-n1 mb-2">
|
||||
<v-icon large left v-if="!loading">
|
||||
{{ $globals.icons.user }}
|
||||
</v-icon>
|
||||
<v-progress-circular v-else indeterminate color="white" large class="mr-2"> </v-progress-circular>
|
||||
<v-toolbar-title class="headline">{{ $t("user.login") }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
|
||||
<v-form @submit.prevent="login">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="user.email"
|
||||
:prepend-icon="$globals.icons.email"
|
||||
validate-on-blur
|
||||
autocomplete
|
||||
autofocus
|
||||
:label="`${$t('user.email')} or ${$t('user.username')} `"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.password"
|
||||
class="mb-2s"
|
||||
autocomplete
|
||||
:prepend-icon="$globals.icons.lock"
|
||||
:label="$t('user.password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? $globals.icons.eye : $globals.icons.eyeOff"
|
||||
@click:append="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
<v-card-actions>
|
||||
<v-btn v-if="options.isLoggingIn" color="primary" block large type="submit">{{ $t("user.sign-in") }} </v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<v-alert v-if="error" class="mt-3 mb-0" type="error">
|
||||
{{ $t("user.could-not-validate-credentials") }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
error: false,
|
||||
showLogin: false,
|
||||
showPassword: false,
|
||||
user: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
options: {
|
||||
isLoggingIn: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.clear();
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.user = { email: "", password: "" };
|
||||
},
|
||||
async login() {
|
||||
this.loading = true;
|
||||
this.error = false;
|
||||
let formData = new FormData();
|
||||
formData.append("username", this.user.email);
|
||||
formData.append("password", this.user.password);
|
||||
const response = await api.users.login(formData);
|
||||
if (!response) {
|
||||
this.error = true;
|
||||
} else {
|
||||
this.clear();
|
||||
this.$store.commit("setToken", response.data.access_token);
|
||||
this.$emit("logged-in");
|
||||
this.$store.dispatch("requestUserData");
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,141 +0,0 @@
|
|||
<template>
|
||||
<v-card width="500px">
|
||||
<v-divider></v-divider>
|
||||
<v-app-bar dark color="primary" class="mt-n1">
|
||||
<v-icon large left v-if="!loading">
|
||||
{{ $globals.icons.user }}
|
||||
</v-icon>
|
||||
<v-progress-circular v-else indeterminate color="white" large class="mr-2"> </v-progress-circular>
|
||||
<v-toolbar-title class="headline">
|
||||
{{ $t("signup.sign-up") }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card-text>
|
||||
{{ $t("signup.welcome-to-mealie") }}
|
||||
<v-divider class="mt-3"></v-divider>
|
||||
<v-form ref="signUpForm" @submit.prevent="signUp">
|
||||
<v-text-field
|
||||
v-model="user.name"
|
||||
light="light"
|
||||
:prepend-icon="$globals.icons.user"
|
||||
validate-on-blur
|
||||
:rules="[existsRule]"
|
||||
:label="$t('user.full-name')"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.username"
|
||||
light="light"
|
||||
:prepend-icon="$globals.icons.user"
|
||||
validate-on-blur
|
||||
:rules="[existsRule]"
|
||||
:label="$t('user.username')"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.email"
|
||||
light="light"
|
||||
:prepend-icon="$globals.icons.email"
|
||||
validate-on-blur
|
||||
:rules="[existsRule, emailRule]"
|
||||
:label="$t('user.email')"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.password"
|
||||
light="light"
|
||||
class="mb-2s"
|
||||
:prepend-icon="$globals.icons.lock"
|
||||
validate-on-blur
|
||||
:label="$t('user.password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:rules="[minRule]"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.passwordConfirm"
|
||||
light="light"
|
||||
class="mb-2s"
|
||||
:prepend-icon="$globals.icons.lock"
|
||||
:label="$t('user.password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? $globals.icons.eye : $globals.icons.eyeOff"
|
||||
:rules="[user.password === user.passwordConfirm || $t('user.password-must-match')]"
|
||||
@click:append="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
<v-card-actions>
|
||||
<v-btn v-if="options.isLoggingIn" dark color="primary" block="block" type="submit">
|
||||
{{ $t("signup.sign-up") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
<v-alert dense v-if="error" outlined class="mt-3 mb-0" type="error">
|
||||
{{ $t("signup.error-signing-up") }}
|
||||
</v-alert>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
mixins: [validators],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
error: false,
|
||||
showPassword: false,
|
||||
user: {
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
passwordConfirm: "",
|
||||
},
|
||||
options: {
|
||||
isLoggingIn: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.clear();
|
||||
},
|
||||
computed: {
|
||||
token() {
|
||||
return this.$route.params.token;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.user = {
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
passwordConfirm: "",
|
||||
};
|
||||
},
|
||||
async signUp() {
|
||||
this.loading = true;
|
||||
this.error = false;
|
||||
|
||||
const userData = {
|
||||
fullName: this.user.name,
|
||||
username: this.user.username,
|
||||
email: this.user.email,
|
||||
group: "default",
|
||||
password: this.user.password,
|
||||
admin: false,
|
||||
};
|
||||
|
||||
if (this.$refs.signUpForm.validate()) {
|
||||
if (await api.signUps.createUser(this.token, userData)) {
|
||||
this.$emit("user-created");
|
||||
this.$router.push("/");
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,100 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="650">
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
{{ $t("shopping-list.shopping-list") }}
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="accent" @click="group = !group">
|
||||
{{ $t("meal-plan.group") }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text v-if="group == false">
|
||||
<v-list dense v-for="(recipe, index) in ingredients" :key="`${index}-recipe`">
|
||||
<v-subheader>{{ recipe.name }} </v-subheader>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list-item-group color="primary">
|
||||
<v-list-item v-for="(item, i) in recipe.recipe_ingredient" :key="i">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-else>
|
||||
<v-list dense>
|
||||
<v-list-item-group color="primary">
|
||||
<v-list-item v-for="(item, i) in rawIngredients" :key="i">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
const levenshtein = require("fast-levenshtein");
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
planID: 0,
|
||||
ingredients: [],
|
||||
rawIngredients: [],
|
||||
group: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openDialog: function(id) {
|
||||
this.dialog = true;
|
||||
this.planID = id;
|
||||
this.getIngredients();
|
||||
},
|
||||
async getIngredients() {
|
||||
this.ingredients = await api.mealPlans.shoppingList(this.planID);
|
||||
this.getRawIngredients();
|
||||
},
|
||||
getRawIngredients() {
|
||||
this.rawIngredients = [];
|
||||
this.ingredients.forEach(element => {
|
||||
this.rawIngredients.push(element.recipe_ingredient);
|
||||
});
|
||||
|
||||
this.rawIngredients = this.rawIngredients.flat();
|
||||
this.rawIngredients = this.levenshteinFilter(this.rawIngredients);
|
||||
},
|
||||
levenshteinFilter(source, maximum = 5) {
|
||||
let _source, matches, x, y;
|
||||
_source = source.slice();
|
||||
matches = [];
|
||||
for (x = _source.length - 1; x >= 0; x--) {
|
||||
let output = _source.splice(x, 1);
|
||||
for (y = _source.length - 1; y >= 0; y--) {
|
||||
if (levenshtein.get(output[0], _source[y]) <= maximum) {
|
||||
output.push(_source[y]);
|
||||
_source.splice(y, 1);
|
||||
x--;
|
||||
}
|
||||
}
|
||||
matches.push(output);
|
||||
}
|
||||
return matches.flat();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,163 +0,0 @@
|
|||
<template>
|
||||
<v-btn
|
||||
:color="btnAttrs.color"
|
||||
:small="small"
|
||||
:x-small="xSmall"
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
@click="$emit('click')"
|
||||
:outlined="btnStyle.outlined"
|
||||
:text="btnStyle.text"
|
||||
>
|
||||
<v-icon left>
|
||||
<slot name="icon">
|
||||
{{ btnAttrs.icon }}
|
||||
</slot>
|
||||
</v-icon>
|
||||
<slot>
|
||||
{{ btnAttrs.text }}
|
||||
</slot>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TheButton",
|
||||
props: {
|
||||
// Types
|
||||
cancel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
create: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
update: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
delete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// Property
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// Styles
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
xSmall: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
secondary: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
minor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
buttonOptions: {
|
||||
create: {
|
||||
text: this.$t("general.create"),
|
||||
icon: this.$globals.icons.create,
|
||||
color: "success",
|
||||
},
|
||||
update: {
|
||||
text: this.$t("general.update"),
|
||||
icon: this.$globals.icons.update,
|
||||
color: "success",
|
||||
},
|
||||
save: {
|
||||
text: this.$t("general.save"),
|
||||
icon: this.$globals.icons.save,
|
||||
color: "success",
|
||||
},
|
||||
edit: {
|
||||
text: this.$t("general.edit"),
|
||||
icon: this.$globals.icons.edit,
|
||||
color: "info",
|
||||
},
|
||||
delete: {
|
||||
text: this.$t("general.delete"),
|
||||
icon: this.$globals.icons.delete,
|
||||
color: "error",
|
||||
},
|
||||
cancel: {
|
||||
text: this.$t("general.cancel"),
|
||||
icon: this.$globals.icons.close,
|
||||
color: "grey",
|
||||
},
|
||||
},
|
||||
buttonStyles: {
|
||||
defaults: {
|
||||
text: false,
|
||||
outlined: false,
|
||||
},
|
||||
secondary: {
|
||||
text: false,
|
||||
outlined: true,
|
||||
},
|
||||
minor: {
|
||||
outlined: false,
|
||||
text: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
btnAttrs() {
|
||||
if (this.delete) {
|
||||
return this.buttonOptions.delete;
|
||||
} else if (this.update) {
|
||||
return this.buttonOptions.update;
|
||||
} else if (this.edit) {
|
||||
return this.buttonOptions.edit;
|
||||
} else if (this.cancel) {
|
||||
this.setMinor();
|
||||
return this.buttonOptions.cancel;
|
||||
} else if (this.save) {
|
||||
return this.buttonOptions.save;
|
||||
}
|
||||
|
||||
return this.buttonOptions.create;
|
||||
},
|
||||
btnStyle() {
|
||||
if (this.secondary) {
|
||||
return this.buttonStyles.secondary;
|
||||
} else if (this.minor) {
|
||||
return this.buttonStyles.minor;
|
||||
}
|
||||
return this.buttonStyles.defaults;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setMinor() {
|
||||
this.buttonStyles.defaults = this.buttonStyles.minor;
|
||||
},
|
||||
setSecondary() {
|
||||
this.buttonStyles.defaults = this.buttonStyles.secondary;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -1,71 +0,0 @@
|
|||
<template>
|
||||
<v-tooltip
|
||||
ref="copyToolTip"
|
||||
v-model="show"
|
||||
color="success lighten-1"
|
||||
top
|
||||
:open-on-hover="false"
|
||||
:open-on-click="true"
|
||||
close-delay="500"
|
||||
transition="slide-y-transition"
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
icon
|
||||
:color="color"
|
||||
@click="
|
||||
on.click;
|
||||
textToClipboard();
|
||||
"
|
||||
@blur="on.blur"
|
||||
retain-focus-on-click
|
||||
>
|
||||
<v-icon>{{ $globals.icons.contentCopy }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>
|
||||
<v-icon left dark>
|
||||
{{ $globals.icons.clipboardCheck }}
|
||||
</v-icon>
|
||||
<slot> {{ $t("general.copied") }}! </slot>
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
copyText: {
|
||||
default: "Default Copy Text",
|
||||
},
|
||||
color: {
|
||||
default: "primary",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleBlur() {
|
||||
this.$refs.copyToolTip.deactivate();
|
||||
},
|
||||
textToClipboard() {
|
||||
this.show = true;
|
||||
const copyText = this.copyText;
|
||||
navigator.clipboard.writeText(copyText).then(
|
||||
() => console.log("Copied", copyText),
|
||||
() => console.log("Copied Failed", copyText)
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.toggleBlur();
|
||||
}, 500);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -1,54 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot v-bind="{ downloading, downloadFile }">
|
||||
<v-btn color="accent" text :loading="downloading" @click="downloadFile">
|
||||
{{ showButtonText }}
|
||||
</v-btn>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* The download button used for the entire site
|
||||
* pass a URL to the endpoint that will return a
|
||||
* file_token which will then be used to request the file
|
||||
* from the server and open that link in a new tab
|
||||
*/
|
||||
import { apiReq } from "@/api/api-utils";
|
||||
export default {
|
||||
props: {
|
||||
/**
|
||||
* URL to get token from
|
||||
*/
|
||||
downloadUrl: {
|
||||
default: "",
|
||||
},
|
||||
/**
|
||||
* Override button text. Defaults to "Download"
|
||||
*/
|
||||
buttonText: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
downloading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showButtonText() {
|
||||
return this.buttonText || this.$t("general.download");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async downloadFile() {
|
||||
this.downloading = true;
|
||||
await apiReq.download(this.downloadUrl);
|
||||
this.downloading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -1,89 +0,0 @@
|
|||
<template>
|
||||
<v-form ref="file">
|
||||
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
|
||||
<slot v-bind="{ isSelecting, onButtonClick }">
|
||||
<v-btn :loading="isSelecting" @click="onButtonClick" :small="small" color="accent" :text="textBtn">
|
||||
<v-icon left> {{ effIcon }}</v-icon>
|
||||
{{ text ? text : defaultText }}
|
||||
</v-btn>
|
||||
</slot>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const UPLOAD_EVENT = "uploaded";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
props: {
|
||||
small: {
|
||||
default: false,
|
||||
},
|
||||
post: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
url: String,
|
||||
text: String,
|
||||
icon: { default: null },
|
||||
fileName: { default: "archive" },
|
||||
textBtn: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
file: null,
|
||||
isSelecting: false,
|
||||
}),
|
||||
|
||||
computed: {
|
||||
effIcon() {
|
||||
return this.icon ? this.icon : this.$globals.icons.upload;
|
||||
},
|
||||
defaultText() {
|
||||
return this.$t("general.upload");
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async upload() {
|
||||
if (this.file != null) {
|
||||
this.isSelecting = true;
|
||||
|
||||
if (!this.post) {
|
||||
this.$emit(UPLOAD_EVENT, this.file);
|
||||
this.isSelecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append(this.fileName, this.file);
|
||||
|
||||
const response = await api.utils.uploadFile(this.url, formData);
|
||||
|
||||
if (response) {
|
||||
this.$emit(UPLOAD_EVENT, response);
|
||||
}
|
||||
this.isSelecting = false;
|
||||
}
|
||||
},
|
||||
onButtonClick() {
|
||||
this.isSelecting = true;
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
this.isSelecting = false;
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
this.$refs.uploader.click();
|
||||
},
|
||||
onFileChanged(e) {
|
||||
this.file = e.target.files[0];
|
||||
this.upload();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,146 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<BaseDialog
|
||||
:title="$t('settings.backup.create-heading')"
|
||||
:titleIcon="$globals.icons.database"
|
||||
@submit="createBackup"
|
||||
:submit-text="$t('general.create')"
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-slot:open="{ open }">
|
||||
<v-btn @click="open" class="mx-2" small :color="color">
|
||||
<v-icon left> {{ $globals.icons.create }} </v-icon> {{ $t("general.custom") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="mt-6">
|
||||
<v-text-field dense :label="$t('settings.backup.backup-tag')" v-model="tag"></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions class="mt-n9 flex-wrap">
|
||||
<v-switch v-model="fullBackup" :label="switchLabel"></v-switch>
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
<v-expand-transition>
|
||||
<div v-if="!fullBackup">
|
||||
<v-card-text class="mt-n4">
|
||||
<v-row>
|
||||
<v-col sm="4">
|
||||
<p>{{ $t("general.options") }}</p>
|
||||
<ImportOptions @update-options="updateOptions" class="mt-5" />
|
||||
</v-col>
|
||||
<v-col>
|
||||
<p>{{ $t("general.templates") }}</p>
|
||||
<v-checkbox
|
||||
v-for="template in availableTemplates"
|
||||
:key="template"
|
||||
class="mb-n4 mt-n3"
|
||||
dense
|
||||
:label="template"
|
||||
@click="appendTemplate(template)"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import ImportOptions from "@/components/FormHelpers/ImportOptions";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
props: {
|
||||
color: { default: "primary" },
|
||||
},
|
||||
components: {
|
||||
BaseDialog,
|
||||
ImportOptions,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tag: null,
|
||||
fullBackup: true,
|
||||
loading: false,
|
||||
options: {
|
||||
recipes: true,
|
||||
settings: true,
|
||||
themes: true,
|
||||
pages: true,
|
||||
users: true,
|
||||
groups: true,
|
||||
},
|
||||
availableTemplates: [],
|
||||
selectedTemplates: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
switchLabel() {
|
||||
if (this.fullBackup) {
|
||||
return this.$t("settings.backup.full-backup");
|
||||
} else return this.$t("settings.backup.partial-backup");
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.resetData();
|
||||
this.getAvailableBackups();
|
||||
},
|
||||
methods: {
|
||||
resetData() {
|
||||
this.tag = null;
|
||||
this.fullBackup = true;
|
||||
this.loading = false;
|
||||
this.options = {
|
||||
recipes: true,
|
||||
settings: true,
|
||||
themes: true,
|
||||
pages: true,
|
||||
users: true,
|
||||
groups: true,
|
||||
notifications: true,
|
||||
};
|
||||
this.availableTemplates = [];
|
||||
this.selectedTemplates = [];
|
||||
},
|
||||
updateOptions(options) {
|
||||
this.options = options;
|
||||
},
|
||||
async getAvailableBackups() {
|
||||
const response = await api.backups.requestAvailable();
|
||||
response.templates.forEach(element => {
|
||||
this.availableTemplates.push(element);
|
||||
});
|
||||
},
|
||||
async createBackup() {
|
||||
this.loading = true;
|
||||
const data = {
|
||||
tag: this.tag,
|
||||
options: {
|
||||
recipes: this.options.recipes,
|
||||
settings: this.options.settings,
|
||||
pages: this.options.pages,
|
||||
themes: this.options.themes,
|
||||
users: this.options.users,
|
||||
groups: this.options.groups,
|
||||
notifications: this.options.notifications,
|
||||
},
|
||||
templates: this.selectedTemplates,
|
||||
};
|
||||
|
||||
if (await api.backups.create(data)) {
|
||||
this.$emit("created");
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
appendTemplate(templateName) {
|
||||
if (this.selectedTemplates.includes(templateName)) {
|
||||
let index = this.selectedTemplates.indexOf(templateName);
|
||||
if (index !== -1) {
|
||||
this.selectedTemplates.splice(index, 1);
|
||||
}
|
||||
} else this.selectedTemplates.push(templateName);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,119 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<BaseDialog
|
||||
:title="name"
|
||||
:titleIcon="$globals.icons.database"
|
||||
:submit-text="$t('general.import')"
|
||||
:loading="loading"
|
||||
ref="baseDialog"
|
||||
@submit="raiseEvent"
|
||||
>
|
||||
<v-card-subtitle class="mb-n3 mt-3" v-if="date"> {{ $d(new Date(date), "medium") }} </v-card-subtitle>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<ImportOptions @update-options="updateOptions" class="mt-5 mb-2" />
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-checkbox
|
||||
dense
|
||||
:label="$t('settings.remove-existing-entries-matching-imported-entries')"
|
||||
v-model="forceImport"
|
||||
></v-checkbox>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<template v-slot:extra-buttons>
|
||||
<TheDownloadBtn :download-url="downloadUrl">
|
||||
<template v-slot:default="{ downloadFile }">
|
||||
<v-btn class="mr-1" color="info" @click="downloadFile">
|
||||
<v-icon left> {{ $globals.icons.download }}</v-icon>
|
||||
{{ $t("general.download") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</TheDownloadBtn>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const IMPORT_EVENT = "import";
|
||||
import { api } from "@/api";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import ImportOptions from "@/components/FormHelpers/ImportOptions";
|
||||
import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue";
|
||||
import { API_ROUTES } from "@/api/apiRoutes";
|
||||
export default {
|
||||
components: { ImportOptions, TheDownloadBtn, BaseDialog },
|
||||
props: {
|
||||
name: {
|
||||
default: "Backup Name",
|
||||
},
|
||||
date: {
|
||||
default: "Backup Date",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
options: {
|
||||
recipes: true,
|
||||
settings: true,
|
||||
themes: true,
|
||||
users: true,
|
||||
groups: true,
|
||||
},
|
||||
dialog: false,
|
||||
forceImport: false,
|
||||
rebaseImport: false,
|
||||
downloading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
downloadUrl() {
|
||||
return API_ROUTES.backupsFileNameDownload(this.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateOptions(options) {
|
||||
this.options = options;
|
||||
},
|
||||
open() {
|
||||
this.dialog = true;
|
||||
this.$refs.baseDialog.open();
|
||||
},
|
||||
close() {
|
||||
this.dialog = false;
|
||||
},
|
||||
async raiseEvent() {
|
||||
const eventData = {
|
||||
name: this.name,
|
||||
force: this.forceImport,
|
||||
rebase: this.rebaseImport,
|
||||
recipes: this.options.recipes,
|
||||
settings: this.options.settings,
|
||||
themes: this.options.themes,
|
||||
users: this.options.users,
|
||||
groups: this.options.groups,
|
||||
notifications: this.options.notifications,
|
||||
};
|
||||
this.loading = true;
|
||||
const importData = await this.importBackup(eventData);
|
||||
|
||||
this.$emit(IMPORT_EVENT, importData);
|
||||
this.loading = false;
|
||||
},
|
||||
async importBackup(data) {
|
||||
this.loading = true;
|
||||
const response = await api.backups.import(data.name, data);
|
||||
if (response) {
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,172 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot v-bind="{ open, close }"> </slot>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
:width="isMobile ? undefined : '65%'"
|
||||
:height="isMobile ? undefined : '0'"
|
||||
:fullscreen="isMobile"
|
||||
content-class="top-dialog"
|
||||
:scrollable="false"
|
||||
>
|
||||
<v-app-bar sticky dark color="primary lighten-1" :rounded="!isMobile">
|
||||
<FuseSearchBar :raw-data="allItems" @results="filterItems" :search="searchString">
|
||||
<v-text-field
|
||||
id="arrow-search"
|
||||
autofocus
|
||||
v-model="searchString"
|
||||
solo
|
||||
flat
|
||||
autocomplete="off"
|
||||
background-color="primary lighten-1"
|
||||
color="white"
|
||||
dense
|
||||
:clearable="!isMobile"
|
||||
class="mx-2 arrow-search"
|
||||
hide-details
|
||||
single-line
|
||||
:placeholder="$t('search.search')"
|
||||
:prepend-inner-icon="$globals.icons.search"
|
||||
>
|
||||
</v-text-field>
|
||||
</FuseSearchBar>
|
||||
<v-btn v-if="isMobile" x-small fab light @click="dialog = false">
|
||||
<v-icon>
|
||||
{{ $globals.icons.close }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
<v-card class="mt-1 pa-1" relative>
|
||||
<v-card-actions>
|
||||
<div class="mr-auto">
|
||||
{{ $t("search.results") }}
|
||||
</div>
|
||||
<router-link to="/search"> {{ $t("search.advanced-search") }} </router-link>
|
||||
</v-card-actions>
|
||||
<v-card-actions v-if="loading">
|
||||
<SiteLoader :loading="loading" />
|
||||
</v-card-actions>
|
||||
<div v-else>
|
||||
<MobileRecipeCard
|
||||
v-for="(recipe, index) in results.slice(0, 10)"
|
||||
:tabindex="index"
|
||||
:key="index"
|
||||
class="ma-1 arrow-nav"
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:route="true"
|
||||
v-on="$listeners.selected ? { selected: () => grabRecipe(recipe) } : {}"
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SiteLoader from "@/components/UI/SiteLoader";
|
||||
const SELECTED_EVENT = "selected";
|
||||
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar";
|
||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
FuseSearchBar,
|
||||
MobileRecipeCard,
|
||||
SiteLoader,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
selectedIndex: -1,
|
||||
dialog: false,
|
||||
searchString: "",
|
||||
searchResults: [],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.dialog = false;
|
||||
},
|
||||
async dialog(val) {
|
||||
if (!val) {
|
||||
this.resetSelected();
|
||||
} else if (this.allItems.length <= 0) {
|
||||
this.loading = true;
|
||||
await this.$store.dispatch("requestAllRecipes");
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("keydown", this.onUpDown);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("keydown", this.onUpDown);
|
||||
},
|
||||
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
allItems() {
|
||||
return this.$store.getters.getAllRecipes;
|
||||
},
|
||||
results() {
|
||||
if (this.searchString != null && this.searchString.length >= 1) {
|
||||
return this.searchResults;
|
||||
}
|
||||
return this.allItems;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
close() {
|
||||
this.dialog = false;
|
||||
},
|
||||
filterItems(val) {
|
||||
this.searchResults = val.map(x => x.item);
|
||||
},
|
||||
grabRecipe(recipe) {
|
||||
this.dialog = false;
|
||||
this.$emit(SELECTED_EVENT, recipe);
|
||||
},
|
||||
onUpDown(e) {
|
||||
if (e.keyCode === 38) {
|
||||
e.preventDefault();
|
||||
this.selectedIndex--;
|
||||
} else if (e.keyCode === 40) {
|
||||
e.preventDefault();
|
||||
this.selectedIndex++;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
this.selectRecipe();
|
||||
},
|
||||
resetSelected() {
|
||||
this.searchString = "";
|
||||
this.selectedIndex = -1;
|
||||
document.getElementsByClassName("arrow-nav")[0].focus();
|
||||
},
|
||||
selectRecipe() {
|
||||
const recipeCards = document.getElementsByClassName("arrow-nav");
|
||||
if (recipeCards) {
|
||||
if (this.selectedIndex < 0) {
|
||||
this.selectedIndex = -1;
|
||||
document.getElementById("arrow-search").focus();
|
||||
return;
|
||||
}
|
||||
this.selectedIndex >= recipeCards.length ? (this.selectedIndex = recipeCards.length - 1) : null;
|
||||
recipeCards[this.selectedIndex].focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style >
|
||||
</style>
|
|
@ -1,46 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center ma-2">
|
||||
<v-snackbar v-model="snackbar.open" top :color="snackbar.color" timeout="3500">
|
||||
<v-icon dark left>
|
||||
{{ icon }}
|
||||
</v-icon>
|
||||
|
||||
{{ snackbar.title }}
|
||||
{{ snackbar.text }}
|
||||
|
||||
<template v-slot:action="{ attrs }">
|
||||
<v-btn text v-bind="attrs" @click="snackbar.open = false">
|
||||
{{ $t("general.close") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({}),
|
||||
computed: {
|
||||
snackbar: {
|
||||
set(val) {
|
||||
this.$store.commit("setSnackbar", val);
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.getSnackbar;
|
||||
},
|
||||
},
|
||||
icon() {
|
||||
switch (this.snackbar.color) {
|
||||
case "error":
|
||||
return this.$globals.icons.alert;
|
||||
case "success":
|
||||
return this.$globals.icons.checkboxMarkedCircle;
|
||||
case "info":
|
||||
return this.$globals.icons.information;
|
||||
default:
|
||||
return this.$globals.icons.bellAlert;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,81 +0,0 @@
|
|||
<template>
|
||||
<div class="mt-2">
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
Log
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
class="ml-auto shrink mb-n7"
|
||||
solo
|
||||
:label="$t('about.log-lines')"
|
||||
type="number"
|
||||
:append-icon="$globals.icons.refreshCircle"
|
||||
v-model="lines"
|
||||
@click:append="getLogText"
|
||||
suffix="lines"
|
||||
single-line
|
||||
>
|
||||
</v-text-field>
|
||||
<TheDownloadBtn :button-text="$t('about.download-log')" download-url="/api/debug/log">
|
||||
<template v-slot:default="{ downloadFile }">
|
||||
<v-btn bottom right relative fab icon color="primary" @click="downloadFile">
|
||||
<v-icon> {{ $globals.icons.download }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</TheDownloadBtn>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<div v-for="(item, index) in splitText" :key="index" :class="getClass(item)">
|
||||
{{ item }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
components: { TheDownloadBtn },
|
||||
data() {
|
||||
return {
|
||||
lines: 200,
|
||||
text: "",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getLogText();
|
||||
},
|
||||
computed: {
|
||||
splitText() {
|
||||
return this.text.split("/n");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getLogText() {
|
||||
this.text = await api.meta.getLogText(this.lines);
|
||||
},
|
||||
getClass(text) {
|
||||
const isError = text.includes("ERROR:");
|
||||
if (isError) {
|
||||
return "log--error";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-text {
|
||||
background-color: #e0e0e077;
|
||||
}
|
||||
.log--error {
|
||||
color: #ef5350;
|
||||
}
|
||||
.line-number {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
|
@ -1,80 +0,0 @@
|
|||
<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>
|
||||
div {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,65 +0,0 @@
|
|||
<template>
|
||||
<SearchDialog ref="searchDialog">
|
||||
<template v-slot="{ open }">
|
||||
<v-text-field
|
||||
readonly
|
||||
@click="open"
|
||||
ref="searchInput"
|
||||
class="my-auto mt-5 pt-1"
|
||||
dense
|
||||
light
|
||||
dark
|
||||
flat
|
||||
:placeholder="$t('search.search-mealie')"
|
||||
background-color="primary lighten-1"
|
||||
color="white"
|
||||
solo=""
|
||||
:style="`max-width: 450;`"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon color="grey lighten-3" size="29">
|
||||
{{ $globals.icons.search }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</template>
|
||||
</SearchDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchDialog from "@/components/UI/Dialogs/SearchDialog";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SearchDialog,
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener("keydown", this.onDocumentKeydown);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("keydown", this.onDocumentKeydown);
|
||||
},
|
||||
|
||||
methods: {
|
||||
highlight(string) {
|
||||
if (!this.search) {
|
||||
return string;
|
||||
}
|
||||
return string.replace(new RegExp(this.search, "gi"), match => `<mark>${match}</mark>`);
|
||||
},
|
||||
|
||||
onDocumentKeydown(e) {
|
||||
if (
|
||||
e.key === "/" &&
|
||||
e.target !== this.$refs.searchInput.$refs.input &&
|
||||
!document.activeElement.id.startsWith("input")
|
||||
) {
|
||||
e.preventDefault();
|
||||
this.$refs.searchDialog.open();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center ">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
:width="isMobile ? undefined : '600'"
|
||||
:height="isMobile ? undefined : '0'"
|
||||
:fullscreen="isMobile"
|
||||
content-class="top-dialog"
|
||||
>
|
||||
<v-card relative>
|
||||
<v-app-bar dark color="primary lighten-1" rounded="0">
|
||||
<SearchBar
|
||||
ref="mealSearchBar"
|
||||
@results="updateResults"
|
||||
@selected="emitSelect"
|
||||
:show-results="!isMobile"
|
||||
:dense="false"
|
||||
:nav-on-click="false"
|
||||
:autofocus="true"
|
||||
/>
|
||||
</v-app-bar>
|
||||
<v-card-text v-if="isMobile">
|
||||
<div v-for="recipe in searchResults.slice(0, 7)" :key="recipe.name">
|
||||
<MobileRecipeCard
|
||||
class="ma-1 px-0"
|
||||
:name="recipe.item.name"
|
||||
:description="recipe.item.description"
|
||||
:slug="recipe.item.slug"
|
||||
:rating="recipe.item.rating"
|
||||
:image="recipe.item.image"
|
||||
:route="true"
|
||||
@selected="dialog = false"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-btn v-if="isMobile" fab bottom @click="dialog = false" class="ma-2">
|
||||
<v-icon> {{ $globals.icons.close }} </v-icon>
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchBar from "./SearchBar";
|
||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
MobileRecipeCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchResults: [],
|
||||
dialog: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"$route.hash"(newHash, oldHash) {
|
||||
if (newHash === "#mobile-search") {
|
||||
this.dialog = true;
|
||||
} else if (oldHash === "#mobile-search") {
|
||||
this.dialog = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateResults(results) {
|
||||
this.searchResults = results;
|
||||
},
|
||||
emitSelect(slug, name) {
|
||||
this.$emit("select", name, slug);
|
||||
this.dialog = false;
|
||||
},
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
toggleDialog(open) {
|
||||
if (open) {
|
||||
this.$router.push("#search");
|
||||
} else {
|
||||
this.$router.back(); // 😎 back button click
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scope>
|
||||
.mobile-dialog {
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.top-dialog {
|
||||
align-self: flex-start;
|
||||
}
|
||||
</style>
|
|
@ -1,103 +0,0 @@
|
|||
w<template>
|
||||
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
|
||||
<div class="d-flex grow flex-wrap">
|
||||
<slot name="avatar">
|
||||
<v-sheet
|
||||
:color="color"
|
||||
:max-height="icon ? 90 : undefined"
|
||||
:width="icon ? 'auto' : '100%'"
|
||||
elevation="6"
|
||||
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
|
||||
dark
|
||||
>
|
||||
<v-icon v-if="icon" size="40" v-text="icon" />
|
||||
<div v-if="text" class="headline font-weight-thin" v-text="text" />
|
||||
</v-sheet>
|
||||
</slot>
|
||||
|
||||
<div v-if="$slots['after-heading']" class="ml-auto">
|
||||
<slot name="after-heading" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
|
||||
<template v-if="$slots.actions">
|
||||
<v-divider class="mt-2" />
|
||||
|
||||
<v-card-actions class="pb-0">
|
||||
<slot name="actions" />
|
||||
</v-card-actions>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.bottom">
|
||||
<v-divider class="mt-2" v-if="!$slots.actions" />
|
||||
|
||||
<div class="pb-0">
|
||||
<slot name="bottom" />
|
||||
</div>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MaterialCard",
|
||||
|
||||
props: {
|
||||
avatar: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
image: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
classes() {
|
||||
return {
|
||||
"v-card--material--has-heading": this.hasHeading,
|
||||
"mt-3": this.$vuetify.breakpoint.name == "xs" || this.$vuetify.breakpoint.name == "sm"
|
||||
};
|
||||
},
|
||||
hasHeading() {
|
||||
return false;
|
||||
},
|
||||
hasAltHeading() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
.v-card--material
|
||||
&__avatar
|
||||
position: relative
|
||||
top: -64px
|
||||
margin-bottom: -32px
|
||||
|
||||
&__heading
|
||||
position: relative
|
||||
top: -40px
|
||||
transition: .3s ease
|
||||
z-index: 1
|
||||
</style>
|
|
@ -1,102 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<TheSidebar ref="theSidebar" />
|
||||
<v-app-bar clipped-left dense app color="primary" dark class="d-print-none" :bottom="isMobile">
|
||||
<v-btn icon @click="openSidebar">
|
||||
<v-icon> {{ $globals.icons.menu }}</v-icon>
|
||||
</v-btn>
|
||||
<router-link to="/">
|
||||
<v-btn icon>
|
||||
<v-icon size="40"> {{ $globals.icons.primary }} </v-icon>
|
||||
</v-btn>
|
||||
</router-link>
|
||||
|
||||
<div v-if="!isMobile" btn class="pl-2">
|
||||
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')">
|
||||
Mealie
|
||||
</v-toolbar-title>
|
||||
</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon class="mr-1" small v-bind="attrs" v-on="on" @click="isDark = !isDark">
|
||||
<v-icon v-text="isDark ? $globals.icons.weatherSunny : $globals.icons.weatherNight"> </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ isDark ? $t("settings.theme.switch-to-light-mode") : $t("settings.theme.switch-to-dark-mode") }}</span>
|
||||
</v-tooltip>
|
||||
<div v-if="!isMobile" style="width: 350px;">
|
||||
<SearchBar :show-results="true" @selected="navigateFromSearch" :max-width="isMobile ? '100%' : '450px'" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-btn icon @click="$refs.recipeSearch.open()">
|
||||
<v-icon> {{ $globals.icons.search }} </v-icon>
|
||||
</v-btn>
|
||||
<SearchDialog ref="recipeSearch" />
|
||||
</div>
|
||||
|
||||
<TheSiteMenu />
|
||||
|
||||
<v-slide-x-reverse-transition>
|
||||
<TheRecipeFab v-if="loggedIn && isMobile" />
|
||||
</v-slide-x-reverse-transition>
|
||||
</v-app-bar>
|
||||
<v-slide-x-reverse-transition>
|
||||
<TheRecipeFab v-if="loggedIn && !isMobile" :absolute="true" />
|
||||
</v-slide-x-reverse-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TheSiteMenu from "@/components/UI/TheSiteMenu";
|
||||
import SearchBar from "@/components/UI/Search/SearchBar";
|
||||
import SearchDialog from "@/components/UI/Dialogs/SearchDialog";
|
||||
import TheRecipeFab from "@/components/UI/TheRecipeFab";
|
||||
import TheSidebar from "@/components/UI/TheSidebar";
|
||||
import { user } from "@/mixins/user";
|
||||
export default {
|
||||
name: "AppBar",
|
||||
|
||||
mixins: [user],
|
||||
components: {
|
||||
SearchDialog,
|
||||
TheRecipeFab,
|
||||
TheSidebar,
|
||||
TheSiteMenu,
|
||||
SearchBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSidebar: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
isDark: {
|
||||
get() {
|
||||
return this.$store.getters.getIsDark;
|
||||
},
|
||||
set() {
|
||||
let setVal = "dark";
|
||||
if (this.isDark) {
|
||||
setVal = "light";
|
||||
}
|
||||
this.$store.commit("setDarkMode", setVal);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
navigateFromSearch(slug) {
|
||||
this.$router.push(`/recipe/${slug}`);
|
||||
},
|
||||
openSidebar() {
|
||||
this.$refs.theSidebar.toggleSidebar();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,247 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center d-print-none">
|
||||
<v-dialog v-model="addRecipe" width="650" @click:outside="reset">
|
||||
<v-card :loading="processing">
|
||||
<v-app-bar dark color="primary mb-2">
|
||||
<v-icon large left v-if="!processing"> {{ $globals.icons.link }} </v-icon>
|
||||
<v-progress-circular v-else indeterminate color="white" large class="mr-2"> </v-progress-circular>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ $t("new-recipe.from-url") }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-form ref="urlForm" @submit.prevent="createRecipe">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="recipeURL"
|
||||
:label="$t('new-recipe.recipe-url')"
|
||||
required
|
||||
validate-on-blur
|
||||
autofocus
|
||||
class="mt-1"
|
||||
:rules="[isValidWebUrl]"
|
||||
:hint="$t('new-recipe.url-form-hint')"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
|
||||
<v-expand-transition>
|
||||
<v-alert v-if="error" color="error" class="mt-6 white--text">
|
||||
<v-card-title class="ma-0 pa-0">
|
||||
<v-icon left color="white" x-large> {{ $globals.icons.robot }} </v-icon>
|
||||
{{ $t("new-recipe.error-title") }}
|
||||
</v-card-title>
|
||||
<v-divider class="my-3 mx-2"></v-divider>
|
||||
|
||||
<p>
|
||||
{{ $t("new-recipe.error-details") }}
|
||||
</p>
|
||||
<div class="d-flex row justify-space-around my-3 force-white">
|
||||
<a
|
||||
class="dark"
|
||||
href="https://developers.google.com/search/docs/data-types/recipe"
|
||||
target="_blank"
|
||||
rel="noreferrer nofollow"
|
||||
>
|
||||
{{ $t("new-recipe.google-ld-json-info") }}
|
||||
</a>
|
||||
<a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow">
|
||||
{{ $t("new-recipe.github-issues") }}
|
||||
</a>
|
||||
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow">
|
||||
{{ $t("new-recipe.recipe-markup-specification") }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn
|
||||
white
|
||||
outlined
|
||||
:to="{ path: '/recipes/debugger', query: { test_url: recipeURL } }"
|
||||
@click="addRecipe = false"
|
||||
>
|
||||
<v-icon left> {{ $globals.icons.externalLink }} </v-icon>
|
||||
{{ $t("new-recipe.view-scraped-data") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn color="grey" text @click="reset">
|
||||
<v-icon left> {{ $globals.icons.close }}</v-icon>
|
||||
{{ $t("general.close") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" type="submit" :loading="processing">
|
||||
<v-icon left> {{ $globals.icons.create }} </v-icon>
|
||||
{{ $t("general.submit") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<BaseDialog
|
||||
:title="$t('new-recipe.upload-a-recipe')"
|
||||
:titleIcon="$globals.icons.zip"
|
||||
:submit-text="$t('general.import')"
|
||||
ref="uploadZipDialog"
|
||||
@submit="uploadZip"
|
||||
:loading="processing"
|
||||
>
|
||||
<v-card-text class="mt-1 pb-0">
|
||||
{{ $t("new-recipe.upload-individual-zip-file") }}
|
||||
|
||||
<div class="headline mx-auto mb-0 pb-0 text-center">
|
||||
{{ this.fileName }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<AppButtonUpload class="mx-auto" :text-btn="false" @uploaded="setFile" :post="false"> </AppButtonUpload>
|
||||
</v-card-actions>
|
||||
</BaseDialog>
|
||||
<v-speed-dial v-model="fab" :open-on-hover="absolute" :fixed="absolute" :bottom="absolute" :right="absolute">
|
||||
<template v-slot:activator>
|
||||
<v-btn v-model="fab" :color="absolute ? 'accent' : 'white'" dark :icon="!absolute" :fab="absolute">
|
||||
<v-icon> {{ $globals.icons.createAlt }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-tooltip left dark color="primary">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn fab dark small color="primary" v-bind="attrs" v-on="on" @click="addRecipe = true">
|
||||
<v-icon>{{ $globals.icons.link }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("new-recipe.from-url") }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip left dark color="accent">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn fab dark small color="accent" v-bind="attrs" v-on="on" @click="$router.push('/new')">
|
||||
<v-icon>{{ $globals.icons.edit }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.new") }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip left dark color="info">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn fab dark small color="info" v-bind="attrs" v-on="on" @click="openZipUploader">
|
||||
<v-icon>{{ $globals.icons.zip }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.upload") }}</span>
|
||||
</v-tooltip>
|
||||
</v-speed-dial>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import AppButtonUpload from "@/components/UI/Buttons/AppButtonUpload.vue";
|
||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog.vue";
|
||||
export default {
|
||||
components: {
|
||||
AppButtonUpload,
|
||||
BaseDialog,
|
||||
},
|
||||
props: {
|
||||
absolute: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: false,
|
||||
fab: false,
|
||||
addRecipe: false,
|
||||
processing: false,
|
||||
uploadData: {
|
||||
fileName: "archive",
|
||||
file: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.$route.query.recipe_import_url) {
|
||||
this.addRecipe = true;
|
||||
this.createRecipe();
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
recipeURL: {
|
||||
set(recipe_import_url) {
|
||||
this.$router.replace({ query: { ...this.$route.query, recipe_import_url } });
|
||||
},
|
||||
get() {
|
||||
return this.$route.query.recipe_import_url || "";
|
||||
},
|
||||
},
|
||||
fileName() {
|
||||
return this.uploadData.file?.name || "";
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetVars() {
|
||||
this.uploadData = {
|
||||
fileName: "archive",
|
||||
file: null,
|
||||
};
|
||||
},
|
||||
setFile(file) {
|
||||
this.uploadData.file = file;
|
||||
console.log("Uploaded");
|
||||
},
|
||||
openZipUploader() {
|
||||
this.resetVars();
|
||||
this.$refs.uploadZipDialog.open();
|
||||
},
|
||||
async uploadZip() {
|
||||
let formData = new FormData();
|
||||
formData.append(this.uploadData.fileName, this.uploadData.file);
|
||||
|
||||
const response = await api.utils.uploadFile("/api/recipes/create-from-zip", formData);
|
||||
|
||||
this.$router.push(`/recipe/${response.data.slug}`);
|
||||
},
|
||||
async createRecipe() {
|
||||
this.error = false;
|
||||
if (this.$refs.urlForm === undefined || this.$refs.urlForm.validate()) {
|
||||
this.processing = true;
|
||||
const response = await api.recipes.createByURL(this.recipeURL);
|
||||
this.processing = false;
|
||||
if (response) {
|
||||
this.addRecipe = false;
|
||||
this.recipeURL = "";
|
||||
this.$router.push(`/recipe/${response.data}`);
|
||||
} else {
|
||||
this.error = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.fab = false;
|
||||
this.error = false;
|
||||
this.addRecipe = false;
|
||||
this.recipeURL = "";
|
||||
this.processing = false;
|
||||
},
|
||||
isValidWebUrl(url) {
|
||||
let regEx =
|
||||
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,256}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/gm;
|
||||
return regEx.test(url) ? true : this.$t("new-recipe.must-be-a-valid-url");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.force-white > a {
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
|
@ -1,253 +0,0 @@
|
|||
<template>
|
||||
<div class="d-print-none no-print">
|
||||
<v-navigation-drawer v-model="showSidebar" width="180px" clipped app>
|
||||
<template v-slot:prepend>
|
||||
<UserAvatar v-if="isLoggedIn" :user="user" />
|
||||
|
||||
<v-list-item dense v-if="isLoggedIn" :to="`/user/${user.id}/favorites`">
|
||||
<v-list-item-icon>
|
||||
<v-icon> {{ $globals.icons.heart }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> {{ $t("general.favorites") }} </v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list nav dense>
|
||||
<v-list-item v-for="nav in effectiveMenu" :key="nav.title" link :to="nav.to">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<!-- Version List Item -->
|
||||
<v-list nav dense class="fixedBottom" v-if="!isMain">
|
||||
<v-list-item href="https://github.com/sponsors/hay-kot" target="_target">
|
||||
<v-list-item-icon>
|
||||
<v-icon color="pink"> {{ $globals.icons.heart }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title> {{ $t("about.support") }} </v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item to="/admin/about">
|
||||
<v-list-item-icon class="mr-3 pt-1">
|
||||
<v-icon :color="newVersionAvailable ? 'red--text' : ''"> {{ $globals.icons.information }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ $t("settings.current") }}
|
||||
{{ appVersion }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<a
|
||||
@click.prevent
|
||||
href="https://github.com/hay-kot/mealie/releases/latest"
|
||||
target="_blank"
|
||||
:class="newVersionAvailable ? 'red--text' : 'green--text'"
|
||||
>
|
||||
{{ $t("settings.latest") }}
|
||||
{{ latestVersion }}
|
||||
</a>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserAvatar from "@/components/User/UserAvatar";
|
||||
import { initials } from "@/mixins/initials";
|
||||
import { user } from "@/mixins/user";
|
||||
import axios from "axios";
|
||||
export default {
|
||||
components: {
|
||||
UserAvatar,
|
||||
},
|
||||
mixins: [initials, user],
|
||||
data() {
|
||||
return {
|
||||
showSidebar: false,
|
||||
links: [],
|
||||
|
||||
latestVersion: null,
|
||||
hideImage: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.showSidebar = !this.isMobile;
|
||||
},
|
||||
|
||||
watch: {
|
||||
user() {
|
||||
this.hideImage = false;
|
||||
},
|
||||
isMain(val) {
|
||||
if (val) {
|
||||
this.$store.dispatch("requestCustomPages");
|
||||
} else {
|
||||
this.getVersion();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isMain() {
|
||||
const testVal = this.$route.path.split("/");
|
||||
if (testVal[1] === "recipe") this.closeSidebar();
|
||||
|
||||
return !(testVal[1] === "admin");
|
||||
},
|
||||
baseMainLinks() {
|
||||
return [
|
||||
{
|
||||
icon: this.$globals.icons.home,
|
||||
to: "/",
|
||||
title: this.$t("sidebar.home-page"),
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.search,
|
||||
to: "/search",
|
||||
title: this.$t("sidebar.search"),
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.viewModule,
|
||||
to: "/recipes/all",
|
||||
title: this.$t("sidebar.all-recipes"),
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.tags,
|
||||
to: "/recipes/category",
|
||||
title: this.$t("sidebar.categories"),
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.tags,
|
||||
to: "/recipes/tag",
|
||||
title: this.$t("sidebar.tags"),
|
||||
},
|
||||
];
|
||||
},
|
||||
customPages() {
|
||||
const pages = this.$store.getters.getCustomPages;
|
||||
if (pages.length > 0) {
|
||||
pages.sort((a, b) => a.position - b.position);
|
||||
return pages.map(x => ({
|
||||
title: x.name,
|
||||
to: `/pages/${x.slug}`,
|
||||
icon: this.$globals.icons.pages,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
mainMenu() {
|
||||
return [...this.baseMainLinks, ...this.customPages];
|
||||
},
|
||||
settingsLinks() {
|
||||
return [
|
||||
{
|
||||
icon: this.$globals.icons.user,
|
||||
to: "/admin/profile",
|
||||
title: this.$t("sidebar.profile"),
|
||||
},
|
||||
];
|
||||
},
|
||||
adminLinks() {
|
||||
return [
|
||||
{
|
||||
icon: this.$globals.icons.viewDashboard,
|
||||
to: "/admin/dashboard",
|
||||
title: this.$t("sidebar.dashboard"),
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.cog,
|
||||
to: "/admin/settings",
|
||||
title: this.$t("sidebar.site-settings"),
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.tools,
|
||||
to: "/admin/toolbox",
|
||||
title: this.$t("sidebar.toolbox"),
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.group,
|
||||
to: "/admin/manage-users",
|
||||
title: this.$t("sidebar.manage-users"),
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.import,
|
||||
to: "/admin/migrations",
|
||||
title: this.$t("sidebar.migrations"),
|
||||
},
|
||||
];
|
||||
},
|
||||
adminMenu() {
|
||||
if (this.user.admin) {
|
||||
return [...this.settingsLinks, ...this.adminLinks];
|
||||
} else {
|
||||
return this.settingsLinks;
|
||||
}
|
||||
},
|
||||
effectiveMenu() {
|
||||
return this.isMain ? this.mainMenu : this.adminMenu;
|
||||
},
|
||||
userProfileImage() {
|
||||
this.resetImage();
|
||||
return `api/users/${this.user.id}/image`;
|
||||
},
|
||||
newVersionAvailable() {
|
||||
return this.latestVersion == this.appVersion ? false : true;
|
||||
},
|
||||
appVersion() {
|
||||
const appInfo = this.$store.getters.getAppInfo;
|
||||
return appInfo.version;
|
||||
},
|
||||
isLoggedIn() {
|
||||
return this.$store.getters.getIsLoggedIn;
|
||||
},
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetImage() {
|
||||
this.hideImage == false;
|
||||
},
|
||||
|
||||
toggleSidebar() {
|
||||
this.showSidebar = !this.showSidebar;
|
||||
},
|
||||
closeSidebar() {
|
||||
this.showSidebar = false;
|
||||
},
|
||||
async getVersion() {
|
||||
let response = await axios.get("https://api.github.com/repos/hay-kot/mealie/releases/latest", {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: null,
|
||||
},
|
||||
});
|
||||
|
||||
this.latestVersion = response.data.tag_name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.fixedBottom {
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,109 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<LoginDialog ref="loginDialog" />
|
||||
<v-menu transition="slide-x-transition" bottom right offset-y offset-overflow open-on-hover close-delay="200">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" icon>
|
||||
<v-icon>{{ $globals.icons.user }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(item, i) in filteredItems"
|
||||
:key="i"
|
||||
link
|
||||
:to="item.nav ? item.nav : null"
|
||||
@click="item.login ? openLoginDialog() : null"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ item.title }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoginDialog from "../Login/LoginDialog";
|
||||
export default {
|
||||
components: {
|
||||
LoginDialog,
|
||||
},
|
||||
computed: {
|
||||
items() {
|
||||
return [
|
||||
{
|
||||
icon: this.$globals.icons.user,
|
||||
title: this.$t("user.login"),
|
||||
restricted: false,
|
||||
login: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.calendarWeek,
|
||||
title: this.$t("meal-plan.dinner-this-week"),
|
||||
nav: "/meal-plan/this-week",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.calendarToday,
|
||||
title: this.$t("meal-plan.dinner-today"),
|
||||
nav: "/meal-plan/today",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.calendarMultiselect,
|
||||
title: this.$t("meal-plan.planner"),
|
||||
nav: "/meal-plan/planner",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.formatListCheck,
|
||||
title: this.$t("shopping-list.shopping-lists"),
|
||||
nav: "/shopping-list",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.logout,
|
||||
title: this.$t("user.logout"),
|
||||
restricted: true,
|
||||
nav: "/logout",
|
||||
},
|
||||
{
|
||||
icon: this.$globals.icons.cog,
|
||||
title: this.$t("general.settings"),
|
||||
nav: "/admin",
|
||||
restricted: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
filteredItems() {
|
||||
if (this.loggedIn) {
|
||||
return this.items.filter(x => x.restricted == true);
|
||||
} else {
|
||||
return this.items.filter(x => x.restricted == false);
|
||||
}
|
||||
},
|
||||
loggedIn() {
|
||||
return this.$store.getters.getIsLoggedIn;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
openLoginDialog() {
|
||||
this.$refs.loginDialog.open();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.menu-text {
|
||||
text-align: left !important;
|
||||
}
|
||||
</style>
|
|
@ -1,38 +0,0 @@
|
|||
import Vue from "vue";
|
||||
import VueI18n from "vue-i18n";
|
||||
import Vuetify from "@/plugins/vuetify";
|
||||
import axios from 'axios';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
const i18n = new VueI18n();
|
||||
|
||||
export default i18n;
|
||||
|
||||
const loadedLanguages = [];
|
||||
|
||||
function setI18nLanguage (lang) {
|
||||
i18n.locale = lang;
|
||||
Vuetify.framework.lang.current = lang;
|
||||
axios.defaults.headers.common['Accept-Language'] = lang
|
||||
document.querySelector('html').setAttribute('lang', lang)
|
||||
return lang
|
||||
}
|
||||
|
||||
export function loadLanguageAsync(lang) {
|
||||
|
||||
if ( ! loadedLanguages.includes(lang)) {
|
||||
const messages = import(`./locales/messages/${lang}.json`);
|
||||
const dateTimeFormats = import(`./locales/dateTimeFormats/${lang}.json`);
|
||||
|
||||
return Promise.all([messages, dateTimeFormats]).then(
|
||||
values => {
|
||||
i18n.setLocaleMessage(lang, values[0].default)
|
||||
i18n.setDateTimeFormat(lang, values[1].default)
|
||||
loadedLanguages.push(lang)
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
)
|
||||
}
|
||||
return Promise.resolve(setI18nLanguage(lang))
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import Vue from "vue";
|
||||
import VueCompositionAPI from "@vue/composition-api";
|
||||
Vue.use(VueCompositionAPI);
|
|
@ -1,44 +0,0 @@
|
|||
import "./installCompAPI"; // Must Be First
|
||||
|
||||
import Vue from "vue";
|
||||
import App from "./App.vue";
|
||||
import vuetify from "./plugins/vuetify";
|
||||
import store from "./store";
|
||||
import VueRouter from "vue-router";
|
||||
import { router } from "./routes";
|
||||
import { globals } from "@/utils/globals";
|
||||
import i18n from "./i18n";
|
||||
import "typeface-roboto/index.css";
|
||||
import "./registerServiceWorker";
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
Vue.use(VueRouter);
|
||||
Vue.component("TheButton", () => import("@/components/UI/Buttons/TheButton.vue"));
|
||||
|
||||
Vue.prototype.$globals = globals;
|
||||
|
||||
const vueApp = new Vue({
|
||||
vuetify,
|
||||
store,
|
||||
router,
|
||||
i18n,
|
||||
render: h => h(App),
|
||||
}).$mount("#app");
|
||||
|
||||
// Truncate
|
||||
const truncate = function(text, length, clamp) {
|
||||
clamp = clamp || "...";
|
||||
let node = document.createElement("div");
|
||||
node.innerHTML = text;
|
||||
let content = node.textContent;
|
||||
return content.length > length ? content.slice(0, length) + clamp : content;
|
||||
};
|
||||
|
||||
const titleCase = function(value) {
|
||||
return value.replace(/(?:^|\s|-)\S/g, x => x.toUpperCase());
|
||||
};
|
||||
|
||||
Vue.filter("truncate", truncate);
|
||||
Vue.filter("titleCase", titleCase);
|
||||
|
||||
export { router, vueApp };
|
|
@ -1,18 +0,0 @@
|
|||
export const initials = {
|
||||
computed: {
|
||||
initials() {
|
||||
if (!this.user.fullName) return "00"
|
||||
const allNames = this.user.fullName.trim().split(" ");
|
||||
const initials = allNames.reduce(
|
||||
(acc, curr, index) => {
|
||||
if (index === 0 || index === allNames.length - 1) {
|
||||
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[""]
|
||||
);
|
||||
return initials;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
import { store } from "@/store";
|
||||
export const user = {
|
||||
computed: {
|
||||
user() {
|
||||
return store.getters.getUserData;
|
||||
},
|
||||
loggedIn() {
|
||||
return store.getters.getIsLoggedIn;
|
||||
},
|
||||
initials() {
|
||||
const allNames = this.user.fullName.trim().split(" ");
|
||||
const initials = allNames.reduce(
|
||||
(acc, curr, index) => {
|
||||
if (index === 0 || index === allNames.length - 1) {
|
||||
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[""]
|
||||
);
|
||||
return initials;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
export const utilMixins = {
|
||||
commputed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
export const validators = {
|
||||
data() {
|
||||
return {
|
||||
emailRule: v => !v || EMAIL_REGEX.test(v) || this.$t("user.e-mail-must-be-valid"),
|
||||
|
||||
existsRule: value => !!value || this.$t("general.field-required"),
|
||||
|
||||
minRule: v => v.length >= 8 || this.$t("user.use-8-characters-or-more-for-your-password"),
|
||||
|
||||
whiteSpace: v => !v || v.split(" ").length <= 1 || this.$t("recipe.no-white-space-allowed"),
|
||||
};
|
||||
},
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
<template>
|
||||
<v-container class="text-center">
|
||||
<The404 />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import The404 from "@/components/Fallbacks/The404";
|
||||
export default {
|
||||
components: { The404 },
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-app-bar color="primary">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn href="https://github.com/sponsors/hay-kot" target="_blank" class="mx-1" color="secondary">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.heart }}
|
||||
</v-icon>
|
||||
{{ $t("about.support") }}
|
||||
</v-btn>
|
||||
<v-btn href="https://github.com/hay-kot" target="_blank" class="mx-1" color="secondary">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.github }}
|
||||
</v-icon>
|
||||
{{ $t("about.github") }}
|
||||
</v-btn>
|
||||
<v-btn href="https://hay-kot.dev" target="_blank" class="mx-1" color="secondary">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.account }}
|
||||
</v-icon>
|
||||
{{ $t("about.portfolio") }}
|
||||
</v-btn>
|
||||
<v-btn href="https://hay-kot.github.io/mealie/" target="_blank" class="mx-1" color="secondary">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.folderOutline }}
|
||||
</v-icon>
|
||||
{{ $t("about.docs") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card class="mt-3">
|
||||
<v-card-title class="headline">
|
||||
{{ $t("about.about-mealie") }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-list-item-group color="primary">
|
||||
<v-list-item v-for="property in prettyInfo" :key="property.name">
|
||||
<v-list-item-icon>
|
||||
<v-icon> {{ property.icon || $globals.icons.user }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="pl-4 flex row justify-space-between">
|
||||
<div>{{ property.name }}</div>
|
||||
<div>{{ property.value }}</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<TheDownloadBtn download-url="/api/debug/last-recipe-json">
|
||||
<template v-slot:default="{ downloadFile }">
|
||||
<v-btn color="primary" @click="downloadFile">
|
||||
<v-icon left> {{ $globals.icons.codeBraces }} </v-icon> {{ $t("about.download-recipe-json") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</TheDownloadBtn>
|
||||
</v-card-actions>
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
<LogCard />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn";
|
||||
import LogCard from "@/components/UI/LogCard.vue";
|
||||
export default {
|
||||
components: { TheDownloadBtn, LogCard },
|
||||
data() {
|
||||
return {
|
||||
prettyInfo: [],
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
await this.getInfo();
|
||||
},
|
||||
methods: {
|
||||
async getInfo() {
|
||||
const debugInfo = await api.meta.getDebugInfo();
|
||||
|
||||
this.prettyInfo = [
|
||||
{
|
||||
name: this.$t("about.version"),
|
||||
icon: this.$globals.icons.information,
|
||||
value: debugInfo.version,
|
||||
},
|
||||
{
|
||||
name: this.$t("about.application-mode"),
|
||||
icon: this.$globals.icons.devTo,
|
||||
value: debugInfo.production ? this.$t("about.production") : this.$t("about.development"),
|
||||
},
|
||||
{
|
||||
name: this.$t("about.demo-status"),
|
||||
icon: this.$globals.icons.testTube,
|
||||
value: debugInfo.demoStatus ? this.$t("about.demo") : this.$t("about.not-demo"),
|
||||
},
|
||||
{
|
||||
name: this.$t("about.api-port"),
|
||||
icon: this.$globals.icons.api,
|
||||
value: debugInfo.apiPort,
|
||||
},
|
||||
{
|
||||
name: this.$t("about.api-docs"),
|
||||
icon: this.$globals.icons.file,
|
||||
value: debugInfo.apiDocs ? this.$t("general.enabled") : this.$t("general.disabled"),
|
||||
},
|
||||
{
|
||||
name: this.$t("about.database-type"),
|
||||
icon: this.$globals.icons.database,
|
||||
value: debugInfo.dbType,
|
||||
},
|
||||
{
|
||||
name: this.$t("about.database-url"),
|
||||
icon: this.$globals.icons.database,
|
||||
value: debugInfo.dbUrl,
|
||||
},
|
||||
{
|
||||
name: this.$t("about.default-group"),
|
||||
icon: this.$globals.icons.group,
|
||||
value: debugInfo.defaultGroup,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,162 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<ImportSummaryDialog ref="report" />
|
||||
<ImportDialog
|
||||
:name="selectedName"
|
||||
:date="selectedDate"
|
||||
ref="import_dialog"
|
||||
@import="importBackup"
|
||||
@delete="deleteBackup"
|
||||
/>
|
||||
<ConfirmationDialog
|
||||
:title="$t('settings.backup.delete-backup')"
|
||||
:message="$t('general.confirm-delete-generic')"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
ref="deleteBackupConfirm"
|
||||
v-on:confirm="emitDelete()"
|
||||
/>
|
||||
<StatCard :icon="$globals.icons.backupRestore" :color="color">
|
||||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<h2 class="body-3 grey--text font-weight-light">
|
||||
{{ $t("settings.backup-and-exports") }}
|
||||
</h2>
|
||||
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
<small> {{ total }}</small>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div class="d-flex row py-3 justify-end">
|
||||
<AppButtonUpload url="/api/backups/upload" @uploaded="getAvailableBackups">
|
||||
<template v-slot="{ isSelecting, onButtonClick }">
|
||||
<v-btn :loading="isSelecting" class="mx-2" small color="info" @click="onButtonClick">
|
||||
<v-icon left> {{ $globals.icons.upload }} </v-icon> {{ $t("general.upload") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</AppButtonUpload>
|
||||
<BackupDialog :color="color" />
|
||||
|
||||
<v-btn :loading="loading" class="mx-2" small color="success" @click="createBackup">
|
||||
<v-icon left> {{ $globals.icons.create }} </v-icon> {{ $t("general.create") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<template v-slot:bottom>
|
||||
<v-virtual-scroll height="290" item-height="70" :items="availableBackups">
|
||||
<template v-slot:default="{ item }">
|
||||
<v-list-item @click.prevent="openDialog(item, btnEvent.IMPORT_EVENT)">
|
||||
<v-list-item-avatar>
|
||||
<v-icon large dark :color="color">
|
||||
{{ $globals.icons.database }}
|
||||
</v-icon>
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle>
|
||||
{{ $d(Date.parse(item.date), "medium") }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-action class="ml-auto">
|
||||
<v-btn large icon @click.stop="openDialog(item, btnEvent.DELETE_EVENT)">
|
||||
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</template>
|
||||
</StatCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppButtonUpload from "@/components/UI/Buttons/AppButtonUpload";
|
||||
import ImportSummaryDialog from "@/components/ImportSummaryDialog";
|
||||
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
|
||||
import { api } from "@/api";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
import BackupDialog from "@/components/UI/Dialogs/BackupDialog";
|
||||
import ImportDialog from "@/components/UI/Dialogs/ImportDialog";
|
||||
const IMPORT_EVENT = "import";
|
||||
const DELETE_EVENT = "delete";
|
||||
|
||||
export default {
|
||||
components: { StatCard, ImportDialog, AppButtonUpload, ImportSummaryDialog, BackupDialog, ConfirmationDialog },
|
||||
data() {
|
||||
return {
|
||||
color: "accent",
|
||||
selectedName: "",
|
||||
selectedDate: "",
|
||||
loading: false,
|
||||
events: [],
|
||||
availableBackups: [],
|
||||
btnEvent: { IMPORT_EVENT, DELETE_EVENT },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
return this.availableBackups.length;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getAvailableBackups();
|
||||
},
|
||||
methods: {
|
||||
async getAvailableBackups() {
|
||||
const response = await api.backups.requestAvailable();
|
||||
this.availableBackups = response.imports;
|
||||
},
|
||||
|
||||
async deleteBackup(name) {
|
||||
this.loading = true;
|
||||
await api.backups.delete(name);
|
||||
this.loading = false;
|
||||
this.getAvailableBackups();
|
||||
},
|
||||
|
||||
openDialog(backup, event) {
|
||||
this.selectedDate = backup.date;
|
||||
this.selectedName = backup.name;
|
||||
|
||||
switch (event) {
|
||||
case IMPORT_EVENT:
|
||||
this.$refs.import_dialog.open();
|
||||
break;
|
||||
case DELETE_EVENT:
|
||||
this.$refs.deleteBackupConfirm.open();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
async importBackup(data) {
|
||||
this.$refs.report.open(data);
|
||||
},
|
||||
|
||||
async createBackup() {
|
||||
this.loading = true;
|
||||
|
||||
let data = {
|
||||
tag: this.tag,
|
||||
options: {},
|
||||
templates: [],
|
||||
};
|
||||
|
||||
if (await api.backups.create(data)) {
|
||||
this.getAvailableBackups();
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
emitDelete() {
|
||||
this.deleteBackup(this.selectedName);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -1,135 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<ConfirmationDialog
|
||||
:title="$t('events.delete-event')"
|
||||
:message="$t('general.confirm-delete-generic')"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
ref="deleteEventConfirm"
|
||||
v-on:confirm="emitDelete()"
|
||||
/>
|
||||
<StatCard :icon="$globals.icons.bellAlert" :color="color">
|
||||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<h2 class="body-3 grey--text font-weight-light">
|
||||
{{ $t("settings.events") }}
|
||||
</h2>
|
||||
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
<small> {{ total }} </small>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<div class="d-flex row py-3 justify-end">
|
||||
<v-btn class="mx-2" small color="error lighten-1" @click="deleteAll">
|
||||
<v-icon left> {{ $globals.icons.notificationClearAll }} </v-icon> {{ $t("general.clear") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<template v-slot:bottom>
|
||||
<v-virtual-scroll height="290" item-height="70" :items="events">
|
||||
<template v-slot:default="{ item }">
|
||||
<v-list-item>
|
||||
<v-list-item-avatar>
|
||||
<v-icon large dark :color="icons[item.category].color">
|
||||
{{ icons[item.category].icon }}
|
||||
</v-icon>
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.title"></v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle v-text="item.text"></v-list-item-subtitle>
|
||||
<v-list-item-subtitle>
|
||||
{{ $d(Date.parse(item.timeStamp), "long") }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-action class="ml-auto">
|
||||
<v-btn large icon @click="openDialog(item)">
|
||||
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</template>
|
||||
</StatCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
|
||||
export default {
|
||||
components: { StatCard, ConfirmationDialog },
|
||||
data() {
|
||||
return {
|
||||
color: "accent",
|
||||
total: 0,
|
||||
selectedId: "",
|
||||
events: [],
|
||||
icons: {
|
||||
general: {
|
||||
icon: this.$globals.icons.information,
|
||||
color: "info",
|
||||
},
|
||||
recipe: {
|
||||
icon: this.$globals.icons.primary,
|
||||
color: "primary",
|
||||
},
|
||||
backup: {
|
||||
icon: this.$globals.icons.database,
|
||||
color: "primary",
|
||||
},
|
||||
schedule: {
|
||||
icon: this.$globals.icons.calendar,
|
||||
color: "primary",
|
||||
},
|
||||
migration: {
|
||||
icon: this.$globals.icons.backupRestore,
|
||||
color: "primary",
|
||||
},
|
||||
user: {
|
||||
icon: this.$globals.icons.user,
|
||||
color: "accent",
|
||||
},
|
||||
group: {
|
||||
icon: this.$globals.icons.group,
|
||||
color: "accent",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getEvents();
|
||||
},
|
||||
methods: {
|
||||
async getEvents() {
|
||||
const events = await api.about.getEvents();
|
||||
this.events = events.events;
|
||||
this.total = events.total;
|
||||
},
|
||||
async deleteEvent(id) {
|
||||
await api.about.deleteEvent(id);
|
||||
this.getEvents();
|
||||
},
|
||||
async deleteAll() {
|
||||
await api.about.deleteAllEvents();
|
||||
this.getEvents();
|
||||
},
|
||||
|
||||
openDialog(events) {
|
||||
this.selectedId = events.id;
|
||||
this.$refs.deleteEventConfirm.open();
|
||||
},
|
||||
|
||||
emitDelete() {
|
||||
this.deleteEvent(this.selectedId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -1,126 +0,0 @@
|
|||
<template>
|
||||
<div class="mt-10">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="4">
|
||||
<StatCard :icon="$globals.icons.primary">
|
||||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<h2 class="body-3 grey--text font-weight-light">
|
||||
{{ $t("general.recipes") }}
|
||||
</h2>
|
||||
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
<small> {{ statistics.totalRecipes }}</small>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<div class="d-flex row py-3 justify-space-around">
|
||||
<v-btn small color="primary" :to="{ path: '/admin/toolbox/', query: { tab: 'organize', filter: 'tag' } }">
|
||||
<v-icon left> {{ $globals.icons.tags }} </v-icon>
|
||||
{{ $tc("tag.untagged-count", [statistics.untaggedRecipes]) }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
small
|
||||
color="primary"
|
||||
:to="{ path: '/admin/toolbox/', query: { tab: 'organize', filter: 'category' } }"
|
||||
>
|
||||
<v-icon left> {{ $globals.icons.tags }} </v-icon>
|
||||
{{ $tc("category.uncategorized-count", [statistics.uncategorizedRecipes]) }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</StatCard>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="4">
|
||||
<StatCard :icon="$globals.icons.user">
|
||||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<h2 class="body-3 grey--text font-weight-light">
|
||||
{{ $t("user.users") }}
|
||||
</h2>
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
<small> {{ statistics.totalUsers }}</small>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<div class="ml-auto">
|
||||
<v-btn color="primary" small to="/admin/manage-users?tab=users">
|
||||
<v-icon left>{{ $globals.icons.user }}</v-icon>
|
||||
{{ $t("user.manage-users") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</StatCard>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="4">
|
||||
<StatCard :icon="$globals.icons.group">
|
||||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<h2 class="body-3 grey--text font-weight-light">
|
||||
{{ $t("group.groups") }}
|
||||
</h2>
|
||||
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
<small> {{ statistics.totalGroups }}</small>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<div class="ml-auto">
|
||||
<v-btn color="primary" small to="/admin/manage-users?tab=groups">
|
||||
<v-icon left>{{ $globals.icons.group }}</v-icon>
|
||||
{{ $t("group.manage-groups") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</StatCard>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-10">
|
||||
<v-col cols="12" sm="12" lg="6">
|
||||
<EventViewer />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" lg="6"> <BackupViewer /> </v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
import EventViewer from "./EventViewer";
|
||||
import BackupViewer from "./BackupViewer";
|
||||
export default {
|
||||
components: { StatCard, EventViewer, BackupViewer },
|
||||
data() {
|
||||
return {
|
||||
statistics: {
|
||||
totalGroups: 0,
|
||||
totalRecipes: 0,
|
||||
totalUsers: 0,
|
||||
uncategorizedRecipes: 0,
|
||||
untaggedRecipes: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getStatistics();
|
||||
},
|
||||
methods: {
|
||||
async getStatistics() {
|
||||
this.statistics = await api.meta.getStatistics();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.grid-style {
|
||||
flex-grow: inherit;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
|
@ -1,122 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<ConfirmationDialog
|
||||
ref="deleteGroupConfirm"
|
||||
:title="$t('group.confirm-group-deletion')"
|
||||
:message="
|
||||
$t('group.are-you-sure-you-want-to-delete-the-group', {
|
||||
groupName: group.name,
|
||||
})
|
||||
"
|
||||
:icon="$globals.icons.alert"
|
||||
@confirm="deleteGroup"
|
||||
:width="450"
|
||||
@close="closeGroupDelete"
|
||||
/>
|
||||
<v-card class="ma-auto" tile min-height="325px">
|
||||
<v-list dense>
|
||||
<v-card-title class="py-1">{{ group.name }}</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-subheader>{{ $t("group.group-id-with-value", { groupID: group.id }) }}</v-subheader>
|
||||
<v-list-item-group color="primary">
|
||||
<v-list-item v-for="property in groupProps" :key="property.text">
|
||||
<v-list-item-icon>
|
||||
<v-icon> {{ property.icon || $globals.icons.user }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="pl-4 flex row justify-space-between">
|
||||
<div>{{ property.text }}</div>
|
||||
<div>{{ property.value }}</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn small color="error" @click="confirmDelete" :disabled="ableToDelete">
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
<!-- Coming Soon! -->
|
||||
<v-btn small color="success" disabled>
|
||||
{{ $t("general.edit") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const RENDER_EVENT = "update";
|
||||
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
components: { ConfirmationDialog },
|
||||
props: {
|
||||
group: {
|
||||
default: {
|
||||
name: "DEFAULT_NAME",
|
||||
id: 1,
|
||||
users: [],
|
||||
mealplans: [],
|
||||
categories: [],
|
||||
webhookUrls: [],
|
||||
webhookTime: "00:00",
|
||||
webhookEnable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groupProps: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ableToDelete() {
|
||||
return this.group.users.length >= 1 ? true : false;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.buildData();
|
||||
},
|
||||
methods: {
|
||||
confirmDelete() {
|
||||
this.$refs.deleteGroupConfirm.open();
|
||||
},
|
||||
async deleteGroup() {
|
||||
if (await api.groups.delete(this.group.id)) {
|
||||
this.$emit(RENDER_EVENT);
|
||||
}
|
||||
},
|
||||
closeGroupDelete() {
|
||||
console.log("Close Delete");
|
||||
},
|
||||
buildData() {
|
||||
this.groupProps = [
|
||||
{
|
||||
text: this.$t("user.total-users"),
|
||||
icon: this.$globals.icons.user,
|
||||
value: this.group.users.length,
|
||||
},
|
||||
{
|
||||
text: this.$t("user.total-mealplans"),
|
||||
icon: this.$globals.icons.food,
|
||||
value: this.group.mealplans.length,
|
||||
},
|
||||
{
|
||||
text: this.$t("user.webhooks-enabled"),
|
||||
icon: this.$globals.icons.webhook,
|
||||
value: this.group.webhookEnable ? this.$t("general.yes") : this.$t("general.no"),
|
||||
},
|
||||
{
|
||||
text: this.$t("user.webhook-time"),
|
||||
icon: this.$globals.icons.clockOutline,
|
||||
value: this.group.webhookTime,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,100 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card outlined class="mt-n1">
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<div width="100px">
|
||||
<v-text-field
|
||||
v-model="filter"
|
||||
clearable
|
||||
class="mr-2 pt-0"
|
||||
:append-icon="$globals.icons.filter"
|
||||
:label="$t('general.filter')"
|
||||
single-line
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-dialog v-model="groupDialog" max-width="400">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn class="mx-2" small color="success" dark v-bind="attrs" v-on="on">
|
||||
{{ $t("group.create-group") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.group }}
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ $t("group.create-group") }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-form ref="newGroup" @submit.prevent="createGroup">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="newGroupName"
|
||||
:label="$t('group.group-name')"
|
||||
:rules="[existsRule]"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="groupDialog = false">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" type="submit">
|
||||
{{ $t("general.create") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-card-actions>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col :sm="6" :md="6" :lg="4" :xl="3" v-for="group in groups" :key="group.id">
|
||||
<GroupCard :group="group" @update="$store.dispatch('requestAllGroups')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { validators } from "@/mixins/validators";
|
||||
import { api } from "@/api";
|
||||
import GroupCard from "./GroupCard";
|
||||
export default {
|
||||
components: { GroupCard },
|
||||
mixins: [validators],
|
||||
data() {
|
||||
return {
|
||||
filter: "",
|
||||
groupDialog: false,
|
||||
newGroupName: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
groups() {
|
||||
return this.$store.getters.getGroups;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async createGroup() {
|
||||
this.groupLoading = true;
|
||||
if (await api.groups.create(this.newGroupName)) {
|
||||
this.groupDialog = false;
|
||||
this.$store.dispatch("requestAllGroups");
|
||||
}
|
||||
this.groupLoading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,230 +0,0 @@
|
|||
<template>
|
||||
<v-card outlined class="mt-n1">
|
||||
<ConfirmationDialog
|
||||
ref="deleteTokenDialog"
|
||||
:title="$t('user.confirm-link-deletion')"
|
||||
:message="
|
||||
$t('user.are-you-sure-you-want-to-delete-the-link', {
|
||||
link: activeName,
|
||||
})
|
||||
"
|
||||
:icon="$globals.icons.alert"
|
||||
@confirm="deleteToken"
|
||||
:width="450"
|
||||
@close="closeDelete"
|
||||
/>
|
||||
<v-toolbar flat>
|
||||
<v-icon large color="accent" class="mr-1">
|
||||
{{ $globals.icons.externalLink }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headine">
|
||||
{{ $t("signup.sign-up-links") }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer> </v-spacer>
|
||||
<v-dialog v-model="dialog" max-width="500">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn small color="success" dark v-bind="attrs" v-on="on">
|
||||
{{ $t("user.create-link") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.user }}
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ $t("user.create-link") }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-form ref="newUser" @submit.prevent="save">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="editedItem.name"
|
||||
:label="$t('user.link-name')"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
<v-checkbox v-model="editedItem.admin" :label="$t('user.admin')"></v-checkbox>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="close">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" type="submit">
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<v-data-table :headers="headers" :items="links" sort-by="calories">
|
||||
<template v-slot:item.token="{ item }">
|
||||
{{ `${baseURL}/sign-up/${item.token}` }}
|
||||
<AppCopyButton :copy-text="`${baseURL}/sign-up/${item.token}`" />
|
||||
</template>
|
||||
<template v-slot:item.admin="{ item }">
|
||||
<v-btn small :color="item.admin ? 'success' : 'error'" text>
|
||||
<v-icon small left>
|
||||
{{ $globals.icons.admin }}
|
||||
</v-icon>
|
||||
{{ item.admin ? $t("general.yes") : $t("general.no") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
|
||||
<v-icon small left>
|
||||
{{ $globals.icons.delete }}
|
||||
</v-icon>
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppCopyButton from "@/components/UI/Buttons/AppCopyButton";
|
||||
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
|
||||
import { api } from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
components: { ConfirmationDialog, AppCopyButton },
|
||||
mixins: [validators],
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
headers: [
|
||||
{
|
||||
text: this.$t("user.link-id"),
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: this.$t("general.name"), value: "name" },
|
||||
{ text: this.$t("general.token"), value: "token" },
|
||||
{ text: this.$t("user.admin"), value: "admin", align: "center" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
links: [],
|
||||
editedIndex: -1,
|
||||
editedItem: {
|
||||
name: "",
|
||||
admin: false,
|
||||
token: "",
|
||||
id: 0,
|
||||
},
|
||||
defaultItem: {
|
||||
name: "",
|
||||
token: "",
|
||||
admin: false,
|
||||
id: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
baseURL() {
|
||||
return window.location.origin;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close();
|
||||
},
|
||||
dialogDelete(val) {
|
||||
val || this.closeDelete();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateClipboard(newClip) {
|
||||
navigator.clipboard.writeText(newClip).then(
|
||||
() => {
|
||||
console.log("Copied", newClip);
|
||||
},
|
||||
() => {
|
||||
console.log("Copy Failed", newClip);
|
||||
}
|
||||
);
|
||||
},
|
||||
async initialize() {
|
||||
this.links = await api.signUps.getAll();
|
||||
},
|
||||
|
||||
async deleteToken() {
|
||||
if (await api.signUps.deleteToken(this.activeId)) {
|
||||
this.initialize();
|
||||
}
|
||||
},
|
||||
|
||||
editItem(item) {
|
||||
this.editedIndex = this.links.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
deleteItem(item) {
|
||||
this.activeId = item.token;
|
||||
this.activeName = item.name;
|
||||
this.editedIndex = this.links.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.$refs.deleteTokenDialog.open();
|
||||
},
|
||||
|
||||
deleteItemConfirm() {
|
||||
this.links.splice(this.editedIndex, 1);
|
||||
this.closeDelete();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
closeDelete() {
|
||||
this.dialogDelete = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
api.links.update(this.editedItem);
|
||||
this.close();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
api.signUps.createToken({
|
||||
name: this.editedItem.name,
|
||||
admin: this.editedItem.admin,
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
await this.initialize();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,284 +0,0 @@
|
|||
<template>
|
||||
<v-card outlined class="mt-n1">
|
||||
<ConfirmationDialog
|
||||
ref="deleteUserDialog"
|
||||
:title="$t('user.confirm-user-deletion')"
|
||||
:message="
|
||||
$t('user.are-you-sure-you-want-to-delete-the-user', {
|
||||
activeName,
|
||||
activeId,
|
||||
})
|
||||
"
|
||||
:icon="$globals.icons.alert"
|
||||
@confirm="deleteUser"
|
||||
:width="450"
|
||||
@close="closeDelete"
|
||||
/>
|
||||
<v-toolbar flat>
|
||||
<v-spacer> </v-spacer>
|
||||
<div width="100px">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
class="mr-2"
|
||||
:append-icon="$globals.icons.filter"
|
||||
:label="$t('general.filter')"
|
||||
single-line
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn small color="success" dark v-bind="attrs" v-on="on">
|
||||
{{ $t("user.create-user") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.user }}
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ formTitle }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-title class="headline" v-if="!this.createMode">
|
||||
{{ $t("user.user-id-with-value", { id: editedItem.id }) }}
|
||||
</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
<v-form ref="newUser" @submit.prevent="save">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.fullName"
|
||||
:label="$t('user.full-name')"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.email"
|
||||
:label="$t('user.email')"
|
||||
:rules="[existsRule, emailRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-select
|
||||
dense
|
||||
v-model="editedItem.group"
|
||||
:items="existingGroups"
|
||||
:label="$t('group.user-group')"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6" v-if="createMode">
|
||||
<v-text-field
|
||||
dense
|
||||
v-model="editedItem.password"
|
||||
:label="$t('user.user-password')"
|
||||
:rules="[existsRule, minRule]"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-switch v-model="editedItem.admin" :label="$t('user.admin')"></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn color="info" text @click="resetPassword" v-if="!createMode">
|
||||
{{ $t("user.reset-password") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="close">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" type="submit">
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-data-table :headers="headers" :items="users" sort-by="calories" :search="search">
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
|
||||
<v-icon small left>
|
||||
{{ $globals.icons.delete }}
|
||||
</v-icon>
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
<v-btn small color="success" @click="editItem(item)">
|
||||
<v-icon small left class="mr-2">
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
{{ $t("general.edit") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.admin="{ item }">
|
||||
{{ item.admin ? "Admin" : "User" }}
|
||||
</template>
|
||||
<template v-slot:no-data>
|
||||
<v-btn color="primary" @click="initialize">
|
||||
{{ $t("general.reset") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
|
||||
import { api } from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
components: { ConfirmationDialog },
|
||||
mixins: [validators],
|
||||
data() {
|
||||
return {
|
||||
search: "",
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
headers: [
|
||||
{
|
||||
text: this.$t("user.user-id"),
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: this.$t("user.username"), value: "username" },
|
||||
{ text: this.$t("user.full-name"), value: "fullName" },
|
||||
{ text: this.$t("user.email"), value: "email" },
|
||||
{ text: this.$t("group.group"), value: "group" },
|
||||
{ text: this.$t("user.admin"), value: "admin" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
users: [],
|
||||
editedIndex: -1,
|
||||
editedItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
group: "",
|
||||
admin: false,
|
||||
},
|
||||
defaultItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
group: "",
|
||||
admin: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
formTitle() {
|
||||
return this.createMode ? this.$t("user.new-user") : this.$t("user.edit-user");
|
||||
},
|
||||
createMode() {
|
||||
return this.editedIndex === -1 ? true : false;
|
||||
},
|
||||
existingGroups() {
|
||||
return this.$store.getters.getGroupNames;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close();
|
||||
},
|
||||
dialogDelete(val) {
|
||||
val || this.closeDelete();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.users = await api.users.allUsers();
|
||||
},
|
||||
|
||||
async deleteUser() {
|
||||
if (await api.users.delete(this.activeId)) {
|
||||
this.initialize();
|
||||
}
|
||||
},
|
||||
|
||||
editItem(item) {
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
deleteItem(item) {
|
||||
this.activeId = item.id;
|
||||
this.activeName = item.fullName;
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.$refs.deleteUserDialog.open();
|
||||
},
|
||||
|
||||
deleteItemConfirm() {
|
||||
this.users.splice(this.editedIndex, 1);
|
||||
this.closeDelete();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
closeDelete() {
|
||||
this.dialogDelete = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
this.updateUser();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
this.createUser();
|
||||
}
|
||||
await this.initialize();
|
||||
},
|
||||
resetPassword() {
|
||||
api.users.resetPassword(this.editedItem.id);
|
||||
},
|
||||
|
||||
async createUser() {
|
||||
if (await api.users.create(this.editedItem)) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
async updateUser() {
|
||||
if (await api.users.update(this.editedItem)) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,68 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card flat>
|
||||
<v-tabs v-model="tab" background-color="primary" centered dark icons-and-text show-arrows>
|
||||
<v-tabs-slider></v-tabs-slider>
|
||||
|
||||
<v-tab href="#users">
|
||||
{{ $t("user.users") }}
|
||||
<v-icon>{{ $globals.icons.user }}</v-icon>
|
||||
</v-tab>
|
||||
|
||||
<v-tab href="#sign-ups">
|
||||
{{ $t("signup.sign-up-links") }}
|
||||
<v-icon>{{ $globals.icons.accountPlusOutline }}</v-icon>
|
||||
</v-tab>
|
||||
|
||||
<v-tab href="#groups" @click="reqGroups">
|
||||
{{ $t("group.groups") }}
|
||||
<v-icon>{{ $globals.icons.group }}</v-icon>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-items v-model="tab">
|
||||
<v-tab-item value="users">
|
||||
<TheUserTable />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="sign-ups">
|
||||
<TheSignUpTable />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="groups">
|
||||
<GroupDashboard />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TheUserTable from "./TheUserTable";
|
||||
import GroupDashboard from "./GroupDashboard";
|
||||
import TheSignUpTable from "./TheSignUpTable";
|
||||
export default {
|
||||
components: { TheUserTable, GroupDashboard, TheSignUpTable },
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
tab: {
|
||||
set(tab) {
|
||||
this.$router.replace({ query: { ...this.$route.query, tab } });
|
||||
},
|
||||
get() {
|
||||
return this.$route.query.tab;
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.reqGroups();
|
||||
},
|
||||
methods: {
|
||||
reqGroups() {
|
||||
this.$store.dispatch("requestAllGroups");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,94 +0,0 @@
|
|||
<template>
|
||||
<v-card outlined class="my-2" :loading="loading">
|
||||
<MigrationDialog ref="migrationDialog" />
|
||||
<v-card-title>
|
||||
{{ title }}
|
||||
<v-spacer></v-spacer>
|
||||
<span>
|
||||
<AppButtonUpload
|
||||
class="mt-1"
|
||||
:url="`/api/migrations/${folder}/upload`"
|
||||
fileName="archive"
|
||||
@uploaded="$emit('refresh')"
|
||||
:post="true"
|
||||
/>
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-card-text> {{ description }}</v-card-text>
|
||||
<div v-if="available[0]">
|
||||
<v-card outlined v-for="migration in available" :key="migration.name" class="ma-2">
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="2">
|
||||
<v-icon large color="primary">{{ $globals.icons.import }}</v-icon>
|
||||
</v-col>
|
||||
<v-col cols="10">
|
||||
<div class="text-truncate">
|
||||
<strong>{{ migration.name }}</strong>
|
||||
</div>
|
||||
<div class="text-truncate">
|
||||
{{ $d(new Date(migration.date), "medium") }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-card-actions class="mt-n6">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" text @click="deleteMigration(migration.name)">
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
<v-btn color="accent" text @click="importMigration(migration.name)" :loading="loading" :disabled="loading">
|
||||
{{ $t("general.import") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-card outlined class="text-center ma-2">
|
||||
<v-card-text>
|
||||
{{ $t("migration.no-migration-data-available") }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
<br />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppButtonUpload from "@/components/UI/Buttons/AppButtonUpload";
|
||||
import { api } from "@/api";
|
||||
import MigrationDialog from "./MigrationDialog";
|
||||
export default {
|
||||
props: {
|
||||
folder: String,
|
||||
title: String,
|
||||
description: String,
|
||||
available: Array,
|
||||
},
|
||||
components: {
|
||||
AppButtonUpload,
|
||||
MigrationDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async deleteMigration(file_name) {
|
||||
if (await api.migrations.delete(this.folder, file_name)) {
|
||||
this.$emit("refresh");
|
||||
}
|
||||
},
|
||||
async importMigration(file_name) {
|
||||
this.loading = true;
|
||||
let response = await api.migrations.import(this.folder, file_name);
|
||||
this.$refs.migrationDialog.open(response);
|
||||
// this.$emit("imported", response.successful, response.failed);
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,106 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="70%">
|
||||
<v-card>
|
||||
<v-app-bar dark color="primary mb-2">
|
||||
<v-icon large left>
|
||||
{{ $globals.icons.import }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline">
|
||||
Migration Summary
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card-text class="mb-n4">
|
||||
<v-row>
|
||||
<div v-for="values in allNumbers" :key="values.title">
|
||||
<v-card-text>
|
||||
<div>
|
||||
<h3>{{ values.title }}</h3>
|
||||
</div>
|
||||
<div class="success--text">Success: {{ values.success }}</div>
|
||||
<div class="error--text">Failed: {{ values.failure }}</div>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-tabs v-model="tab" show-arrows="">
|
||||
<v-tab>{{ $t("general.recipes") }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-items v-model="tab">
|
||||
<v-tab-item v-for="(table, index) in allTables" :key="index">
|
||||
<v-card flat>
|
||||
<DataTable :data-headers="importHeaders" :data-set="table" />
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataTable from "@/components/ImportSummaryDialog/DataTable";
|
||||
export default {
|
||||
components: {
|
||||
DataTable,
|
||||
},
|
||||
data: () => ({
|
||||
tab: null,
|
||||
dialog: false,
|
||||
recipeData: [],
|
||||
themeData: [],
|
||||
settingsData: [],
|
||||
userData: [],
|
||||
groupData: [],
|
||||
pageData: [],
|
||||
importHeaders: [
|
||||
{
|
||||
text: "Status",
|
||||
value: "status",
|
||||
},
|
||||
{
|
||||
text: "Name",
|
||||
align: "start",
|
||||
sortable: true,
|
||||
value: "name",
|
||||
},
|
||||
|
||||
{ text: "Exception", value: "data-table-expand", align: "center" },
|
||||
],
|
||||
allDataTables: [],
|
||||
}),
|
||||
|
||||
computed: {
|
||||
recipeNumbers() {
|
||||
return this.calculateNumbers(this.$t("general.recipes"), this.recipeData);
|
||||
},
|
||||
allNumbers() {
|
||||
return [this.recipeNumbers];
|
||||
},
|
||||
allTables() {
|
||||
return [this.recipeData];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
calculateNumbers(title, list_array) {
|
||||
if (!list_array) return;
|
||||
let numbers = { title: title, success: 0, failure: 0 };
|
||||
list_array.forEach(element => {
|
||||
if (element.status) {
|
||||
numbers.success++;
|
||||
} else numbers.failure++;
|
||||
});
|
||||
return numbers;
|
||||
},
|
||||
open(importData) {
|
||||
this.recipeData = importData;
|
||||
|
||||
this.dialog = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card :loading="loading">
|
||||
<v-card-title class="headline">
|
||||
{{ $t("migration.recipe-migration") }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<v-col :cols="12" :sm="6" :md="6" :lg="4" :xl="3" v-for="migration in migrations" :key="migration.title">
|
||||
<MigrationCard
|
||||
:title="migration.title"
|
||||
:folder="migration.urlVariable"
|
||||
:description="migration.description"
|
||||
:available="migration.availableImports"
|
||||
@refresh="getAvailableMigrations"
|
||||
@imported="showReport"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MigrationCard from "./MigrationCard";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
components: {
|
||||
MigrationCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
success: [],
|
||||
failed: [],
|
||||
migrations: {
|
||||
nextcloud: {
|
||||
title: this.$t("migration.nextcloud.title"),
|
||||
description: this.$t("migration.nextcloud.description"),
|
||||
urlVariable: "nextcloud",
|
||||
availableImports: [],
|
||||
},
|
||||
chowdown: {
|
||||
title: this.$t("migration.chowdown.title"),
|
||||
description: this.$t("migration.chowdown.description"),
|
||||
urlVariable: "chowdown",
|
||||
availableImports: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getAvailableMigrations();
|
||||
},
|
||||
methods: {
|
||||
finished() {
|
||||
this.loading = false;
|
||||
this.$store.dispatch("requestRecentRecipes");
|
||||
},
|
||||
async getAvailableMigrations() {
|
||||
let response = await api.migrations.getMigrations();
|
||||
response.forEach(element => {
|
||||
if (element.type === "nextcloud") {
|
||||
this.migrations.nextcloud.availableImports = element.files;
|
||||
} else if (element.type === "chowdown") {
|
||||
this.migrations.chowdown.availableImports = element.files;
|
||||
}
|
||||
});
|
||||
},
|
||||
showReport(successful, failed) {
|
||||
this.success = successful;
|
||||
this.failed = failed;
|
||||
this.$refs.report.open();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,150 +0,0 @@
|
|||
<template>
|
||||
<StatCard :icon="$globals.icons.api" color="accent">
|
||||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<h2 class="body-3 grey--text font-weight-light">
|
||||
{{ $t("settings.token.api-tokens") }}
|
||||
</h2>
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
<small> {{ user.tokens.length }} </small>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:bottom>
|
||||
<v-subheader class="mb-n2">{{ $t("settings.token.active-tokens") }}</v-subheader>
|
||||
<v-virtual-scroll height="210" item-height="70" :items="user.tokens" class="mt-2">
|
||||
<template v-slot:default="{ item }">
|
||||
<v-divider></v-divider>
|
||||
<v-list-item @click.prevent>
|
||||
<v-list-item-avatar>
|
||||
<v-icon large dark color="accent">
|
||||
{{ $globals.icons.api }}
|
||||
</v-icon>
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-action class="ml-auto">
|
||||
<v-btn large icon @click.stop="deleteToken(item.id)">
|
||||
<v-icon color="accent">{{ $globals.icons.delete }}</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions class="pb-1 pt-3">
|
||||
<v-spacer></v-spacer>
|
||||
<BaseDialog
|
||||
:title="$t('settings.token.create-an-api-token')"
|
||||
:title-icon="$globals.icons.api"
|
||||
@submit="createToken"
|
||||
:submit-text="buttonText"
|
||||
:loading="loading"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-form ref="newTokenForm">
|
||||
<v-text-field v-model="name" :label="$t('settings.token.token-name')" required> </v-text-field>
|
||||
</v-form>
|
||||
|
||||
<div v-if="createdToken != ''">
|
||||
<v-textarea
|
||||
class="mb-0 pb-0"
|
||||
:label="$t('settings.token.api-token')"
|
||||
readonly
|
||||
v-model="createdToken"
|
||||
:append-outer-icon="$globals.icons.contentCopy"
|
||||
@click:append-outer="copyToken"
|
||||
>
|
||||
</v-textarea>
|
||||
<v-subheader class="text-center">
|
||||
{{
|
||||
$t(
|
||||
"settings.token.copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again"
|
||||
)
|
||||
}}
|
||||
</v-subheader>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<template v-slot:open="{ open }">
|
||||
<TheButton create @click="open" />
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</v-card-actions>
|
||||
</template>
|
||||
</StatCard>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
import { api } from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
import { initials } from "@/mixins/initials";
|
||||
export default {
|
||||
components: {
|
||||
BaseDialog,
|
||||
StatCard,
|
||||
},
|
||||
mixins: [validators, initials],
|
||||
data() {
|
||||
return {
|
||||
name: "",
|
||||
loading: false,
|
||||
createdToken: "",
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$store.dispatch("requestUserData");
|
||||
},
|
||||
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.getters.getUserData;
|
||||
},
|
||||
buttonText() {
|
||||
if (this.createdToken === "") {
|
||||
return this.$t("general.create");
|
||||
} else {
|
||||
return this.$t("general.close");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async createToken() {
|
||||
if (this.loading === true) {
|
||||
this.loading = false;
|
||||
this.$store.dispatch("requestUserData");
|
||||
this.createdToken = "";
|
||||
this.name = "";
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
if (this.$refs.newTokenForm.validate()) {
|
||||
const response = await api.users.createAPIToken(this.name);
|
||||
this.createdToken = response.token;
|
||||
}
|
||||
},
|
||||
async deleteToken(id) {
|
||||
await api.users.deleteAPIToken(id);
|
||||
this.$store.dispatch("requestUserData");
|
||||
},
|
||||
copyToken() {
|
||||
const copyText = this.createdToken;
|
||||
navigator.clipboard.writeText(copyText).then(
|
||||
() => console.log("Copied", copyText),
|
||||
() => console.log("Copied Failed", copyText)
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,211 +0,0 @@
|
|||
<template>
|
||||
<StatCard :icon="$globals.icons.group">
|
||||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<div class="body-3 grey--text font-weight-light" v-text="$t('group.group')" />
|
||||
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
<small> {{ currentGroup.name }} </small>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:bottom>
|
||||
<div v-if="todaysMeal">
|
||||
<v-subheader>{{ $t("meal-plan.dinner-tonight") }}</v-subheader>
|
||||
<MobileRecipeCard
|
||||
:name="todaysMeal.name"
|
||||
:slug="todaysMeal.slug"
|
||||
:description="todaysMeal.description"
|
||||
:rating="todaysMeal.rating"
|
||||
:tags="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-subheader>{{ $t("user.users-header") }}</v-subheader>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-virtual-scroll v-if="currentGroup.users" :items="currentGroup.users" height="257" item-height="64">
|
||||
<template v-slot:default="{ item }">
|
||||
<v-list-item :key="item.id" @click.prevent>
|
||||
<v-list-item-action>
|
||||
<v-btn fab small depressed color="primary">
|
||||
{{ generateInitials(item.fullName) }}
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ item.fullName }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
|
||||
<div class="mt-3">
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
<v-icon x-large>
|
||||
{{ $globals.icons.food }}
|
||||
</v-icon>
|
||||
<small> {{ $t("meal-plan.mealplan-settings") }} </small>
|
||||
</h3>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-subheader>{{ $t("meal-plan.mealplan-categories") }}</v-subheader>
|
||||
<v-card-text class="mt-0 pt-0">
|
||||
{{ $t("meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans") }}
|
||||
</v-card-text>
|
||||
<CategoryTagSelector
|
||||
:solo="true"
|
||||
:dense="false"
|
||||
v-model="groupSettings.categories"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
/>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-subheader>{{ $t("settings.webhooks.webhooks-caps") }}</v-subheader>
|
||||
<v-card-text class="mt-0 pt-0">
|
||||
{{
|
||||
$t(
|
||||
"settings.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"
|
||||
)
|
||||
}}
|
||||
<strong>{{ groupSettings.webhookTime }}</strong>
|
||||
</v-card-text>
|
||||
<v-row dense class="flex align-center">
|
||||
<v-switch class="ml-5 mr-auto" v-model="groupSettings.webhookEnable" :label="$t('general.enabled')"></v-switch>
|
||||
<TimePickerDialog @save-time="saveTime" class="" />
|
||||
</v-row>
|
||||
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
:prepend-icon="$globals.icons.delete"
|
||||
v-for="(url, index) in groupSettings.webhookUrls"
|
||||
@click:prepend="removeWebhook(index)"
|
||||
:key="index"
|
||||
v-model="groupSettings.webhookUrls[index]"
|
||||
:label="$t('settings.webhooks.webhook-url')"
|
||||
></v-text-field>
|
||||
<v-card-actions class="pa-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn small color="success" @click="addWebhook">
|
||||
<v-icon left> {{ $globals.icons.webhook }} </v-icon>
|
||||
{{ $t("general.new") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions class="pb-0">
|
||||
<v-btn class="ma-2" color="info" @click="testWebhooks">
|
||||
<v-icon left> {{ $globals.icons.webhook }} </v-icon>
|
||||
{{ $t("settings.webhooks.test-webhooks") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<TheButton update @click="saveGroupSettings" />
|
||||
</v-card-actions>
|
||||
</template>
|
||||
</StatCard>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TimePickerDialog from "@/components/FormHelpers/TimePickerDialog";
|
||||
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||
import { validators } from "@/mixins/validators";
|
||||
import { initials } from "@/mixins/initials";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
components: {
|
||||
StatCard,
|
||||
MobileRecipeCard,
|
||||
CategoryTagSelector,
|
||||
TimePickerDialog,
|
||||
},
|
||||
mixins: [validators, initials],
|
||||
data() {
|
||||
return {
|
||||
todaysMeal: false,
|
||||
hideImage: false,
|
||||
passwordLoading: false,
|
||||
password: {
|
||||
current: "",
|
||||
newOne: "",
|
||||
newTwo: "",
|
||||
},
|
||||
groupSettings: {},
|
||||
showPassword: false,
|
||||
loading: false,
|
||||
user: {
|
||||
fullName: "",
|
||||
email: "",
|
||||
group: "",
|
||||
admin: false,
|
||||
id: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
userProfileImage() {
|
||||
this.resetImage();
|
||||
return `api/users/${this.user.id}/image`;
|
||||
},
|
||||
currentGroup() {
|
||||
return this.$store.getters.getCurrentGroup;
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.getTodaysMeal();
|
||||
await this.$store.dispatch("requestCurrentGroup");
|
||||
this.getSiteSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getTodaysMeal() {
|
||||
const response = await api.mealPlans.today();
|
||||
this.todaysMeal = response.data;
|
||||
},
|
||||
generateInitials(text) {
|
||||
const allNames = text.trim().split(" ");
|
||||
return allNames.reduce(
|
||||
(acc, curr, index) => {
|
||||
if (index === 0 || index === allNames.length - 1) {
|
||||
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[""]
|
||||
);
|
||||
},
|
||||
getSiteSettings() {
|
||||
this.groupSettings = this.$store.getters.getCurrentGroup;
|
||||
},
|
||||
saveTime(value) {
|
||||
this.groupSettings.webhookTime = value;
|
||||
},
|
||||
addWebhook() {
|
||||
this.groupSettings.webhookUrls.push(" ");
|
||||
},
|
||||
removeWebhook(index) {
|
||||
this.groupSettings.webhookUrls.splice(index, 1);
|
||||
},
|
||||
async saveGroupSettings() {
|
||||
if (await api.groups.update(this.groupSettings)) {
|
||||
await this.$store.dispatch("requestCurrentGroup");
|
||||
this.getSiteSettings();
|
||||
}
|
||||
},
|
||||
testWebhooks() {
|
||||
api.settings.testWebhooks();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -1,225 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<StatCard :icon="$globals.icons.formatColorFill" :color="color">
|
||||
<template v-slot:after-heading>
|
||||
<div class="ml-auto text-right">
|
||||
<div class="body-3 grey--text font-weight-light" v-text="$t('general.themes')" />
|
||||
|
||||
<h3 class="display-2 font-weight-light text--primary">
|
||||
<small> {{ selectedTheme.name }} </small>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:actions>
|
||||
<v-btn-toggle v-model="darkMode" color="primary " mandatory>
|
||||
<v-btn small value="system">
|
||||
<v-icon>{{ $globals.icons.desktopTowerMonitor }}</v-icon>
|
||||
<span class="ml-1" v-show="$vuetify.breakpoint.smAndUp">
|
||||
{{ $t("settings.theme.default-to-system") }}
|
||||
</span>
|
||||
</v-btn>
|
||||
|
||||
<v-btn small value="light">
|
||||
<v-icon>{{ $globals.icons.weatherSunny }}</v-icon>
|
||||
<span class="ml-1" v-show="$vuetify.breakpoint.smAndUp">
|
||||
{{ $t("settings.theme.light") }}
|
||||
</span>
|
||||
</v-btn>
|
||||
|
||||
<v-btn small value="dark">
|
||||
<v-icon>{{ $globals.icons.weatherNight }}</v-icon>
|
||||
<span class="ml-1" v-show="$vuetify.breakpoint.smAndUp">
|
||||
{{ $t("settings.theme.dark") }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</template>
|
||||
|
||||
<template v-slot:bottom>
|
||||
<v-virtual-scroll height="290" item-height="70" :items="availableThemes" class="mt-2">
|
||||
<template v-slot:default="{ item }">
|
||||
<v-divider></v-divider>
|
||||
<v-list-item @click="selectedTheme = item">
|
||||
<v-list-item-avatar>
|
||||
<v-icon large dark :color="item.colors.primary">
|
||||
{{ $globals.icons.formatColorFill }}
|
||||
</v-icon>
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item.name"></v-list-item-title>
|
||||
|
||||
<v-row flex align-center class="mt-2 justify-space-around px-4 pb-2">
|
||||
<v-sheet
|
||||
class="rounded flex mx-1"
|
||||
v-for="(item, index) in item.colors"
|
||||
:key="index"
|
||||
:color="item"
|
||||
height="20"
|
||||
>
|
||||
</v-sheet>
|
||||
</v-row>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-action class="ml-auto">
|
||||
<v-btn large icon @click.stop="editTheme(item)">
|
||||
<v-icon color="accent">{{ $globals.icons.edit }}</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<TheButton class="ml-auto mt-1 mb-n1" create @click="createTheme" />
|
||||
</v-card-actions>
|
||||
</template>
|
||||
</StatCard>
|
||||
<BaseDialog
|
||||
:loading="loading"
|
||||
:title="modalLabel.title"
|
||||
:title-icon="$globals.icons.formatColorFill"
|
||||
modal-width="700"
|
||||
ref="themeDialog"
|
||||
:submit-text="modalLabel.button"
|
||||
@submit="processSubmit"
|
||||
@delete="deleteTheme"
|
||||
>
|
||||
<v-card-text class="mt-3">
|
||||
<v-text-field
|
||||
:label="$t('settings.theme.theme-name')"
|
||||
v-model="defaultData.name"
|
||||
:rules="[rules.required]"
|
||||
:append-outer-icon="jsonEditor ? $globals.icons.formSelect : $globals.icons.codeBraces"
|
||||
@click:append-outer="jsonEditor = !jsonEditor"
|
||||
></v-text-field>
|
||||
<v-row dense dflex wrap justify-content-center v-if="defaultData.colors && !jsonEditor">
|
||||
<v-col cols="12" sm="6" v-for="(_, key) in defaultData.colors" :key="key">
|
||||
<ColorPickerDialog :button-text="labels[key]" v-model="defaultData.colors[key]" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<VJsoneditor @error="logError()" v-else v-model="defaultData" height="250px" :options="jsonEditorOptions" />
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import ColorPickerDialog from "@/components/FormHelpers/ColorPickerDialog";
|
||||
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
export default {
|
||||
components: {
|
||||
StatCard,
|
||||
BaseDialog,
|
||||
ColorPickerDialog,
|
||||
VJsoneditor: () => import(/* webpackChunkName: "json-editor" */ "v-jsoneditor"),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
jsonEditor: false,
|
||||
jsonEditorOptions: {
|
||||
mode: "code",
|
||||
search: false,
|
||||
mainMenuBar: false,
|
||||
},
|
||||
availableThemes: [],
|
||||
color: "accent",
|
||||
newTheme: false,
|
||||
loading: false,
|
||||
defaultData: {
|
||||
name: "",
|
||||
colors: {
|
||||
primary: "#E58325",
|
||||
accent: "#00457A",
|
||||
secondary: "#973542",
|
||||
success: "#43A047",
|
||||
info: "#4990BA",
|
||||
warning: "#FF4081",
|
||||
error: "#EF5350",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
required: val => !!val || this.$t("settings.theme.theme-name-is-required"),
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
labels() {
|
||||
return {
|
||||
primary: this.$t("settings.theme.primary"),
|
||||
secondary: this.$t("settings.theme.secondary"),
|
||||
accent: this.$t("settings.theme.accent"),
|
||||
success: this.$t("settings.theme.success"),
|
||||
info: this.$t("settings.theme.info"),
|
||||
warning: this.$t("settings.theme.warning"),
|
||||
error: this.$t("settings.theme.error"),
|
||||
};
|
||||
},
|
||||
modalLabel() {
|
||||
if (this.newTheme) {
|
||||
return {
|
||||
title: this.$t("settings.add-a-new-theme"),
|
||||
button: this.$t("general.create"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
title: "Update Theme",
|
||||
button: this.$t("general.update"),
|
||||
};
|
||||
}
|
||||
},
|
||||
selectedTheme: {
|
||||
set(val) {
|
||||
this.$store.commit("setTheme", val);
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.getActiveTheme;
|
||||
},
|
||||
},
|
||||
darkMode: {
|
||||
set(val) {
|
||||
this.$store.commit("setDarkMode", val);
|
||||
},
|
||||
get() {
|
||||
return this.$store.getters.getDarkMode;
|
||||
},
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.getAllThemes();
|
||||
},
|
||||
methods: {
|
||||
async getAllThemes() {
|
||||
this.availableThemes = await api.themes.requestAll();
|
||||
},
|
||||
editTheme(theme) {
|
||||
this.defaultData = theme;
|
||||
this.newTheme = false;
|
||||
this.$refs.themeDialog.open();
|
||||
},
|
||||
createTheme() {
|
||||
this.newTheme = true;
|
||||
this.$refs.themeDialog.open();
|
||||
},
|
||||
async processSubmit() {
|
||||
if (this.newTheme) {
|
||||
await api.themes.create(this.defaultData);
|
||||
} else {
|
||||
await api.themes.update(this.defaultData);
|
||||
}
|
||||
this.getAllThemes();
|
||||
},
|
||||
async deleteTheme() {
|
||||
await api.themes.delete(this.defaultData.id);
|
||||
this.getAllThemes();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|