mirror of
https://github.com/hay-kot/mealie.git
synced 2025-07-06 04:52:25 -07:00
Refactor/backend routers (#388)
* update router * update caddy file * setup depends in docker-fole * make changes for serving on subpath * set dev config * fix router signups * consolidate links * backup-functionality to dashboard * new user card * consolidate theme into profile * fix theme tests * fix pg tests * fix pg tests * remove unused import * mobile margin Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
be5ac7a17a
commit
c1370afb16
52 changed files with 878 additions and 1094 deletions
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
|
@ -1,28 +1,19 @@
|
||||||
{
|
{
|
||||||
"python.formatting.provider": "black",
|
"python.formatting.provider": "black",
|
||||||
"python.pythonPath": ".venv/bin/python3.9",
|
"python.pythonPath": ".venv/bin/python3.9",
|
||||||
"python.linting.pylintEnabled": true,
|
"python.linting.pylintEnabled": false,
|
||||||
"python.linting.enabled": true,
|
"python.linting.enabled": true,
|
||||||
"python.testing.unittestEnabled": false,
|
"python.testing.unittestEnabled": false,
|
||||||
"python.testing.nosetestsEnabled": false,
|
"python.testing.nosetestsEnabled": false,
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"python.testing.autoTestDiscoverOnSaveEnabled": false,
|
"python.testing.autoTestDiscoverOnSaveEnabled": false,
|
||||||
"python.testing.pytestArgs": ["tests"],
|
"python.testing.pytestArgs": ["tests"],
|
||||||
"cSpell.enableFiletypes": [
|
"cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"],
|
||||||
"!javascript",
|
|
||||||
"!python",
|
|
||||||
"!yaml"
|
|
||||||
],
|
|
||||||
"i18n-ally.localesPaths": "frontend/src/locales/messages",
|
"i18n-ally.localesPaths": "frontend/src/locales/messages",
|
||||||
"i18n-ally.sourceLanguage": "en-US",
|
"i18n-ally.sourceLanguage": "en-US",
|
||||||
"i18n-ally.enabledFrameworks": ["vue"],
|
"i18n-ally.enabledFrameworks": ["vue"],
|
||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
"cSpell.words": [
|
"cSpell.words": ["compression", "hkotel", "performant", "postgres", "webp"],
|
||||||
"compression",
|
"search.mode": "reuseEditor",
|
||||||
"hkotel",
|
"python.linting.flake8Enabled": true
|
||||||
"performant",
|
|
||||||
"postgres",
|
|
||||||
"webp"
|
|
||||||
],
|
|
||||||
"search.mode": "reuseEditor"
|
|
||||||
}
|
}
|
||||||
|
|
10
Caddyfile
10
Caddyfile
|
@ -6,11 +6,11 @@
|
||||||
:80 {
|
:80 {
|
||||||
@proxied path /api/* /docs /openapi.json
|
@proxied path /api/* /docs /openapi.json
|
||||||
|
|
||||||
root * /app/dist
|
encode gzip zstd
|
||||||
encode gzip
|
|
||||||
uri strip_suffix /
|
uri strip_suffix /
|
||||||
|
|
||||||
handle_path /api/recipes/media/* {
|
# Handles Recipe Images / Assets
|
||||||
|
handle_path /api/media/recipes/* {
|
||||||
root * /app/data/recipes/
|
root * /app/data/recipes/
|
||||||
file_server
|
file_server
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
handle {
|
handle {
|
||||||
try_files {path}.html {path} /
|
root * /app/dist
|
||||||
|
try_files {path}.html {path} /index.html
|
||||||
file_server
|
file_server
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
9
Caddyfile.dev
Normal file
9
Caddyfile.dev
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
admin off
|
||||||
|
}
|
||||||
|
|
||||||
|
localhost {
|
||||||
|
handle /mealie/* {
|
||||||
|
reverse_proxy http://127.0.0.1:9090
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ services:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- "postgres"
|
||||||
ports:
|
ports:
|
||||||
- 9090:80
|
- 9090:80
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -135,14 +135,18 @@ export const recipeAPI = {
|
||||||
},
|
},
|
||||||
|
|
||||||
recipeImage(recipeSlug) {
|
recipeImage(recipeSlug) {
|
||||||
return `/api/recipes/media/${recipeSlug}/images/original.webp`;
|
return `/api/media/recipes/${recipeSlug}/images/original.webp`;
|
||||||
},
|
},
|
||||||
|
|
||||||
recipeSmallImage(recipeSlug) {
|
recipeSmallImage(recipeSlug) {
|
||||||
return `/api/recipes/media/${recipeSlug}/images/min-original.webp`;
|
return `/api/media/recipes/${recipeSlug}/images/min-original.webp`;
|
||||||
},
|
},
|
||||||
|
|
||||||
recipeTinyImage(recipeSlug) {
|
recipeTinyImage(recipeSlug) {
|
||||||
return `/api/recipes/media/${recipeSlug}/images/tiny-original.webp`;
|
return `/api/media/recipes/${recipeSlug}/images/tiny-original.webp`;
|
||||||
|
},
|
||||||
|
|
||||||
|
recipeAssetPath(recipeSlug, assetName) {
|
||||||
|
return `api/media/recipes/${recipeSlug}/assets/${assetName}`;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,10 +6,10 @@ const prefix = baseURL + "themes";
|
||||||
|
|
||||||
const settingsURLs = {
|
const settingsURLs = {
|
||||||
allThemes: `${baseURL}themes`,
|
allThemes: `${baseURL}themes`,
|
||||||
specificTheme: themeName => `${prefix}/${themeName}`,
|
specificTheme: id => `${prefix}/${id}`,
|
||||||
createTheme: `${prefix}/create`,
|
createTheme: `${prefix}/create`,
|
||||||
updateTheme: themeName => `${prefix}/${themeName}`,
|
updateTheme: id => `${prefix}/${id}`,
|
||||||
deleteTheme: themeName => `${prefix}/${themeName}`,
|
deleteTheme: id => `${prefix}/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themeAPI = {
|
export const themeAPI = {
|
||||||
|
@ -32,22 +32,18 @@ export const themeAPI = {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
update(themeName, colors) {
|
update(data) {
|
||||||
const body = {
|
|
||||||
name: themeName,
|
|
||||||
colors: colors,
|
|
||||||
};
|
|
||||||
return apiReq.put(
|
return apiReq.put(
|
||||||
settingsURLs.updateTheme(themeName),
|
settingsURLs.updateTheme(data.id),
|
||||||
body,
|
data,
|
||||||
() => i18n.t("settings.theme.error-updating-theme"),
|
() => i18n.t("settings.theme.error-updating-theme"),
|
||||||
() => i18n.t("settings.theme.theme-updated")
|
() => i18n.t("settings.theme.theme-updated")
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
delete(themeName) {
|
delete(id) {
|
||||||
return apiReq.delete(
|
return apiReq.delete(
|
||||||
settingsURLs.deleteTheme(themeName),
|
settingsURLs.deleteTheme(id),
|
||||||
null,
|
null,
|
||||||
() => i18n.t("settings.theme.error-deleting-theme"),
|
() => i18n.t("settings.theme.error-deleting-theme"),
|
||||||
() => i18n.t("settings.theme.theme-deleted")
|
() => i18n.t("settings.theme.theme-deleted")
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h3>{{ buttonText }}</h3>
|
<h3>{{ buttonText }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<v-text-field v-model="color" hide-details class="ma-0 pa-0" solo v-show="$vuetify.breakpoint.mdAndUp">
|
<v-text-field v-model="color" hide-details class="ma-0 pa-0" solo >
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false">
|
<v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false">
|
||||||
<template v-slot:activator="{ on }">
|
<template v-slot:activator="{ on }">
|
||||||
|
@ -17,15 +17,7 @@
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
<div class="text-center" v-show="$vuetify.breakpoint.smAndDown">
|
|
||||||
<v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false">
|
|
||||||
<template v-slot:activator="{ on, attrs }">
|
|
||||||
<v-chip label :color="`${color}`" dark v-bind="attrs" v-on="on">
|
|
||||||
{{ color }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
</v-menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-for="option in options"
|
v-for="(option, index) in options"
|
||||||
:key="option.text"
|
:key="index"
|
||||||
class="mb-n4 mt-n3"
|
class="mb-n4 mt-n3"
|
||||||
dense
|
dense
|
||||||
:label="option.text"
|
:label="option.text"
|
||||||
|
@ -61,6 +61,4 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
|
@ -14,14 +14,7 @@
|
||||||
<v-list-item-title class="pl-2" v-text="item.name"></v-list-item-title>
|
<v-list-item-title class="pl-2" v-text="item.name"></v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
<v-list-item-action>
|
<v-list-item-action>
|
||||||
<v-btn
|
<v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top>
|
||||||
v-if="!edit"
|
|
||||||
color="primary"
|
|
||||||
icon
|
|
||||||
:href="`/api/recipes/media/${slug}/assets/${item.fileName}`"
|
|
||||||
target="_blank"
|
|
||||||
top
|
|
||||||
>
|
|
||||||
<v-icon> mdi-download</v-icon>
|
<v-icon> mdi-download</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
@ -118,6 +111,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
assetURL(assetName) {
|
||||||
|
return api.recipes.recipeAssetPath(this.slug, assetName);
|
||||||
|
},
|
||||||
setFileObject(obj) {
|
setFileObject(obj) {
|
||||||
this.fileObject = obj;
|
this.fileObject = obj;
|
||||||
},
|
},
|
||||||
|
@ -135,7 +131,8 @@ export default {
|
||||||
this.value.splice(index, 1);
|
this.value.splice(index, 1);
|
||||||
},
|
},
|
||||||
copyLink(name, fileName) {
|
copyLink(name, fileName) {
|
||||||
const copyText = ``;
|
const assetLink = api.recipes.recipeAssetPath(this.slug, fileName);
|
||||||
|
const copyText = ``;
|
||||||
navigator.clipboard.writeText(copyText).then(
|
navigator.clipboard.writeText(copyText).then(
|
||||||
() => console.log("Copied", copyText),
|
() => console.log("Copied", copyText),
|
||||||
() => console.log("Copied Failed", copyText)
|
() => console.log("Copied Failed", copyText)
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
color="secondary darken-1"
|
color="secondary darken-1"
|
||||||
class="rounded-sm static"
|
class="rounded-sm static"
|
||||||
>
|
>
|
||||||
{{ yields }}
|
{{ recipe.yields }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
<Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
|
<Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<v-form ref="file">
|
<v-form ref="file">
|
||||||
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
|
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
|
||||||
<slot v-bind="{ isSelecting, onButtonClick }">
|
<slot v-bind="{ isSelecting, onButtonClick }">
|
||||||
<v-btn :loading="isSelecting" @click="onButtonClick" color="accent" :text="textBtn">
|
<v-btn :loading="isSelecting" @click="onButtonClick" :small="small" color="accent" :text="textBtn">
|
||||||
<v-icon left> {{ icon }}</v-icon>
|
<v-icon left> {{ icon }}</v-icon>
|
||||||
{{ text ? text : defaultText }}
|
{{ text ? text : defaultText }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -15,6 +15,9 @@ const UPLOAD_EVENT = "uploaded";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
small: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
post: {
|
post: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -27,7 +30,7 @@ export default {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
file: null,
|
file: null,
|
||||||
isSelecting: false,
|
isSelecting: false,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,39 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mt-n5" v-if="recipes">
|
<div v-if="recipes">
|
||||||
<v-card flat class="transparent" height="60px">
|
<v-app-bar color="transparent" flat class="mt-n1 rounded" v-if="!disableToolbar">
|
||||||
<v-card-text>
|
<v-icon large left v-if="title">
|
||||||
<v-row v-if="title != null">
|
{{ titleIcon }}
|
||||||
<v-col>
|
</v-icon>
|
||||||
<v-btn-toggle group>
|
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||||
<v-btn text>
|
<v-spacer></v-spacer>
|
||||||
{{ title.toUpperCase() }}
|
<v-menu offset-y v-if="$listeners.sortRecent || $listeners.sort">
|
||||||
</v-btn>
|
<template v-slot:activator="{ on, attrs }">
|
||||||
</v-btn-toggle>
|
<v-btn-toggle group>
|
||||||
</v-col>
|
<v-btn text v-bind="attrs" v-on="on">
|
||||||
<v-spacer></v-spacer>
|
{{ $t("general.sort") }}
|
||||||
<v-col align="end">
|
</v-btn>
|
||||||
<v-menu offset-y v-if="sortable">
|
</v-btn-toggle>
|
||||||
<template v-slot:activator="{ on, attrs }">
|
</template>
|
||||||
<v-btn-toggle group>
|
<v-list>
|
||||||
<v-btn text v-bind="attrs" v-on="on">
|
<v-list-item @click="$emit('sortRecent')">
|
||||||
{{ $t("general.sort") }}
|
<v-list-item-title>{{ $t("general.recent") }}</v-list-item-title>
|
||||||
</v-btn>
|
</v-list-item>
|
||||||
</v-btn-toggle>
|
<v-list-item @click="$emit('sort')">
|
||||||
</template>
|
<v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
|
||||||
<v-list>
|
</v-list-item>
|
||||||
<v-list-item @click="$emit('sort-recent')">
|
</v-list>
|
||||||
<v-list-item-title>{{ $t("general.recent") }}</v-list-item-title>
|
</v-menu>
|
||||||
</v-list-item>
|
</v-app-bar>
|
||||||
<v-list-item @click="$emit('sort')">
|
<div v-if="recipes" class="mt-2">
|
||||||
<v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
<div v-if="recipes">
|
|
||||||
<v-row v-if="!viewScale">
|
<v-row v-if="!viewScale">
|
||||||
<v-col :sm="6" :md="6" :lg="4" :xl="3" v-for="recipe in recipes.slice(0, cardLimit)" :key="recipe.name">
|
<v-col :sm="6" :md="6" :lg="4" :xl="3" v-for="recipe in recipes.slice(0, cardLimit)" :key="recipe.name">
|
||||||
<RecipeCard
|
<RecipeCard
|
||||||
|
@ -91,9 +82,12 @@ export default {
|
||||||
MobileRecipeCard,
|
MobileRecipeCard,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
sortable: {
|
disableToolbar: {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
titleIcon: {
|
||||||
|
default: "mdi-tag-multiple-outline",
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
|
142
frontend/src/components/UI/Dialogs/BackupDialog.vue
Normal file
142
frontend/src/components/UI/Dialogs/BackupDialog.vue
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<BaseDialog
|
||||||
|
:title="$t('settings.backup.create-heading')"
|
||||||
|
titleIcon="mdi-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> mdi-plus </v-icon> 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");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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>
|
|
@ -3,14 +3,14 @@
|
||||||
<slot name="open" v-bind="{ open }"> </slot>
|
<slot name="open" v-bind="{ open }"> </slot>
|
||||||
<v-dialog v-model="dialog" :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined">
|
<v-dialog v-model="dialog" :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined">
|
||||||
<v-card class="pb-10" height="100%">
|
<v-card class="pb-10" height="100%">
|
||||||
<v-app-bar dark :color="color" class="mt-n1 mb-2">
|
<v-app-bar dark :color="color" class="mt-n1 mb-0">
|
||||||
<v-icon large left>
|
<v-icon large left>
|
||||||
{{ titleIcon }}
|
{{ titleIcon }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
<v-progress-linear class="mt-1" v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||||
<slot> </slot>
|
<slot> </slot>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<slot name="card-actions">
|
<slot name="card-actions">
|
||||||
|
@ -18,8 +18,12 @@
|
||||||
{{ $t("general.cancel") }}
|
{{ $t("general.cancel") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn color="error" text @click="deleteEvent" v-if="$listeners.delete">
|
||||||
|
{{ $t("general.delete") }}
|
||||||
|
</v-btn>
|
||||||
<v-btn color="success" @click="submitEvent">
|
<v-btn color="success" @click="submitEvent">
|
||||||
{{ $t("general.submit") }}
|
{{ submitText }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</slot>
|
</slot>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
@ -31,6 +35,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import i18n from "@/i18n";
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
color: {
|
color: {
|
||||||
|
@ -51,16 +56,34 @@ export default {
|
||||||
top: {
|
top: {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
submitText: {
|
||||||
|
default: () => i18n.t("general.create"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dialog: false,
|
dialog: false,
|
||||||
|
submitted: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
determineClose() {
|
||||||
|
return this.submitted && !this.loading;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
determineClose() {
|
||||||
|
this.submitted = false;
|
||||||
|
this.dialog = false;
|
||||||
|
},
|
||||||
|
dialog(val) {
|
||||||
|
if (val) this.submitted = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submitEvent() {
|
submitEvent() {
|
||||||
this.$emit("submit");
|
this.$emit("submit");
|
||||||
this.close();
|
this.submitted = true;
|
||||||
},
|
},
|
||||||
open() {
|
open() {
|
||||||
this.dialog = true;
|
this.dialog = true;
|
||||||
|
@ -68,6 +91,10 @@ export default {
|
||||||
close() {
|
close() {
|
||||||
this.dialog = false;
|
this.dialog = false;
|
||||||
},
|
},
|
||||||
|
deleteEvent() {
|
||||||
|
this.$emit("delete");
|
||||||
|
this.submitted = true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ImportOptions from "./ImportOptions";
|
import ImportOptions from "@/components/FormHelpers/ImportOptions";
|
||||||
import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue";
|
import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue";
|
||||||
import { backupURLs } from "@/api/backup";
|
import { backupURLs } from "@/api/backup";
|
||||||
export default {
|
export default {
|
|
@ -1,17 +1,19 @@
|
||||||
w<template>
|
w<template>
|
||||||
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
|
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
|
||||||
<div class="d-flex grow flex-wrap">
|
<div class="d-flex grow flex-wrap">
|
||||||
<v-sheet
|
<slot name="avatar">
|
||||||
:color="color"
|
<v-sheet
|
||||||
:max-height="icon ? 90 : undefined"
|
:color="color"
|
||||||
:width="icon ? 'auto' : '100%'"
|
:max-height="icon ? 90 : undefined"
|
||||||
elevation="6"
|
:width="icon ? 'auto' : '100%'"
|
||||||
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
|
elevation="6"
|
||||||
dark
|
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-icon v-if="icon" size="40" v-text="icon" />
|
||||||
</v-sheet>
|
<div v-if="text" class="headline font-weight-thin" v-text="text" />
|
||||||
|
</v-sheet>
|
||||||
|
</slot>
|
||||||
|
|
||||||
<div v-if="$slots['after-heading']" class="ml-auto">
|
<div v-if="$slots['after-heading']" class="ml-auto">
|
||||||
<slot name="after-heading" />
|
<slot name="after-heading" />
|
||||||
|
@ -29,7 +31,7 @@ w<template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="$slots.bottom">
|
<template v-if="$slots.bottom">
|
||||||
<v-divider class="mt-2" />
|
<v-divider class="mt-2" v-if="!$slots.actions" />
|
||||||
|
|
||||||
<div class="pb-0">
|
<div class="pb-0">
|
||||||
<slot name="bottom" />
|
<slot name="bottom" />
|
||||||
|
@ -73,6 +75,7 @@ export default {
|
||||||
classes() {
|
classes() {
|
||||||
return {
|
return {
|
||||||
"v-card--material--has-heading": this.hasHeading,
|
"v-card--material--has-heading": this.hasHeading,
|
||||||
|
"mt-3": this.$vuetify.breakpoint.name == "xs" || this.$vuetify.breakpoint.name == "sm"
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
hasHeading() {
|
hasHeading() {
|
|
@ -133,11 +133,6 @@ export default {
|
||||||
to: "/admin/profile",
|
to: "/admin/profile",
|
||||||
title: this.$t("settings.profile"),
|
title: this.$t("settings.profile"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: "mdi-format-color-fill",
|
|
||||||
to: "/admin/themes",
|
|
||||||
title: this.$t("general.themes"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: "mdi-food",
|
icon: "mdi-food",
|
||||||
to: "/admin/meal-planner",
|
to: "/admin/meal-planner",
|
||||||
|
@ -167,11 +162,6 @@ export default {
|
||||||
to: "/admin/manage-users",
|
to: "/admin/manage-users",
|
||||||
title: this.$t("settings.manage-users"),
|
title: this.$t("settings.manage-users"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: "mdi-backup-restore",
|
|
||||||
to: "/admin/backups",
|
|
||||||
title: this.$t("settings.backup-and-exports"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: "mdi-database-import",
|
icon: "mdi-database-import",
|
||||||
to: "/admin/migrations",
|
to: "/admin/migrations",
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<ImportDialog
|
|
||||||
:name="selectedName"
|
|
||||||
:date="selectedDate"
|
|
||||||
ref="import_dialog"
|
|
||||||
@import="importBackup"
|
|
||||||
@delete="deleteBackup"
|
|
||||||
/>
|
|
||||||
<v-row>
|
|
||||||
<v-col :cols="12" :sm="6" :md="6" :lg="4" :xl="4" v-for="backup in backups" :key="backup.name">
|
|
||||||
<v-card hover outlined @click="openDialog(backup)">
|
|
||||||
<v-card-text>
|
|
||||||
<v-row align="center">
|
|
||||||
<v-col cols="2">
|
|
||||||
<v-icon large color="primary">mdi-backup-restore</v-icon>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="10">
|
|
||||||
<div class="text-truncate">
|
|
||||||
<strong>{{ backup.name }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="text-truncate">{{ $d(Date.parse(backup.date), "medium") }}</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import ImportDialog from "./ImportDialog";
|
|
||||||
import { api } from "@/api";
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
backups: Array,
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
ImportDialog,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedName: "",
|
|
||||||
selectedDate: "",
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
openDialog(backup) {
|
|
||||||
this.selectedDate = backup.date;
|
|
||||||
this.selectedName = backup.name;
|
|
||||||
this.$refs.import_dialog.open();
|
|
||||||
},
|
|
||||||
async importBackup(data) {
|
|
||||||
this.$emit("loading");
|
|
||||||
const response = await api.backups.import(data.name, data);
|
|
||||||
if (response) {
|
|
||||||
let importData = response.data;
|
|
||||||
this.$emit("finished", importData);
|
|
||||||
} else {
|
|
||||||
this.$emit("finished");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteBackup(data) {
|
|
||||||
this.$emit("loading");
|
|
||||||
|
|
||||||
if (await api.backups.delete(data.name)) {
|
|
||||||
this.selectedBackup = null;
|
|
||||||
}
|
|
||||||
this.backupLoading = false;
|
|
||||||
|
|
||||||
this.$emit("finished");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -1,113 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-card :loading="loading">
|
|
||||||
<v-card-title> {{ $t("settings.backup.create-heading") }} </v-card-title>
|
|
||||||
<v-card-text class="mt-n3">
|
|
||||||
<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-btn color="success" text @click="createBackup()">
|
|
||||||
{{ $t("general.create") }}
|
|
||||||
</v-btn>
|
|
||||||
</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>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import ImportOptions from "./ImportOptions";
|
|
||||||
import { api } from "@/api";
|
|
||||||
export default {
|
|
||||||
components: { ImportOptions },
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
tag: null,
|
|
||||||
fullBackup: true,
|
|
||||||
loading: false,
|
|
||||||
options: {
|
|
||||||
recipes: true,
|
|
||||||
settings: true,
|
|
||||||
themes: true,
|
|
||||||
users: true,
|
|
||||||
groups: true,
|
|
||||||
},
|
|
||||||
availableTemplates: [],
|
|
||||||
selectedTemplates: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getAvailableBackups();
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
switchLabel() {
|
|
||||||
if (this.fullBackup) {
|
|
||||||
return this.$t("settings.backup.full-backup");
|
|
||||||
} else return this.$t("settings.backup.partial-backup");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateOptions(options) {
|
|
||||||
this.options = options;
|
|
||||||
},
|
|
||||||
async getAvailableBackups() {
|
|
||||||
let response = await api.backups.requestAvailable();
|
|
||||||
response.templates.forEach(element => {
|
|
||||||
this.availableTemplates.push(element);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async createBackup() {
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
let data = {
|
|
||||||
tag: this.tag,
|
|
||||||
options: {
|
|
||||||
recipes: this.options.recipes,
|
|
||||||
settings: this.options.settings,
|
|
||||||
themes: this.options.themes,
|
|
||||||
users: this.options.users,
|
|
||||||
groups: this.options.groups,
|
|
||||||
},
|
|
||||||
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>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -1,75 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-card :loading="backupLoading" class="mt-3">
|
|
||||||
<v-card-title class="headline">
|
|
||||||
{{ $t("settings.backup-and-exports") }}
|
|
||||||
</v-card-title>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
|
|
||||||
<v-card-text>
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12" md="6" sm="12">
|
|
||||||
<NewBackupCard @created="processFinished" />
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="6" sm="12">
|
|
||||||
<p>
|
|
||||||
{{ $t("settings.backup-info") }}
|
|
||||||
</p>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<v-divider class="my-3"></v-divider>
|
|
||||||
<v-card-title class="mt-n6">
|
|
||||||
{{ $t("settings.available-backups") }}
|
|
||||||
<span>
|
|
||||||
<TheUploadBtn class="mt-1" url="/api/backups/upload" @uploaded="getAvailableBackups" />
|
|
||||||
</span>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-card-title>
|
|
||||||
<AvailableBackupCard @loading="backupLoading = true" @finished="processFinished" :backups="availableBackups" />
|
|
||||||
|
|
||||||
<ImportSummaryDialog ref="report" :import-data="importData" />
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { api } from "@/api";
|
|
||||||
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
|
||||||
import ImportSummaryDialog from "@/components/ImportSummaryDialog";
|
|
||||||
import AvailableBackupCard from "@/pages/Admin/Backup/AvailableBackupCard";
|
|
||||||
import NewBackupCard from "@/pages/Admin/Backup/NewBackupCard";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
TheUploadBtn,
|
|
||||||
AvailableBackupCard,
|
|
||||||
NewBackupCard,
|
|
||||||
ImportSummaryDialog,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
failedImports: [],
|
|
||||||
successfulImports: [],
|
|
||||||
backupLoading: false,
|
|
||||||
availableBackups: [],
|
|
||||||
importData: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getAvailableBackups();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async getAvailableBackups() {
|
|
||||||
let response = await api.backups.requestAvailable();
|
|
||||||
this.availableBackups = response.imports;
|
|
||||||
this.availableTemplates = response.templates;
|
|
||||||
},
|
|
||||||
processFinished(data) {
|
|
||||||
this.getAvailableBackups();
|
|
||||||
this.backupLoading = false;
|
|
||||||
this.$refs.report.open(data);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -26,6 +26,8 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</TheUploadBtn>
|
</TheUploadBtn>
|
||||||
|
<BackupDialog :color="color" />
|
||||||
|
|
||||||
<v-btn :loading="loading" class="mx-2" small :color="color" @click="createBackup">
|
<v-btn :loading="loading" class="mx-2" small :color="color" @click="createBackup">
|
||||||
<v-icon left> mdi-plus </v-icon> Create
|
<v-icon left> mdi-plus </v-icon> Create
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -36,7 +38,7 @@
|
||||||
<v-list-item @click.prevent="openDialog(item)">
|
<v-list-item @click.prevent="openDialog(item)">
|
||||||
<v-list-item-avatar>
|
<v-list-item-avatar>
|
||||||
<v-icon large dark :color="color">
|
<v-icon large dark :color="color">
|
||||||
mdi-backup-restore
|
mdi-database
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
|
|
||||||
|
@ -65,13 +67,14 @@
|
||||||
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
||||||
import ImportSummaryDialog from "@/components/ImportSummaryDialog";
|
import ImportSummaryDialog from "@/components/ImportSummaryDialog";
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import StatCard from "./StatCard";
|
import StatCard from "@/components/UI/StatCard";
|
||||||
import ImportDialog from "../Backup/ImportDialog";
|
import BackupDialog from "@/components/UI/Dialogs/BackupDialog";
|
||||||
|
import ImportDialog from "@/components/UI/Dialogs/ImportDialog";
|
||||||
export default {
|
export default {
|
||||||
components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog },
|
components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog, BackupDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
color: "secondary",
|
color: "accent",
|
||||||
selectedName: "",
|
selectedName: "",
|
||||||
selectedDate: "",
|
selectedDate: "",
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -91,7 +94,6 @@ export default {
|
||||||
async getAvailableBackups() {
|
async getAvailableBackups() {
|
||||||
const response = await api.backups.requestAvailable();
|
const response = await api.backups.requestAvailable();
|
||||||
this.availableBackups = response.imports;
|
this.availableBackups = response.imports;
|
||||||
console.log(this.availableBackups);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteBackup(name) {
|
async deleteBackup(name) {
|
||||||
|
@ -106,6 +108,7 @@ export default {
|
||||||
this.selectedName = backup.name;
|
this.selectedName = backup.name;
|
||||||
this.$refs.import_dialog.open();
|
this.$refs.import_dialog.open();
|
||||||
},
|
},
|
||||||
|
|
||||||
async importBackup(data) {
|
async importBackup(data) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const response = await api.backups.import(data.name, data);
|
const response = await api.backups.import(data.name, data);
|
||||||
|
|
|
@ -49,12 +49,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import StatCard from "./StatCard";
|
import StatCard from "@/components/UI/StatCard";
|
||||||
export default {
|
export default {
|
||||||
components: { StatCard },
|
components: { StatCard },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
color: "secondary",
|
color: "accent",
|
||||||
total: 0,
|
total: 0,
|
||||||
events: [],
|
events: [],
|
||||||
icons: {
|
icons: {
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { api } from "@/api";
|
import { api } from "@/api";
|
||||||
import StatCard from "./StatCard";
|
import StatCard from "@/components/UI/StatCard";
|
||||||
import EventViewer from "./EventViewer";
|
import EventViewer from "./EventViewer";
|
||||||
import BackupViewer from "./BackupViewer";
|
import BackupViewer from "./BackupViewer";
|
||||||
export default {
|
export default {
|
||||||
|
|
215
frontend/src/pages/Admin/Profile/ThemeCard.vue
Normal file
215
frontend/src/pages/Admin/Profile/ThemeCard.vue
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<StatCard icon="mdi-format-color-fill" :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>mdi-desktop-tower-monitor</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>mdi-white-balance-sunny</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>mdi-weather-night</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-list-item @click="selectedTheme = item">
|
||||||
|
<v-list-item-avatar>
|
||||||
|
<v-icon large dark :color="item.colors.primary">
|
||||||
|
mdi-format-color-fill
|
||||||
|
</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">mdi-square-edit-outline</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</v-virtual-scroll>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer class="mx-2"></v-spacer>
|
||||||
|
<v-btn class="my-1 mb-n1" :color="color" @click="createTheme">
|
||||||
|
<v-icon left> mdi-plus </v-icon> {{ $t("general.create") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</template>
|
||||||
|
</StatCard>
|
||||||
|
<BaseDialog
|
||||||
|
:loading="loading"
|
||||||
|
:title="modalLabel.title"
|
||||||
|
title-icon="mdi-format-color-fill"
|
||||||
|
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]"
|
||||||
|
></v-text-field>
|
||||||
|
<v-row dense dflex wrap justify-content-center v-if="defaultData.colors">
|
||||||
|
<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>
|
||||||
|
</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 },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
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 mounted() {
|
||||||
|
await this.getAllThemes();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getAllThemes() {
|
||||||
|
this.availableThemes = await api.themes.requestAll();
|
||||||
|
},
|
||||||
|
editTheme(theme) {
|
||||||
|
console.log(theme);
|
||||||
|
this.defaultData = theme;
|
||||||
|
this.newTheme = false;
|
||||||
|
this.$refs.themeDialog.open();
|
||||||
|
},
|
||||||
|
createTheme() {
|
||||||
|
this.newTheme = true;
|
||||||
|
this.$refs.themeDialog.open();
|
||||||
|
console.log("Create Theme");
|
||||||
|
},
|
||||||
|
async processSubmit() {
|
||||||
|
if (this.newTheme) {
|
||||||
|
console.log("New Theme");
|
||||||
|
await api.themes.create(this.defaultData);
|
||||||
|
} else {
|
||||||
|
await api.themes.update(this.defaultData);
|
||||||
|
}
|
||||||
|
this.getAllThemes();
|
||||||
|
},
|
||||||
|
async deleteTheme() {
|
||||||
|
console.log(this.defaultData);
|
||||||
|
await api.themes.delete(this.defaultData.id);
|
||||||
|
this.getAllThemes();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
190
frontend/src/pages/Admin/Profile/UserCard.vue
Normal file
190
frontend/src/pages/Admin/Profile/UserCard.vue
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
<template>
|
||||||
|
<StatCard icon="mdi-account">
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<v-avatar color="accent" size="120" class="white--text headline mt-n16">
|
||||||
|
<img :src="userProfileImage" v-if="!hideImage" @error="hideImage = true" />
|
||||||
|
<div v-else>
|
||||||
|
{{ initials }}
|
||||||
|
</div>
|
||||||
|
</v-avatar>
|
||||||
|
</template>
|
||||||
|
<template v-slot:after-heading>
|
||||||
|
<div class="ml-auto text-right">
|
||||||
|
<div class="body-3 grey--text font-weight-light" v-text="$t('user.user-id-with-value', { id: user.id })" />
|
||||||
|
|
||||||
|
<h3 class="display-2 font-weight-light text--primary">
|
||||||
|
<small> {{ $t("group.group") }}: {{ user.group }} </small>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:actions>
|
||||||
|
<BaseDialog
|
||||||
|
:title="$t('user.reset-password')"
|
||||||
|
title-icon="mdi-lock"
|
||||||
|
:submit-text="$t('settings.change-password')"
|
||||||
|
@submit="changePassword"
|
||||||
|
:loading="loading"
|
||||||
|
:top="true"
|
||||||
|
>
|
||||||
|
<template v-slot:open="{ open }">
|
||||||
|
<v-btn color="primary" class="mr-1" small @click="open">
|
||||||
|
<v-icon left>mdi-lock</v-icon>
|
||||||
|
Change Password
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-form ref="passChange">
|
||||||
|
<v-text-field
|
||||||
|
v-model="password.current"
|
||||||
|
prepend-icon="mdi-lock"
|
||||||
|
:label="$t('user.current-password')"
|
||||||
|
:rules="[existsRule]"
|
||||||
|
validate-on-blur
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
@click:append="showPassword.current = !showPassword.current"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="password.newOne"
|
||||||
|
prepend-icon="mdi-lock"
|
||||||
|
:label="$t('user.new-password')"
|
||||||
|
:rules="[minRule]"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
@click:append="showPassword.newOne = !showPassword.newOne"
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="password.newTwo"
|
||||||
|
prepend-icon="mdi-lock"
|
||||||
|
:label="$t('user.confirm-password')"
|
||||||
|
:rules="[password.newOne === password.newTwo || $t('user.password-must-match')]"
|
||||||
|
validate-on-blur
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
@click:append="showPassword.newTwo = !showPassword.newTwo"
|
||||||
|
></v-text-field>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</BaseDialog>
|
||||||
|
</template>
|
||||||
|
<template v-slot:bottom>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form>
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('user.full-name')"
|
||||||
|
required
|
||||||
|
v-model="user.fullName"
|
||||||
|
:rules="[existsRule]"
|
||||||
|
validate-on-blur
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field :label="$t('user.email')" :rules="[emailRule]" validate-on-blur required v-model="user.email">
|
||||||
|
</v-text-field>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-card-actions class="pb-1 pt-3">
|
||||||
|
<TheUploadBtn
|
||||||
|
icon="mdi-image-area"
|
||||||
|
:text="$t('user.upload-photo')"
|
||||||
|
:url="userProfileImage"
|
||||||
|
file-name="profile_image"
|
||||||
|
/>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="success" @click="updateUser">
|
||||||
|
<v-icon left> mdi-content-save </v-icon>
|
||||||
|
{{ $t("general.update") }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</template>
|
||||||
|
</StatCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
|
||||||
|
import StatCard from "@/components/UI/StatCard";
|
||||||
|
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
||||||
|
import { api } from "@/api";
|
||||||
|
import { validators } from "@/mixins/validators";
|
||||||
|
import { initials } from "@/mixins/initials";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BaseDialog,
|
||||||
|
TheUploadBtn,
|
||||||
|
StatCard,
|
||||||
|
},
|
||||||
|
mixins: [validators, initials],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hideImage: false,
|
||||||
|
passwordLoading: false,
|
||||||
|
password: {
|
||||||
|
current: "",
|
||||||
|
newOne: "",
|
||||||
|
newTwo: "",
|
||||||
|
},
|
||||||
|
showPassword: false,
|
||||||
|
loading: false,
|
||||||
|
user: {
|
||||||
|
fullName: "",
|
||||||
|
email: "",
|
||||||
|
group: "",
|
||||||
|
admin: false,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
userProfileImage() {
|
||||||
|
this.resetImage();
|
||||||
|
return `api/users/${this.user.id}/image`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.refreshProfile();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
resetImage() {
|
||||||
|
this.hideImage = false;
|
||||||
|
},
|
||||||
|
async refreshProfile() {
|
||||||
|
this.user = await api.users.self();
|
||||||
|
},
|
||||||
|
openAvatarPicker() {
|
||||||
|
this.showAvatarPicker = true;
|
||||||
|
},
|
||||||
|
selectAvatar(avatar) {
|
||||||
|
this.user.avatar = avatar;
|
||||||
|
},
|
||||||
|
async updateUser() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
const response = await api.users.update(this.user);
|
||||||
|
if (response) {
|
||||||
|
this.$store.commit("setToken", response.data.access_token);
|
||||||
|
this.refreshProfile();
|
||||||
|
this.loading = false;
|
||||||
|
this.$store.dispatch("requestUserData");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async changePassword() {
|
||||||
|
this.paswordLoading = true;
|
||||||
|
let data = {
|
||||||
|
currentPassword: this.password.current,
|
||||||
|
newPassword: this.password.newOne,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.$refs.passChange.validate()) {
|
||||||
|
if (await api.users.changePassword(this.user.id, data)) {
|
||||||
|
this.$emit("refresh");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.paswordLoading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
|
@ -1,206 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<v-row dense>
|
<div class="mt-10">
|
||||||
<v-col cols="12" md="8" sm="12">
|
<v-row>
|
||||||
<v-card>
|
<v-col cols="12" sm="12" lg="6">
|
||||||
<v-card-title class="headline">
|
<UserCard />
|
||||||
<span>
|
</v-col>
|
||||||
<v-progress-circular v-if="loading" indeterminate color="primary" large class="mr-2"> </v-progress-circular>
|
<v-col cols="12" sm="12" lg="6"> </v-col>
|
||||||
</span>
|
</v-row>
|
||||||
{{ $t("settings.profile") }}
|
<v-row class="mt-7">
|
||||||
<v-spacer></v-spacer>
|
<v-col cols="12" sm="12" lg="6">
|
||||||
{{ $t("user.user-id-with-value", { id: user.id }) }}
|
<ThemeCard />
|
||||||
</v-card-title>
|
</v-col>
|
||||||
<v-divider></v-divider>
|
<v-col cols="12" sm="12" lg="6"> </v-col>
|
||||||
<v-card-text>
|
</v-row>
|
||||||
<v-row>
|
</div>
|
||||||
<v-col cols="12" md="3" align="center" justify="center">
|
|
||||||
<v-avatar color="accent" size="120" class="white--text headline mr-2">
|
|
||||||
<img :src="userProfileImage" v-if="!hideImage" @error="hideImage = true" />
|
|
||||||
<div v-else>
|
|
||||||
{{ initials }}
|
|
||||||
</div>
|
|
||||||
</v-avatar>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="9">
|
|
||||||
<v-form>
|
|
||||||
<v-text-field
|
|
||||||
:label="$t('user.full-name')"
|
|
||||||
required
|
|
||||||
v-model="user.fullName"
|
|
||||||
:rules="[existsRule]"
|
|
||||||
validate-on-blur
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
:label="$t('user.email')"
|
|
||||||
:rules="[emailRule]"
|
|
||||||
validate-on-blur
|
|
||||||
required
|
|
||||||
v-model="user.email"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
:label="$t('group.group')"
|
|
||||||
readonly
|
|
||||||
v-model="user.group"
|
|
||||||
persistent-hint
|
|
||||||
:hint="$t('group.groups-can-only-be-set-by-administrators')"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
</v-form>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<TheUploadBtn
|
|
||||||
icon="mdi-image-area"
|
|
||||||
:text="$t('user.upload-photo')"
|
|
||||||
:url="userProfileImage"
|
|
||||||
file-name="profile_image"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn color="success" class="mr-2" @click="updateUser">
|
|
||||||
<v-icon left> mdi-content-save </v-icon>
|
|
||||||
{{ $t("general.save") }}
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="4" sm="12">
|
|
||||||
<v-card height="100%">
|
|
||||||
<v-card-title class="headline">
|
|
||||||
{{ $t("user.reset-password") }}
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-card-title>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-card-text>
|
|
||||||
<v-form ref="passChange">
|
|
||||||
<v-text-field
|
|
||||||
v-model="password.current"
|
|
||||||
prepend-icon="mdi-lock"
|
|
||||||
:label="$t('user.current-password')"
|
|
||||||
:rules="[existsRule]"
|
|
||||||
validate-on-blur
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
@click:append="showPassword.current = !showPassword.current"
|
|
||||||
></v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
v-model="password.newOne"
|
|
||||||
prepend-icon="mdi-lock"
|
|
||||||
:label="$t('user.new-password')"
|
|
||||||
:rules="[minRule]"
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
@click:append="showPassword.newOne = !showPassword.newOne"
|
|
||||||
></v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
v-model="password.newTwo"
|
|
||||||
prepend-icon="mdi-lock"
|
|
||||||
:label="$t('user.confirm-password')"
|
|
||||||
:rules="[password.newOne === password.newTwo || $t('user.password-must-match')]"
|
|
||||||
validate-on-blur
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
@click:append="showPassword.newTwo = !showPassword.newTwo"
|
|
||||||
></v-text-field>
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn icon @click="showPassword = !showPassword" :loading="passwordLoading">
|
|
||||||
<v-icon v-if="!showPassword">mdi-eye-off</v-icon>
|
|
||||||
<v-icon v-else> mdi-eye </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn color="accent" class="mr-2" @click="changePassword">
|
|
||||||
<v-icon left> mdi-lock </v-icon>
|
|
||||||
{{ $t("settings.change-password") }}
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// import AvatarPicker from '@/components/AvatarPicker'
|
import ThemeCard from "./ThemeCard";
|
||||||
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
import UserCard from "./UserCard";
|
||||||
import { api } from "@/api";
|
|
||||||
import { validators } from "@/mixins/validators";
|
|
||||||
import { initials } from "@/mixins/initials";
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
TheUploadBtn,
|
UserCard,
|
||||||
},
|
ThemeCard,
|
||||||
mixins: [validators, initials],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hideImage: false,
|
|
||||||
passwordLoading: false,
|
|
||||||
password: {
|
|
||||||
current: "",
|
|
||||||
newOne: "",
|
|
||||||
newTwo: "",
|
|
||||||
},
|
|
||||||
showPassword: false,
|
|
||||||
loading: false,
|
|
||||||
user: {
|
|
||||||
fullName: "",
|
|
||||||
email: "",
|
|
||||||
group: "",
|
|
||||||
admin: false,
|
|
||||||
id: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
userProfileImage() {
|
|
||||||
this.resetImage();
|
|
||||||
return `api/users/${this.user.id}/image`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
this.refreshProfile();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
resetImage() {
|
|
||||||
this.hideImage = false;
|
|
||||||
},
|
|
||||||
async refreshProfile() {
|
|
||||||
this.user = await api.users.self();
|
|
||||||
},
|
|
||||||
openAvatarPicker() {
|
|
||||||
this.showAvatarPicker = true;
|
|
||||||
},
|
|
||||||
selectAvatar(avatar) {
|
|
||||||
this.user.avatar = avatar;
|
|
||||||
},
|
|
||||||
async updateUser() {
|
|
||||||
this.loading = true;
|
|
||||||
const response = await api.users.update(this.user);
|
|
||||||
if (response) {
|
|
||||||
this.$store.commit("setToken", response.data.access_token);
|
|
||||||
this.refreshProfile();
|
|
||||||
this.loading = false;
|
|
||||||
this.$store.dispatch("requestUserData");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async changePassword() {
|
|
||||||
this.paswordLoading = true;
|
|
||||||
let data = {
|
|
||||||
currentPassword: this.password.current,
|
|
||||||
newPassword: this.password.newOne,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.$refs.passChange.validate()) {
|
|
||||||
if (await api.users.changePassword(this.user.id, data)) {
|
|
||||||
this.$emit("refresh");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.paswordLoading = false;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-btn text color="info" @click="dialog = true">
|
|
||||||
{{ $t("settings.add-a-new-theme") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-dialog v-model="dialog" width="500">
|
|
||||||
<v-card>
|
|
||||||
<v-app-bar dense dark color="primary mb-2">
|
|
||||||
<v-icon large left class="mt-1">
|
|
||||||
mdi-format-color-fill
|
|
||||||
</v-icon>
|
|
||||||
|
|
||||||
<v-toolbar-title class="headline">
|
|
||||||
{{ $t("settings.add-a-new-theme") }}
|
|
||||||
</v-toolbar-title>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-app-bar>
|
|
||||||
<v-card-title> </v-card-title>
|
|
||||||
<v-form @submit.prevent="select">
|
|
||||||
<v-card-text>
|
|
||||||
<v-text-field
|
|
||||||
:label="$t('settings.theme.theme-name')"
|
|
||||||
v-model="themeName"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
></v-text-field>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn color="grey" text @click="dialog = false">
|
|
||||||
{{ $t("general.cancel") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-btn color="success" text type="submit" :disabled="!themeName">
|
|
||||||
{{ $t("general.create") }}
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-form>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
buttonText: String,
|
|
||||||
value: String,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
dialog: false,
|
|
||||||
themeName: "",
|
|
||||||
rules: {
|
|
||||||
required: val => !!val || this.$t("settings.theme.theme-name-is-required"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
color() {
|
|
||||||
this.updateColor();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
randomColor() {
|
|
||||||
return "#" + Math.floor(Math.random() * 16777215).toString(16);
|
|
||||||
},
|
|
||||||
select() {
|
|
||||||
const newTheme = {
|
|
||||||
name: this.themeName,
|
|
||||||
colors: {
|
|
||||||
primary: "#E58325",
|
|
||||||
accent: "#00457A",
|
|
||||||
secondary: "#973542",
|
|
||||||
success: "#5AB1BB",
|
|
||||||
info: "#4990BA",
|
|
||||||
warning: "#FF4081",
|
|
||||||
error: "#EF5350",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$emit("new-theme", newTheme);
|
|
||||||
this.dialog = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -1,88 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<ConfirmationDialog
|
|
||||||
:title="$t('settings.theme.delete-theme')"
|
|
||||||
:message="$t('settings.theme.are-you-sure-you-want-to-delete-this-theme')"
|
|
||||||
color="error"
|
|
||||||
icon="mdi-alert-circle"
|
|
||||||
ref="deleteThemeConfirm"
|
|
||||||
v-on:confirm="deleteSelectedTheme()"
|
|
||||||
/>
|
|
||||||
<v-card flat outlined class="ma-2">
|
|
||||||
<v-card-text class="mb-n5 mt-n2">
|
|
||||||
<h3>
|
|
||||||
{{ theme.name }}
|
|
||||||
{{ current ? $t("general.current-parenthesis") : "" }}
|
|
||||||
</h3>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-text>
|
|
||||||
<v-row flex align-center>
|
|
||||||
<v-card
|
|
||||||
v-for="(color, index) in theme.colors"
|
|
||||||
:key="index"
|
|
||||||
class="ma-1 mx-auto"
|
|
||||||
height="34"
|
|
||||||
width="36"
|
|
||||||
:color="color"
|
|
||||||
>
|
|
||||||
</v-card>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn text color="error" @click="confirmDelete">
|
|
||||||
{{ $t("general.delete") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<!-- <v-btn text color="accent" @click="editTheme">Edit</v-btn> -->
|
|
||||||
<v-btn text color="success" @click="saveThemes">{{ $t("general.apply") }}</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog";
|
|
||||||
import { api } from "@/api";
|
|
||||||
|
|
||||||
const DELETE_EVENT = "delete";
|
|
||||||
const APPLY_EVENT = "apply";
|
|
||||||
const EDIT_EVENT = "edit";
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
ConfirmationDialog,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
theme: Object,
|
|
||||||
current: {
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
confirmDelete() {
|
|
||||||
if (this.theme.name === "default") {
|
|
||||||
// Notify User Can't Delete Default
|
|
||||||
} else if (this.theme !== {}) {
|
|
||||||
this.$refs.deleteThemeConfirm.open();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteSelectedTheme() {
|
|
||||||
//Delete Theme from DB
|
|
||||||
if (await api.themes.delete(this.theme.name)) {
|
|
||||||
//Get the new list of available from DB
|
|
||||||
this.availableThemes = await api.themes.requestAll();
|
|
||||||
this.$emit(DELETE_EVENT);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async saveThemes() {
|
|
||||||
this.$store.commit("setTheme", this.theme);
|
|
||||||
this.$emit(APPLY_EVENT, this.theme);
|
|
||||||
},
|
|
||||||
editTheme() {
|
|
||||||
this.$emit(EDIT_EVENT);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -1,155 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="headline">
|
|
||||||
{{ $t("settings.theme.theme-settings") }}
|
|
||||||
</v-card-title>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-card-text>
|
|
||||||
<h2 class="mt-4 mb-1">{{ $t("settings.theme.dark-mode") }}</h2>
|
|
||||||
<p>
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"settings.theme.choose-how-mealie-looks-to-you-set-your-theme-preference-to-follow-your-system-settings-or-choose-to-use-the-light-or-dark-theme"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<v-row dense align="center">
|
|
||||||
<v-col cols="6">
|
|
||||||
<v-btn-toggle v-model="selectedDarkMode" color="primary " mandatory @change="setStoresDarkMode">
|
|
||||||
<v-btn value="system">
|
|
||||||
<v-icon>mdi-desktop-tower-monitor</v-icon>
|
|
||||||
<span class="ml-1" v-show="$vuetify.breakpoint.smAndUp">
|
|
||||||
{{ $t("settings.theme.default-to-system") }}
|
|
||||||
</span>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn value="light">
|
|
||||||
<v-icon>mdi-white-balance-sunny</v-icon>
|
|
||||||
<span class="ml-1" v-show="$vuetify.breakpoint.smAndUp">
|
|
||||||
{{ $t("settings.theme.light") }}
|
|
||||||
</span>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn value="dark">
|
|
||||||
<v-icon>mdi-weather-night</v-icon>
|
|
||||||
<span class="ml-1" v-show="$vuetify.breakpoint.smAndUp">
|
|
||||||
{{ $t("settings.theme.dark") }}
|
|
||||||
</span>
|
|
||||||
</v-btn>
|
|
||||||
</v-btn-toggle>
|
|
||||||
</v-col>
|
|
||||||
</v-row></v-card-text
|
|
||||||
>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-card-text>
|
|
||||||
<h2 class="mt-1 mb-1">{{ $t("settings.theme.theme") }}</h2>
|
|
||||||
<p>
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"settings.theme.select-a-theme-from-the-dropdown-or-create-a-new-theme-note-that-the-default-theme-will-be-served-to-all-users-who-have-not-set-a-theme-preference"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<v-row dense align-content="center" v-if="selectedTheme.colors">
|
|
||||||
<v-col>
|
|
||||||
<ColorPickerDialog :button-text="$t('settings.theme.primary')" v-model="selectedTheme.colors.primary" />
|
|
||||||
</v-col>
|
|
||||||
<v-col>
|
|
||||||
<ColorPickerDialog :button-text="$t('settings.theme.secondary')" v-model="selectedTheme.colors.secondary" />
|
|
||||||
</v-col>
|
|
||||||
<v-col>
|
|
||||||
<ColorPickerDialog :button-text="$t('settings.theme.accent')" v-model="selectedTheme.colors.accent" />
|
|
||||||
</v-col>
|
|
||||||
<v-col>
|
|
||||||
<ColorPickerDialog :button-text="$t('settings.theme.success')" v-model="selectedTheme.colors.success" />
|
|
||||||
</v-col>
|
|
||||||
<v-col>
|
|
||||||
<ColorPickerDialog :button-text="$t('settings.theme.info')" v-model="selectedTheme.colors.info" />
|
|
||||||
</v-col>
|
|
||||||
<v-col>
|
|
||||||
<ColorPickerDialog :button-text="$t('settings.theme.warning')" v-model="selectedTheme.colors.warning" />
|
|
||||||
</v-col>
|
|
||||||
<v-col>
|
|
||||||
<ColorPickerDialog :button-text="$t('settings.theme.error')" v-model="selectedTheme.colors.error" />
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-text>
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12" sm="12" md="6" lg="4" xl="3" v-for="theme in availableThemes" :key="theme.name">
|
|
||||||
<ThemeCard :theme="theme" :current="selectedTheme.name == theme.name ? true : false" @delete="getAllThemes" />
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<NewThemeDialog @new-theme="appendTheme" class="mt-1" />
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn color="success" @click="saveThemes" class="mr-2">
|
|
||||||
<v-icon left> mdi-content-save </v-icon>
|
|
||||||
{{ $t("general.save") }}
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { api } from "@/api";
|
|
||||||
import ColorPickerDialog from "@/components/FormHelpers/ColorPickerDialog";
|
|
||||||
import NewThemeDialog from "./NewThemeDialog";
|
|
||||||
import ThemeCard from "./ThemeCard";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
ColorPickerDialog,
|
|
||||||
NewThemeDialog,
|
|
||||||
ThemeCard,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedDarkMode: "system",
|
|
||||||
availableThemes: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
await this.getAllThemes();
|
|
||||||
|
|
||||||
this.selectedDarkMode = this.$store.getters.getDarkMode;
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
selectedTheme() {
|
|
||||||
return this.$store.getters.getActiveTheme;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async getAllThemes() {
|
|
||||||
this.availableThemes = await api.themes.requestAll();
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Create the new Theme and select it.
|
|
||||||
*/
|
|
||||||
async appendTheme(NewThemeDialog) {
|
|
||||||
const response = await api.themes.create(NewThemeDialog);
|
|
||||||
if (response) {
|
|
||||||
this.availableThemes.push(NewThemeDialog);
|
|
||||||
this.$store.commit("setTheme", NewThemeDialog);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setStoresDarkMode() {
|
|
||||||
this.$store.commit("setDarkMode", this.selectedDarkMode);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* This will save the current colors and make the selected theme live.
|
|
||||||
*/
|
|
||||||
saveThemes() {
|
|
||||||
api.themes.update(this.selectedTheme.name, this.selectedTheme.colors);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<CardSection
|
<CardSection
|
||||||
|
title-icon=""
|
||||||
v-if="siteSettings.showRecent"
|
v-if="siteSettings.showRecent"
|
||||||
:title="$t('page.recent')"
|
:title="$t('page.recent')"
|
||||||
:recipes="recentRecipes"
|
:recipes="recentRecipes"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||||
<CardSection
|
<CardSection
|
||||||
|
title-icon=""
|
||||||
:sortable="true"
|
:sortable="true"
|
||||||
:title="$t('page.all-recipes')"
|
:title="$t('page.all-recipes')"
|
||||||
:recipes="allRecipes"
|
:recipes="allRecipes"
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import Admin from "@/pages/Admin";
|
import Admin from "@/pages/Admin";
|
||||||
import Backup from "@/pages/Admin/Backup";
|
|
||||||
import Theme from "@/pages/Admin/Theme";
|
|
||||||
import MealPlanner from "@/pages/Admin/MealPlanner";
|
import MealPlanner from "@/pages/Admin/MealPlanner";
|
||||||
import Migration from "@/pages/Admin/Migration";
|
import Migration from "@/pages/Admin/Migration";
|
||||||
import Profile from "@/pages/Admin/Profile";
|
import Profile from "@/pages/Admin/Profile";
|
||||||
|
@ -31,21 +29,6 @@ export const adminRoutes = {
|
||||||
title: "settings.profile",
|
title: "settings.profile",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
path: "backups",
|
|
||||||
component: Backup,
|
|
||||||
meta: {
|
|
||||||
title: "settings.backup-and-exports",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "themes",
|
|
||||||
component: Theme,
|
|
||||||
meta: {
|
|
||||||
title: "general.themes",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "meal-planner",
|
path: "meal-planner",
|
||||||
component: MealPlanner,
|
component: MealPlanner,
|
||||||
|
|
|
@ -22,6 +22,7 @@ export const routes = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
|
base: process.env.BASE_URL,
|
||||||
routes,
|
routes,
|
||||||
mode: process.env.NODE_ENV === "production" ? "history" : "hash",
|
mode: process.env.NODE_ENV === "production" ? "history" : "hash",
|
||||||
scrollBehavior() {
|
scrollBehavior() {
|
||||||
|
|
|
@ -60,7 +60,7 @@ const actions = {
|
||||||
},
|
},
|
||||||
|
|
||||||
async resetTheme({ commit }) {
|
async resetTheme({ commit }) {
|
||||||
const defaultTheme = await api.themes.requestByName("default");
|
const defaultTheme = await api.themes.requestByName(1);
|
||||||
if (defaultTheme.colors) {
|
if (defaultTheme.colors) {
|
||||||
Vuetify.framework.theme.themes.dark = defaultTheme.colors;
|
Vuetify.framework.theme.themes.dark = defaultTheme.colors;
|
||||||
Vuetify.framework.theme.themes.light = defaultTheme.colors;
|
Vuetify.framework.theme.themes.light = defaultTheme.colors;
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from mealie.core import root_logger
|
|
||||||
from mealie.core.config import APP_VERSION, settings
|
from mealie.core.config import APP_VERSION, settings
|
||||||
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes
|
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes
|
||||||
from mealie.routes.about import about_router
|
from mealie.routes.about import about_router
|
||||||
from mealie.routes.groups import groups
|
from mealie.routes.groups import groups_router
|
||||||
from mealie.routes.mealplans import mealplans
|
from mealie.routes.mealplans import meal_plan_router
|
||||||
from mealie.routes.recipe import router as recipe_router
|
from mealie.routes.media import media_router
|
||||||
from mealie.routes.site_settings import all_settings
|
from mealie.routes.recipe import recipe_router
|
||||||
from mealie.routes.users import users
|
from mealie.routes.site_settings import settings_router
|
||||||
|
from mealie.routes.users import user_router
|
||||||
from mealie.services.events import create_general_event
|
from mealie.services.events import create_general_event
|
||||||
|
|
||||||
logger = root_logger.get_logger()
|
logger = get_logger()
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Mealie",
|
title="Mealie",
|
||||||
|
@ -29,15 +30,16 @@ def start_scheduler():
|
||||||
|
|
||||||
def api_routers():
|
def api_routers():
|
||||||
# Authentication
|
# Authentication
|
||||||
app.include_router(users.router)
|
app.include_router(user_router)
|
||||||
app.include_router(groups.router)
|
app.include_router(groups_router)
|
||||||
# Recipes
|
# Recipes
|
||||||
app.include_router(recipe_router)
|
app.include_router(recipe_router)
|
||||||
|
app.include_router(media_router)
|
||||||
app.include_router(about_router)
|
app.include_router(about_router)
|
||||||
# Meal Routes
|
# Meal Routes
|
||||||
app.include_router(mealplans.router)
|
app.include_router(meal_plan_router)
|
||||||
# Settings Routes
|
# Settings Routes
|
||||||
app.include_router(all_settings.router)
|
app.include_router(settings_router)
|
||||||
app.include_router(theme_routes.router)
|
app.include_router(theme_routes.router)
|
||||||
# Backups/Imports Routes
|
# Backups/Imports Routes
|
||||||
app.include_router(backup_routes.router)
|
app.include_router(backup_routes.router)
|
||||||
|
|
|
@ -93,7 +93,7 @@ class _Settings(BaseDocument):
|
||||||
|
|
||||||
class _Themes(BaseDocument):
|
class _Themes(BaseDocument):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.primary_key = "name"
|
self.primary_key = "id"
|
||||||
self.sql_model = SiteThemeModel
|
self.sql_model = SiteThemeModel
|
||||||
self.schema = SiteTheme
|
self.schema = SiteTheme
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from mealie.db.models.model_base import SqlAlchemyBase
|
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
from sqlalchemy.sql.sqltypes import Integer
|
||||||
|
|
||||||
|
|
||||||
class SiteThemeModel(SqlAlchemyBase):
|
class SiteThemeModel(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "site_theme"
|
__tablename__ = "site_theme"
|
||||||
name = sa.Column(sa.String, primary_key=True)
|
id = sa.Column(Integer, primary_key=True, unique=True)
|
||||||
|
name = sa.Column(sa.String, nullable=False, unique=True)
|
||||||
colors = orm.relationship("ThemeColorsModel", uselist=False, cascade="all, delete")
|
colors = orm.relationship("ThemeColorsModel", uselist=False, cascade="all, delete")
|
||||||
|
|
||||||
def __init__(self, name: str, colors: dict, session=None) -> None:
|
def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.colors = ThemeColorsModel(**colors)
|
self.colors = ThemeColorsModel(**colors)
|
||||||
|
|
||||||
def update(self, session=None, name: str = None, colors: dict = None) -> dict:
|
|
||||||
self.colors.update(**colors)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
class ThemeColorsModel(SqlAlchemyBase, BaseMixins):
|
||||||
class ThemeColorsModel(SqlAlchemyBase):
|
|
||||||
__tablename__ = "theme_colors"
|
__tablename__ = "theme_colors"
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
id = sa.Column(sa.Integer, primary_key=True)
|
||||||
parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name"))
|
parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name"))
|
||||||
|
@ -28,21 +26,3 @@ class ThemeColorsModel(SqlAlchemyBase):
|
||||||
info = sa.Column(sa.String)
|
info = sa.Column(sa.String)
|
||||||
warning = sa.Column(sa.String)
|
warning = sa.Column(sa.String)
|
||||||
error = sa.Column(sa.String)
|
error = sa.Column(sa.String)
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
primary: str = None,
|
|
||||||
accent: str = None,
|
|
||||||
secondary: str = None,
|
|
||||||
success: str = None,
|
|
||||||
info: str = None,
|
|
||||||
warning: str = None,
|
|
||||||
error: str = None,
|
|
||||||
) -> None:
|
|
||||||
self.primary = primary
|
|
||||||
self.accent = accent
|
|
||||||
self.secondary = secondary
|
|
||||||
self.success = success
|
|
||||||
self.info = info
|
|
||||||
self.warning = warning
|
|
||||||
self.error = error
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from .events import router as events_router
|
from . import events
|
||||||
|
|
||||||
about_router = APIRouter(prefix="/api/about")
|
about_router = APIRouter(prefix="/api/about")
|
||||||
|
|
||||||
about_router.include_router(events_router)
|
about_router.include_router(events.router)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from . import crud, groups
|
||||||
|
|
||||||
|
groups_router = APIRouter()
|
||||||
|
|
||||||
|
groups_router.include_router(crud.router)
|
||||||
|
groups_router.include_router(groups.router)
|
|
@ -0,0 +1,9 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from . import crud, helpers, mealplans
|
||||||
|
|
||||||
|
meal_plan_router = APIRouter()
|
||||||
|
|
||||||
|
meal_plan_router.include_router(crud.router)
|
||||||
|
meal_plan_router.include_router(helpers.router)
|
||||||
|
meal_plan_router.include_router(mealplans.router)
|
7
mealie/routes/media/__init__.py
Normal file
7
mealie/routes/media/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from . import recipe
|
||||||
|
|
||||||
|
media_router = APIRouter(prefix="/api/media", tags=["Site Media"])
|
||||||
|
|
||||||
|
media_router.include_router(recipe.router)
|
41
mealie/routes/media/recipe.py
Normal file
41
mealie/routes/media/recipe.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
from mealie.schema.recipe import Recipe
|
||||||
|
from starlette.responses import FileResponse
|
||||||
|
|
||||||
|
"""
|
||||||
|
These routes are for development only! These assets are served by Caddy when not
|
||||||
|
in development mode. If you make changes, be sure to test the production container.
|
||||||
|
"""
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/recipes")
|
||||||
|
|
||||||
|
|
||||||
|
class ImageType(str, Enum):
|
||||||
|
original = "original.webp"
|
||||||
|
small = "min-original.webp"
|
||||||
|
tiny = "tiny-original.webp"
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{recipe_slug}/images/{file_name}")
|
||||||
|
async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original):
|
||||||
|
"""Takes in a recipe slug, returns the static image. This route is proxied in the docker image
|
||||||
|
and should not hit the API in production"""
|
||||||
|
recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value)
|
||||||
|
|
||||||
|
if recipe_image:
|
||||||
|
return FileResponse(recipe_image)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{recipe_slug}/assets/{file_name}")
|
||||||
|
async def get_recipe_asset(recipe_slug: str, file_name: str):
|
||||||
|
""" Returns a recipe asset """
|
||||||
|
file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return FileResponse(file)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
0
mealie/routes/media/user.py
Normal file
0
mealie/routes/media/user.py
Normal file
|
@ -1,10 +1,9 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, recipe_media, tag_routes
|
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
|
||||||
|
|
||||||
router = APIRouter()
|
recipe_router = APIRouter()
|
||||||
|
|
||||||
router.include_router(all_recipe_routes.router)
|
recipe_router.include_router(all_recipe_routes.router)
|
||||||
router.include_router(recipe_crud_routes.router)
|
recipe_router.include_router(recipe_crud_routes.router)
|
||||||
router.include_router(recipe_media.router)
|
recipe_router.include_router(category_routes.router)
|
||||||
router.include_router(category_routes.router)
|
recipe_router.include_router(tag_routes.router)
|
||||||
router.include_router(tag_routes.router)
|
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
|
from shutil import copyfileobj
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
|
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
|
||||||
|
from fastapi.datastructures import UploadFile
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
from mealie.schema.recipe import Recipe, RecipeURLIn
|
from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn
|
||||||
from mealie.services.events import create_recipe_event
|
from mealie.services.events import create_recipe_event
|
||||||
from mealie.services.image.image import scrape_image, write_image
|
from mealie.services.image.image import scrape_image, write_image
|
||||||
from mealie.services.recipe.media import check_assets, delete_assets
|
from mealie.services.recipe.media import check_assets, delete_assets
|
||||||
from mealie.services.scraper.scraper import create_from_url
|
from mealie.services.scraper.scraper import create_from_url
|
||||||
|
from slugify import slugify
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
|
router = APIRouter(prefix="/api/recipes", tags=["Recipe CRUD"])
|
||||||
|
@ -126,3 +130,30 @@ def scrape_image_url(
|
||||||
""" Removes an existing image and replaces it with the incoming file. """
|
""" Removes an existing image and replaces it with the incoming file. """
|
||||||
|
|
||||||
scrape_image(url.url, recipe_slug)
|
scrape_image(url.url, recipe_slug)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
|
||||||
|
def upload_recipe_asset(
|
||||||
|
recipe_slug: str,
|
||||||
|
name: str = Form(...),
|
||||||
|
icon: str = Form(...),
|
||||||
|
extension: str = Form(...),
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
session: Session = Depends(generate_session),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
""" Upload a file to store as a recipe asset """
|
||||||
|
file_name = slugify(name) + "." + extension
|
||||||
|
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
|
||||||
|
dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
|
||||||
|
|
||||||
|
with dest.open("wb") as buffer:
|
||||||
|
copyfileobj(file.file, buffer)
|
||||||
|
|
||||||
|
if not dest.is_file():
|
||||||
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
recipe: Recipe = db.recipes.get(session, recipe_slug)
|
||||||
|
recipe.assets.append(asset_in)
|
||||||
|
db.recipes.update(session, recipe_slug, recipe.dict())
|
||||||
|
return asset_in
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
import shutil
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, status
|
|
||||||
from fastapi.datastructures import UploadFile
|
|
||||||
from mealie.db.database import db
|
|
||||||
from mealie.db.db_setup import generate_session
|
|
||||||
from mealie.routes.deps import get_current_user
|
|
||||||
from mealie.schema.recipe import Recipe, RecipeAsset
|
|
||||||
from slugify import slugify
|
|
||||||
from sqlalchemy.orm.session import Session
|
|
||||||
from starlette.responses import FileResponse
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/recipes/media", tags=["Recipe Media"])
|
|
||||||
|
|
||||||
|
|
||||||
class ImageType(str, Enum):
|
|
||||||
original = "original.webp"
|
|
||||||
small = "min-original.webp"
|
|
||||||
tiny = "tiny-original.webp"
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{recipe_slug}/images/{file_name}")
|
|
||||||
async def get_recipe_img(recipe_slug: str, file_name: ImageType = ImageType.original):
|
|
||||||
"""Takes in a recipe slug, returns the static image. This route is proxied in the docker image
|
|
||||||
and should not hit the API in production"""
|
|
||||||
recipe_image = Recipe(slug=recipe_slug).image_dir.joinpath(file_name.value)
|
|
||||||
|
|
||||||
if recipe_image:
|
|
||||||
return FileResponse(recipe_image)
|
|
||||||
else:
|
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{recipe_slug}/assets/{file_name}")
|
|
||||||
async def get_recipe_asset(recipe_slug: str, file_name: str):
|
|
||||||
""" Returns a recipe asset """
|
|
||||||
file = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return FileResponse(file)
|
|
||||||
except Exception:
|
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{recipe_slug}/assets", response_model=RecipeAsset)
|
|
||||||
def upload_recipe_asset(
|
|
||||||
recipe_slug: str,
|
|
||||||
name: str = Form(...),
|
|
||||||
icon: str = Form(...),
|
|
||||||
extension: str = Form(...),
|
|
||||||
file: UploadFile = File(...),
|
|
||||||
session: Session = Depends(generate_session),
|
|
||||||
current_user=Depends(get_current_user),
|
|
||||||
):
|
|
||||||
""" Upload a file to store as a recipe asset """
|
|
||||||
file_name = slugify(name) + "." + extension
|
|
||||||
asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name)
|
|
||||||
dest = Recipe(slug=recipe_slug).asset_dir.joinpath(file_name)
|
|
||||||
|
|
||||||
with dest.open("wb") as buffer:
|
|
||||||
shutil.copyfileobj(file.file, buffer)
|
|
||||||
|
|
||||||
if not dest.is_file():
|
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
recipe: Recipe = db.recipes.get(session, recipe_slug)
|
|
||||||
recipe.assets.append(asset_in)
|
|
||||||
db.recipes.update(session, recipe_slug, recipe.dict())
|
|
||||||
return asset_in
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from . import all_settings, custom_pages, site_settings
|
||||||
|
|
||||||
|
settings_router = APIRouter()
|
||||||
|
|
||||||
|
settings_router.include_router(all_settings.router)
|
||||||
|
settings_router.include_router(custom_pages.router)
|
||||||
|
settings_router.include_router(site_settings.router)
|
|
@ -1,4 +1,4 @@
|
||||||
from fastapi import APIRouter, Depends, status, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from mealie.db.database import db
|
from mealie.db.database import db
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.deps import get_current_user
|
from mealie.routes.deps import get_current_user
|
||||||
|
@ -21,27 +21,27 @@ def create_theme(data: SiteTheme, session: Session = Depends(generate_session),
|
||||||
db.themes.create(session, data.dict())
|
db.themes.create(session, data.dict())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/themes/{theme_name}")
|
@router.get("/themes/{id}")
|
||||||
def get_single_theme(theme_name: str, session: Session = Depends(generate_session)):
|
def get_single_theme(id: int, session: Session = Depends(generate_session)):
|
||||||
""" Returns a named theme """
|
""" Returns a named theme """
|
||||||
return db.themes.get(session, theme_name)
|
return db.themes.get(session, id)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/themes/{theme_name}", status_code=status.HTTP_200_OK)
|
@router.put("/themes/{id}", status_code=status.HTTP_200_OK)
|
||||||
def update_theme(
|
def update_theme(
|
||||||
theme_name: str,
|
id: int,
|
||||||
data: SiteTheme,
|
data: SiteTheme,
|
||||||
session: Session = Depends(generate_session),
|
session: Session = Depends(generate_session),
|
||||||
current_user=Depends(get_current_user),
|
current_user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
""" Update a theme database entry """
|
""" Update a theme database entry """
|
||||||
db.themes.update(session, theme_name, data.dict())
|
db.themes.update(session, id, data.dict())
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/themes/{theme_name}", status_code=status.HTTP_200_OK)
|
@router.delete("/themes/{id}", status_code=status.HTTP_200_OK)
|
||||||
def delete_theme(theme_name: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
def delete_theme(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||||
""" Deletes theme from the database """
|
""" Deletes theme from the database """
|
||||||
try:
|
try:
|
||||||
db.themes.delete(session, theme_name)
|
db.themes.delete(session, id)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from . import auth, crud, sign_up
|
||||||
|
|
||||||
|
user_router = APIRouter()
|
||||||
|
|
||||||
|
user_router.include_router(auth.router)
|
||||||
|
user_router.include_router(sign_up.router)
|
||||||
|
user_router.include_router(crud.router)
|
|
@ -1,9 +0,0 @@
|
||||||
from fastapi import APIRouter
|
|
||||||
from mealie.routes.users import auth, crud, sign_up
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
router.include_router(sign_up.router)
|
|
||||||
router.include_router(auth.router)
|
|
||||||
router.include_router(sign_up.router)
|
|
||||||
router.include_router(crud.router)
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +7,7 @@ class Colors(BaseModel):
|
||||||
primary: str = "#E58325"
|
primary: str = "#E58325"
|
||||||
accent: str = "#00457A"
|
accent: str = "#00457A"
|
||||||
secondary: str = "#973542"
|
secondary: str = "#973542"
|
||||||
success: str = "#4CAF50"
|
success: str = "#43A047"
|
||||||
info: str = "#4990BA"
|
info: str = "#4990BA"
|
||||||
warning: str = "#FF4081"
|
warning: str = "#FF4081"
|
||||||
error: str = "#EF5350"
|
error: str = "#EF5350"
|
||||||
|
@ -15,6 +17,7 @@ class Colors(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class SiteTheme(BaseModel):
|
class SiteTheme(BaseModel):
|
||||||
|
id: Optional[int]
|
||||||
name: str = "default"
|
name: str = "default"
|
||||||
colors: Colors = Colors()
|
colors: Colors = Colors()
|
||||||
|
|
||||||
|
|
|
@ -14,18 +14,19 @@ def default_settings():
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def default_theme():
|
def default_theme():
|
||||||
return SiteTheme().dict()
|
return SiteTheme(id=1).dict()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def new_theme():
|
def new_theme():
|
||||||
return {
|
return {
|
||||||
|
"id": 2,
|
||||||
"name": "myTestTheme",
|
"name": "myTestTheme",
|
||||||
"colors": {
|
"colors": {
|
||||||
"primary": "#E58325",
|
"primary": "#E58325",
|
||||||
"accent": "#00457A",
|
"accent": "#00457A",
|
||||||
"secondary": "#973542",
|
"secondary": "#973542",
|
||||||
"success": "#5AB1BB",
|
"success": "#43A047",
|
||||||
"info": "#4990BA",
|
"info": "#4990BA",
|
||||||
"warning": "#FF4081",
|
"warning": "#FF4081",
|
||||||
"error": "#EF5350",
|
"error": "#EF5350",
|
||||||
|
@ -54,7 +55,7 @@ def test_update_settings(api_client: TestClient, api_routes: AppRoutes, default_
|
||||||
|
|
||||||
|
|
||||||
def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme):
|
def test_default_theme(api_client: TestClient, api_routes: AppRoutes, default_theme):
|
||||||
response = api_client.get(api_routes.themes_theme_name("default"))
|
response = api_client.get(api_routes.themes_theme_name(1))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert json.loads(response.content) == default_theme
|
assert json.loads(response.content) == default_theme
|
||||||
|
|
||||||
|
@ -64,7 +65,7 @@ def test_create_theme(api_client: TestClient, api_routes: AppRoutes, new_theme,
|
||||||
response = api_client.post(api_routes.themes_create, json=new_theme, headers=token)
|
response = api_client.post(api_routes.themes_create, json=new_theme, headers=token)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
response = api_client.get(api_routes.themes_theme_name(new_theme.get("name")), headers=token)
|
response = api_client.get(api_routes.themes_theme_name(new_theme.get("id")), headers=token)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert json.loads(response.content) == new_theme
|
assert json.loads(response.content) == new_theme
|
||||||
|
|
||||||
|
@ -77,7 +78,7 @@ def test_read_all_themes(api_client: TestClient, api_routes: AppRoutes, default_
|
||||||
|
|
||||||
def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
|
def test_read_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme):
|
||||||
for theme in [default_theme, new_theme]:
|
for theme in [default_theme, new_theme]:
|
||||||
response = api_client.get(api_routes.themes_theme_name(theme.get("name")))
|
response = api_client.get(api_routes.themes_theme_name(theme.get("id")))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert json.loads(response.content) == theme
|
assert json.loads(response.content) == theme
|
||||||
|
|
||||||
|
@ -94,14 +95,14 @@ def test_update_theme(api_client: TestClient, api_routes: AppRoutes, token, defa
|
||||||
}
|
}
|
||||||
|
|
||||||
new_theme["colors"] = theme_colors
|
new_theme["colors"] = theme_colors
|
||||||
response = api_client.put(api_routes.themes_theme_name(new_theme.get("name")), json=new_theme, headers=token)
|
response = api_client.put(api_routes.themes_theme_name(new_theme.get("id")), json=new_theme, headers=token)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
response = api_client.get(api_routes.themes_theme_name(new_theme.get("name")))
|
response = api_client.get(api_routes.themes_theme_name(new_theme.get("id")))
|
||||||
assert json.loads(response.content) == new_theme
|
assert json.loads(response.content) == new_theme
|
||||||
|
|
||||||
|
|
||||||
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token):
|
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token):
|
||||||
for theme in [default_theme, new_theme]:
|
for theme in [default_theme, new_theme]:
|
||||||
response = api_client.delete(api_routes.themes_theme_name(theme.get("name")), headers=token)
|
response = api_client.delete(api_routes.themes_theme_name(theme.get("id")), headers=token)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue