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.pythonPath": ".venv/bin/python3.9",
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.pylintEnabled": false,
|
||||
"python.linting.enabled": true,
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.nosetestsEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.autoTestDiscoverOnSaveEnabled": false,
|
||||
"python.testing.pytestArgs": ["tests"],
|
||||
"cSpell.enableFiletypes": [
|
||||
"!javascript",
|
||||
"!python",
|
||||
"!yaml"
|
||||
],
|
||||
"cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"],
|
||||
"i18n-ally.localesPaths": "frontend/src/locales/messages",
|
||||
"i18n-ally.sourceLanguage": "en-US",
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"cSpell.words": [
|
||||
"compression",
|
||||
"hkotel",
|
||||
"performant",
|
||||
"postgres",
|
||||
"webp"
|
||||
],
|
||||
"search.mode": "reuseEditor"
|
||||
"cSpell.words": ["compression", "hkotel", "performant", "postgres", "webp"],
|
||||
"search.mode": "reuseEditor",
|
||||
"python.linting.flake8Enabled": true
|
||||
}
|
||||
|
|
10
Caddyfile
10
Caddyfile
|
@ -6,11 +6,11 @@
|
|||
:80 {
|
||||
@proxied path /api/* /docs /openapi.json
|
||||
|
||||
root * /app/dist
|
||||
encode gzip
|
||||
encode gzip zstd
|
||||
uri strip_suffix /
|
||||
|
||||
handle_path /api/recipes/media/* {
|
||||
# Handles Recipe Images / Assets
|
||||
handle_path /api/media/recipes/* {
|
||||
root * /app/data/recipes/
|
||||
file_server
|
||||
}
|
||||
|
@ -20,8 +20,8 @@
|
|||
}
|
||||
|
||||
handle {
|
||||
try_files {path}.html {path} /
|
||||
root * /app/dist
|
||||
try_files {path}.html {path} /index.html
|
||||
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
|
||||
container_name: mealie
|
||||
restart: always
|
||||
depends_on:
|
||||
- "postgres"
|
||||
ports:
|
||||
- 9090:80
|
||||
environment:
|
||||
|
|
|
@ -135,14 +135,18 @@ export const recipeAPI = {
|
|||
},
|
||||
|
||||
recipeImage(recipeSlug) {
|
||||
return `/api/recipes/media/${recipeSlug}/images/original.webp`;
|
||||
return `/api/media/recipes/${recipeSlug}/images/original.webp`;
|
||||
},
|
||||
|
||||
recipeSmallImage(recipeSlug) {
|
||||
return `/api/recipes/media/${recipeSlug}/images/min-original.webp`;
|
||||
return `/api/media/recipes/${recipeSlug}/images/min-original.webp`;
|
||||
},
|
||||
|
||||
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 = {
|
||||
allThemes: `${baseURL}themes`,
|
||||
specificTheme: themeName => `${prefix}/${themeName}`,
|
||||
specificTheme: id => `${prefix}/${id}`,
|
||||
createTheme: `${prefix}/create`,
|
||||
updateTheme: themeName => `${prefix}/${themeName}`,
|
||||
deleteTheme: themeName => `${prefix}/${themeName}`,
|
||||
updateTheme: id => `${prefix}/${id}`,
|
||||
deleteTheme: id => `${prefix}/${id}`,
|
||||
};
|
||||
|
||||
export const themeAPI = {
|
||||
|
@ -32,22 +32,18 @@ export const themeAPI = {
|
|||
);
|
||||
},
|
||||
|
||||
update(themeName, colors) {
|
||||
const body = {
|
||||
name: themeName,
|
||||
colors: colors,
|
||||
};
|
||||
update(data) {
|
||||
return apiReq.put(
|
||||
settingsURLs.updateTheme(themeName),
|
||||
body,
|
||||
settingsURLs.updateTheme(data.id),
|
||||
data,
|
||||
() => i18n.t("settings.theme.error-updating-theme"),
|
||||
() => i18n.t("settings.theme.theme-updated")
|
||||
);
|
||||
},
|
||||
|
||||
delete(themeName) {
|
||||
delete(id) {
|
||||
return apiReq.delete(
|
||||
settingsURLs.deleteTheme(themeName),
|
||||
settingsURLs.deleteTheme(id),
|
||||
null,
|
||||
() => i18n.t("settings.theme.error-deleting-theme"),
|
||||
() => i18n.t("settings.theme.theme-deleted")
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="text-center">
|
||||
<h3>{{ buttonText }}</h3>
|
||||
</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>
|
||||
<v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false">
|
||||
<template v-slot:activator="{ on }">
|
||||
|
@ -17,15 +17,7 @@
|
|||
</v-menu>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-checkbox
|
||||
v-for="option in options"
|
||||
:key="option.text"
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="mb-n4 mt-n3"
|
||||
dense
|
||||
:label="option.text"
|
||||
|
@ -62,5 +62,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</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-content>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
v-if="!edit"
|
||||
color="primary"
|
||||
icon
|
||||
:href="`/api/recipes/media/${slug}/assets/${item.fileName}`"
|
||||
target="_blank"
|
||||
top
|
||||
>
|
||||
<v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top>
|
||||
<v-icon> mdi-download</v-icon>
|
||||
</v-btn>
|
||||
<div v-else>
|
||||
|
@ -118,6 +111,9 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
assetURL(assetName) {
|
||||
return api.recipes.recipeAssetPath(this.slug, assetName);
|
||||
},
|
||||
setFileObject(obj) {
|
||||
this.fileObject = obj;
|
||||
},
|
||||
|
@ -135,7 +131,8 @@ export default {
|
|||
this.value.splice(index, 1);
|
||||
},
|
||||
copyLink(name, fileName) {
|
||||
const copyText = ``;
|
||||
const assetLink = api.recipes.recipeAssetPath(this.slug, fileName);
|
||||
const copyText = ``;
|
||||
navigator.clipboard.writeText(copyText).then(
|
||||
() => console.log("Copied", copyText),
|
||||
() => console.log("Copied Failed", copyText)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
color="secondary darken-1"
|
||||
class="rounded-sm static"
|
||||
>
|
||||
{{ yields }}
|
||||
{{ recipe.yields }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<Rating :value="recipe.rating" :name="recipe.name" :slug="recipe.slug" />
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<v-form ref="file">
|
||||
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
|
||||
<slot v-bind="{ isSelecting, onButtonClick }">
|
||||
<v-btn :loading="isSelecting" @click="onButtonClick" color="accent" :text="textBtn">
|
||||
<v-btn :loading="isSelecting" @click="onButtonClick" :small="small" color="accent" :text="textBtn">
|
||||
<v-icon left> {{ icon }}</v-icon>
|
||||
{{ text ? text : defaultText }}
|
||||
</v-btn>
|
||||
|
@ -15,6 +15,9 @@ const UPLOAD_EVENT = "uploaded";
|
|||
import { api } from "@/api";
|
||||
export default {
|
||||
props: {
|
||||
small: {
|
||||
default: false,
|
||||
},
|
||||
post: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
<template>
|
||||
<div class="mt-n5" v-if="recipes">
|
||||
<v-card flat class="transparent" height="60px">
|
||||
<v-card-text>
|
||||
<v-row v-if="title != null">
|
||||
<v-col>
|
||||
<v-btn-toggle group>
|
||||
<v-btn text>
|
||||
{{ title.toUpperCase() }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
<div v-if="recipes">
|
||||
<v-app-bar color="transparent" flat class="mt-n1 rounded" v-if="!disableToolbar">
|
||||
<v-icon large left v-if="title">
|
||||
{{ titleIcon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col align="end">
|
||||
<v-menu offset-y v-if="sortable">
|
||||
<v-menu offset-y v-if="$listeners.sortRecent || $listeners.sort">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn-toggle group>
|
||||
<v-btn text v-bind="attrs" v-on="on">
|
||||
|
@ -21,7 +15,7 @@
|
|||
</v-btn-toggle>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="$emit('sort-recent')">
|
||||
<v-list-item @click="$emit('sortRecent')">
|
||||
<v-list-item-title>{{ $t("general.recent") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('sort')">
|
||||
|
@ -29,11 +23,8 @@
|
|||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div v-if="recipes">
|
||||
</v-app-bar>
|
||||
<div v-if="recipes" class="mt-2">
|
||||
<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">
|
||||
<RecipeCard
|
||||
|
@ -91,9 +82,12 @@ export default {
|
|||
MobileRecipeCard,
|
||||
},
|
||||
props: {
|
||||
sortable: {
|
||||
disableToolbar: {
|
||||
default: false,
|
||||
},
|
||||
titleIcon: {
|
||||
default: "mdi-tag-multiple-outline",
|
||||
},
|
||||
title: {
|
||||
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>
|
||||
<v-dialog v-model="dialog" :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined">
|
||||
<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>
|
||||
{{ titleIcon }}
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline"> {{ title }} </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
<v-progress-linear class="mt-1" v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
<slot> </slot>
|
||||
<v-card-actions>
|
||||
<slot name="card-actions">
|
||||
|
@ -18,8 +18,12 @@
|
|||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<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">
|
||||
{{ $t("general.submit") }}
|
||||
{{ submitText }}
|
||||
</v-btn>
|
||||
</slot>
|
||||
</v-card-actions>
|
||||
|
@ -31,6 +35,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from "@/i18n";
|
||||
export default {
|
||||
props: {
|
||||
color: {
|
||||
|
@ -51,16 +56,34 @@ export default {
|
|||
top: {
|
||||
default: false,
|
||||
},
|
||||
submitText: {
|
||||
default: () => i18n.t("general.create"),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
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: {
|
||||
submitEvent() {
|
||||
this.$emit("submit");
|
||||
this.close();
|
||||
this.submitted = true;
|
||||
},
|
||||
open() {
|
||||
this.dialog = true;
|
||||
|
@ -68,6 +91,10 @@ export default {
|
|||
close() {
|
||||
this.dialog = false;
|
||||
},
|
||||
deleteEvent() {
|
||||
this.$emit("delete");
|
||||
this.submitted = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ImportOptions from "./ImportOptions";
|
||||
import ImportOptions from "@/components/FormHelpers/ImportOptions";
|
||||
import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue";
|
||||
import { backupURLs } from "@/api/backup";
|
||||
export default {
|
|
@ -1,6 +1,7 @@
|
|||
w<template>
|
||||
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
|
||||
<div class="d-flex grow flex-wrap">
|
||||
<slot name="avatar">
|
||||
<v-sheet
|
||||
:color="color"
|
||||
:max-height="icon ? 90 : undefined"
|
||||
|
@ -12,6 +13,7 @@ w<template>
|
|||
<v-icon v-if="icon" size="40" v-text="icon" />
|
||||
<div v-if="text" class="headline font-weight-thin" v-text="text" />
|
||||
</v-sheet>
|
||||
</slot>
|
||||
|
||||
<div v-if="$slots['after-heading']" class="ml-auto">
|
||||
<slot name="after-heading" />
|
||||
|
@ -29,7 +31,7 @@ w<template>
|
|||
</template>
|
||||
|
||||
<template v-if="$slots.bottom">
|
||||
<v-divider class="mt-2" />
|
||||
<v-divider class="mt-2" v-if="!$slots.actions" />
|
||||
|
||||
<div class="pb-0">
|
||||
<slot name="bottom" />
|
||||
|
@ -73,6 +75,7 @@ export default {
|
|||
classes() {
|
||||
return {
|
||||
"v-card--material--has-heading": this.hasHeading,
|
||||
"mt-3": this.$vuetify.breakpoint.name == "xs" || this.$vuetify.breakpoint.name == "sm"
|
||||
};
|
||||
},
|
||||
hasHeading() {
|
|
@ -133,11 +133,6 @@ export default {
|
|||
to: "/admin/profile",
|
||||
title: this.$t("settings.profile"),
|
||||
},
|
||||
{
|
||||
icon: "mdi-format-color-fill",
|
||||
to: "/admin/themes",
|
||||
title: this.$t("general.themes"),
|
||||
},
|
||||
{
|
||||
icon: "mdi-food",
|
||||
to: "/admin/meal-planner",
|
||||
|
@ -167,11 +162,6 @@ export default {
|
|||
to: "/admin/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",
|
||||
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>
|
||||
</template>
|
||||
</TheUploadBtn>
|
||||
<BackupDialog :color="color" />
|
||||
|
||||
<v-btn :loading="loading" class="mx-2" small :color="color" @click="createBackup">
|
||||
<v-icon left> mdi-plus </v-icon> Create
|
||||
</v-btn>
|
||||
|
@ -36,7 +38,7 @@
|
|||
<v-list-item @click.prevent="openDialog(item)">
|
||||
<v-list-item-avatar>
|
||||
<v-icon large dark :color="color">
|
||||
mdi-backup-restore
|
||||
mdi-database
|
||||
</v-icon>
|
||||
</v-list-item-avatar>
|
||||
|
||||
|
@ -65,13 +67,14 @@
|
|||
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
||||
import ImportSummaryDialog from "@/components/ImportSummaryDialog";
|
||||
import { api } from "@/api";
|
||||
import StatCard from "./StatCard";
|
||||
import ImportDialog from "../Backup/ImportDialog";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
import BackupDialog from "@/components/UI/Dialogs/BackupDialog";
|
||||
import ImportDialog from "@/components/UI/Dialogs/ImportDialog";
|
||||
export default {
|
||||
components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog },
|
||||
components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog, BackupDialog },
|
||||
data() {
|
||||
return {
|
||||
color: "secondary",
|
||||
color: "accent",
|
||||
selectedName: "",
|
||||
selectedDate: "",
|
||||
loading: false,
|
||||
|
@ -91,7 +94,6 @@ export default {
|
|||
async getAvailableBackups() {
|
||||
const response = await api.backups.requestAvailable();
|
||||
this.availableBackups = response.imports;
|
||||
console.log(this.availableBackups);
|
||||
},
|
||||
|
||||
async deleteBackup(name) {
|
||||
|
@ -106,6 +108,7 @@ export default {
|
|||
this.selectedName = backup.name;
|
||||
this.$refs.import_dialog.open();
|
||||
},
|
||||
|
||||
async importBackup(data) {
|
||||
this.loading = true;
|
||||
const response = await api.backups.import(data.name, data);
|
||||
|
|
|
@ -49,12 +49,12 @@
|
|||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import StatCard from "./StatCard";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
export default {
|
||||
components: { StatCard },
|
||||
data() {
|
||||
return {
|
||||
color: "secondary",
|
||||
color: "accent",
|
||||
total: 0,
|
||||
events: [],
|
||||
icons: {
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import StatCard from "./StatCard";
|
||||
import StatCard from "@/components/UI/StatCard";
|
||||
import EventViewer from "./EventViewer";
|
||||
import BackupViewer from "./BackupViewer";
|
||||
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>
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="8" sm="12">
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<span>
|
||||
<v-progress-circular v-if="loading" indeterminate color="primary" large class="mr-2"> </v-progress-circular>
|
||||
</span>
|
||||
{{ $t("settings.profile") }}
|
||||
<v-spacer></v-spacer>
|
||||
{{ $t("user.user-id-with-value", { id: user.id }) }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<div class="mt-10">
|
||||
<v-row>
|
||||
<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 }}
|
||||
<v-col cols="12" sm="12" lg="6">
|
||||
<UserCard />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" lg="6"> </v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-7">
|
||||
<v-col cols="12" sm="12" lg="6">
|
||||
<ThemeCard />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" lg="6"> </v-col>
|
||||
</v-row>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
// import AvatarPicker from '@/components/AvatarPicker'
|
||||
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
|
||||
import { api } from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
import { initials } from "@/mixins/initials";
|
||||
import ThemeCard from "./ThemeCard";
|
||||
import UserCard from "./UserCard";
|
||||
export default {
|
||||
components: {
|
||||
TheUploadBtn,
|
||||
},
|
||||
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;
|
||||
},
|
||||
UserCard,
|
||||
ThemeCard,
|
||||
},
|
||||
};
|
||||
</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>
|
||||
<v-container>
|
||||
<CardSection
|
||||
title-icon=""
|
||||
v-if="siteSettings.showRecent"
|
||||
:title="$t('page.recent')"
|
||||
:recipes="recentRecipes"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<v-container>
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
<CardSection
|
||||
title-icon=""
|
||||
:sortable="true"
|
||||
:title="$t('page.all-recipes')"
|
||||
:recipes="allRecipes"
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import Admin from "@/pages/Admin";
|
||||
import Backup from "@/pages/Admin/Backup";
|
||||
import Theme from "@/pages/Admin/Theme";
|
||||
import MealPlanner from "@/pages/Admin/MealPlanner";
|
||||
import Migration from "@/pages/Admin/Migration";
|
||||
import Profile from "@/pages/Admin/Profile";
|
||||
|
@ -31,21 +29,6 @@ export const adminRoutes = {
|
|||
title: "settings.profile",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
path: "backups",
|
||||
component: Backup,
|
||||
meta: {
|
||||
title: "settings.backup-and-exports",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "themes",
|
||||
component: Theme,
|
||||
meta: {
|
||||
title: "general.themes",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "meal-planner",
|
||||
component: MealPlanner,
|
||||
|
|
|
@ -22,6 +22,7 @@ export const routes = [
|
|||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
base: process.env.BASE_URL,
|
||||
routes,
|
||||
mode: process.env.NODE_ENV === "production" ? "history" : "hash",
|
||||
scrollBehavior() {
|
||||
|
|
|
@ -60,7 +60,7 @@ const actions = {
|
|||
},
|
||||
|
||||
async resetTheme({ commit }) {
|
||||
const defaultTheme = await api.themes.requestByName("default");
|
||||
const defaultTheme = await api.themes.requestByName(1);
|
||||
if (defaultTheme.colors) {
|
||||
Vuetify.framework.theme.themes.dark = defaultTheme.colors;
|
||||
Vuetify.framework.theme.themes.light = defaultTheme.colors;
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
|
||||
from mealie.core import root_logger
|
||||
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.about import about_router
|
||||
from mealie.routes.groups import groups
|
||||
from mealie.routes.mealplans import mealplans
|
||||
from mealie.routes.recipe import router as recipe_router
|
||||
from mealie.routes.site_settings import all_settings
|
||||
from mealie.routes.users import users
|
||||
from mealie.routes.groups import groups_router
|
||||
from mealie.routes.mealplans import meal_plan_router
|
||||
from mealie.routes.media import media_router
|
||||
from mealie.routes.recipe import recipe_router
|
||||
from mealie.routes.site_settings import settings_router
|
||||
from mealie.routes.users import user_router
|
||||
from mealie.services.events import create_general_event
|
||||
|
||||
logger = root_logger.get_logger()
|
||||
logger = get_logger()
|
||||
|
||||
app = FastAPI(
|
||||
title="Mealie",
|
||||
|
@ -29,15 +30,16 @@ def start_scheduler():
|
|||
|
||||
def api_routers():
|
||||
# Authentication
|
||||
app.include_router(users.router)
|
||||
app.include_router(groups.router)
|
||||
app.include_router(user_router)
|
||||
app.include_router(groups_router)
|
||||
# Recipes
|
||||
app.include_router(recipe_router)
|
||||
app.include_router(media_router)
|
||||
app.include_router(about_router)
|
||||
# Meal Routes
|
||||
app.include_router(mealplans.router)
|
||||
app.include_router(meal_plan_router)
|
||||
# Settings Routes
|
||||
app.include_router(all_settings.router)
|
||||
app.include_router(settings_router)
|
||||
app.include_router(theme_routes.router)
|
||||
# Backups/Imports Routes
|
||||
app.include_router(backup_routes.router)
|
||||
|
|
|
@ -93,7 +93,7 @@ class _Settings(BaseDocument):
|
|||
|
||||
class _Themes(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "name"
|
||||
self.primary_key = "id"
|
||||
self.sql_model = SiteThemeModel
|
||||
self.schema = SiteTheme
|
||||
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
import sqlalchemy as sa
|
||||
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"
|
||||
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")
|
||||
|
||||
def __init__(self, name: str, colors: dict, session=None) -> None:
|
||||
def __init__(self, name: str, colors: dict, *arg, **kwargs) -> None:
|
||||
self.name = name
|
||||
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):
|
||||
class ThemeColorsModel(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "theme_colors"
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
parent_id = sa.Column(sa.String, sa.ForeignKey("site_theme.name"))
|
||||
|
@ -28,21 +26,3 @@ class ThemeColorsModel(SqlAlchemyBase):
|
|||
info = sa.Column(sa.String)
|
||||
warning = 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 .events import router as events_router
|
||||
from . import events
|
||||
|
||||
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 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)
|
||||
router.include_router(recipe_crud_routes.router)
|
||||
router.include_router(recipe_media.router)
|
||||
router.include_router(category_routes.router)
|
||||
router.include_router(tag_routes.router)
|
||||
recipe_router.include_router(all_recipe_routes.router)
|
||||
recipe_router.include_router(recipe_crud_routes.router)
|
||||
recipe_router.include_router(category_routes.router)
|
||||
recipe_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.datastructures import UploadFile
|
||||
from mealie.core.root_logger import get_logger
|
||||
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, RecipeURLIn
|
||||
from mealie.schema.recipe import Recipe, RecipeAsset, RecipeURLIn
|
||||
from mealie.services.events import create_recipe_event
|
||||
from mealie.services.image.image import scrape_image, write_image
|
||||
from mealie.services.recipe.media import check_assets, delete_assets
|
||||
from mealie.services.scraper.scraper import create_from_url
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
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. """
|
||||
|
||||
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.db_setup import generate_session
|
||||
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())
|
||||
|
||||
|
||||
@router.get("/themes/{theme_name}")
|
||||
def get_single_theme(theme_name: str, session: Session = Depends(generate_session)):
|
||||
@router.get("/themes/{id}")
|
||||
def get_single_theme(id: int, session: Session = Depends(generate_session)):
|
||||
""" 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(
|
||||
theme_name: str,
|
||||
id: int,
|
||||
data: SiteTheme,
|
||||
session: Session = Depends(generate_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
""" 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)
|
||||
def delete_theme(theme_name: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
@router.delete("/themes/{id}", status_code=status.HTTP_200_OK)
|
||||
def delete_theme(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
|
||||
""" Deletes theme from the database """
|
||||
try:
|
||||
db.themes.delete(session, theme_name)
|
||||
db.themes.delete(session, id)
|
||||
except Exception:
|
||||
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
|
||||
|
||||
|
||||
|
@ -5,7 +7,7 @@ class Colors(BaseModel):
|
|||
primary: str = "#E58325"
|
||||
accent: str = "#00457A"
|
||||
secondary: str = "#973542"
|
||||
success: str = "#4CAF50"
|
||||
success: str = "#43A047"
|
||||
info: str = "#4990BA"
|
||||
warning: str = "#FF4081"
|
||||
error: str = "#EF5350"
|
||||
|
@ -15,6 +17,7 @@ class Colors(BaseModel):
|
|||
|
||||
|
||||
class SiteTheme(BaseModel):
|
||||
id: Optional[int]
|
||||
name: str = "default"
|
||||
colors: Colors = Colors()
|
||||
|
||||
|
|
|
@ -14,18 +14,19 @@ def default_settings():
|
|||
|
||||
@pytest.fixture(scope="session")
|
||||
def default_theme():
|
||||
return SiteTheme().dict()
|
||||
return SiteTheme(id=1).dict()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def new_theme():
|
||||
return {
|
||||
"id": 2,
|
||||
"name": "myTestTheme",
|
||||
"colors": {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"success": "#43A047",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"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):
|
||||
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 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)
|
||||
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 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):
|
||||
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 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
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
def test_delete_theme(api_client: TestClient, api_routes: AppRoutes, default_theme, new_theme, token):
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue