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:
Hayden
2021-05-04 20:45:11 -08:00
committed by GitHub
parent be5ac7a17a
commit c1370afb16
52 changed files with 878 additions and 1094 deletions

View File

@@ -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>

View File

@@ -0,0 +1,64 @@
<template>
<div>
<v-checkbox
v-for="(option, index) in options"
:key="index"
class="mb-n4 mt-n3"
dense
:label="option.text"
v-model="option.value"
@change="emitValue()"
></v-checkbox>
</div>
</template>
<script>
const UPDATE_EVENT = "update-options";
export default {
data() {
return {
options: {
recipes: {
value: true,
text: this.$t("general.recipes"),
},
settings: {
value: true,
text: this.$t("general.settings"),
},
pages: {
value: true,
text: this.$t("settings.pages"),
},
themes: {
value: true,
text: this.$t("general.themes"),
},
users: {
value: true,
text: this.$t("general.users"),
},
groups: {
value: true,
text: this.$t("general.groups"),
},
},
};
},
mounted() {
this.emitValue();
},
methods: {
emitValue() {
this.$emit(UPDATE_EVENT, {
recipes: this.options.recipes.value,
settings: this.options.settings.value,
themes: this.options.themes.value,
pages: this.options.pages.value,
users: this.options.users.value,
groups: this.options.groups.value,
});
},
},
};
</script>

View File

@@ -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 = `![${name}](${this.baseURL}/api/recipes/media/${this.slug}/assets/${fileName})`;
const assetLink = api.recipes.recipeAssetPath(this.slug, fileName);
const copyText = `![${name}](${assetLink})`;
navigator.clipboard.writeText(copyText).then(
() => console.log("Copied", copyText),
() => console.log("Copied Failed", copyText)

View File

@@ -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" />

View File

@@ -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,
@@ -27,7 +30,7 @@ export default {
default: true,
},
},
data: () => ({
data: () => ({
file: null,
isSelecting: false,
}),

View File

@@ -1,39 +1,30 @@
<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>
<v-spacer></v-spacer>
<v-col align="end">
<v-menu offset-y v-if="sortable">
<template v-slot:activator="{ on, attrs }">
<v-btn-toggle group>
<v-btn text v-bind="attrs" v-on="on">
{{ $t("general.sort") }}
</v-btn>
</v-btn-toggle>
</template>
<v-list>
<v-list-item @click="$emit('sort-recent')">
<v-list-item-title>{{ $t("general.recent") }}</v-list-item-title>
</v-list-item>
<v-list-item @click="$emit('sort')">
<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">
<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-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">
{{ $t("general.sort") }}
</v-btn>
</v-btn-toggle>
</template>
<v-list>
<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')">
<v-list-item-title>{{ $t("general.sort-alphabetically") }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</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,
},

View 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>

View File

@@ -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>

View File

@@ -0,0 +1,112 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="500" :fullscreen="$vuetify.breakpoint.xsOnly">
<v-card>
<v-toolbar dark color="primary" v-show="$vuetify.breakpoint.xsOnly">
<v-btn icon dark @click="dialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title></v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<v-btn dark text @click="raiseEvent('import')">
{{ $t("general.import") }}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-card-title> {{ name }} </v-card-title>
<v-card-subtitle class="mb-n3" v-if="date"> {{ $d(new Date(date), "medium") }} </v-card-subtitle>
<v-divider></v-divider>
<v-card-text>
<ImportOptions @update-options="updateOptions" class="mt-5 mb-2" />
<v-divider></v-divider>
<v-checkbox
dense
:label="$t('settings.remove-existing-entries-matching-imported-entries')"
v-model="forceImport"
></v-checkbox>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<TheDownloadBtn :download-url="downloadUrl" />
<v-spacer></v-spacer>
<v-btn color="error" text @click="raiseEvent('delete')">
{{ $t("general.delete") }}
</v-btn>
<v-btn color="success" outlined @click="raiseEvent('import')" v-show="$vuetify.breakpoint.smAndUp">
{{ $t("general.import") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
import ImportOptions from "@/components/FormHelpers/ImportOptions";
import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn.vue";
import { backupURLs } from "@/api/backup";
export default {
components: { ImportOptions, TheDownloadBtn },
props: {
name: {
default: "Backup Name",
},
date: {
default: "Backup Date",
},
},
data() {
return {
options: {
recipes: true,
settings: true,
themes: true,
users: true,
groups: true,
},
dialog: false,
forceImport: false,
rebaseImport: false,
downloading: false,
};
},
computed: {
downloadUrl() {
return backupURLs.downloadBackup(this.name);
},
},
methods: {
updateOptions(options) {
this.options = options;
},
open() {
this.dialog = true;
},
close() {
this.dialog = false;
},
raiseEvent(event) {
let eventData = {
name: this.name,
force: this.forceImport,
rebase: this.rebaseImport,
recipes: this.options.recipes,
settings: this.options.settings,
themes: this.options.themes,
users: this.options.users,
groups: this.options.groups,
};
this.close();
this.$emit(event, eventData);
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,103 @@
w<template>
<v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
<div class="d-flex grow flex-wrap">
<slot name="avatar">
<v-sheet
:color="color"
:max-height="icon ? 90 : undefined"
:width="icon ? 'auto' : '100%'"
elevation="6"
class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
dark
>
<v-icon v-if="icon" size="40" v-text="icon" />
<div v-if="text" class="headline font-weight-thin" v-text="text" />
</v-sheet>
</slot>
<div v-if="$slots['after-heading']" class="ml-auto">
<slot name="after-heading" />
</div>
</div>
<slot />
<template v-if="$slots.actions">
<v-divider class="mt-2" />
<v-card-actions class="pb-0">
<slot name="actions" />
</v-card-actions>
</template>
<template v-if="$slots.bottom">
<v-divider class="mt-2" v-if="!$slots.actions" />
<div class="pb-0">
<slot name="bottom" />
</div>
</template>
</v-card>
</template>
<script>
export default {
name: "MaterialCard",
props: {
avatar: {
type: String,
default: "",
},
color: {
type: String,
default: "primary",
},
icon: {
type: String,
default: undefined,
},
image: {
type: Boolean,
default: false,
},
text: {
type: String,
default: "",
},
title: {
type: String,
default: "",
},
},
computed: {
classes() {
return {
"v-card--material--has-heading": this.hasHeading,
"mt-3": this.$vuetify.breakpoint.name == "xs" || this.$vuetify.breakpoint.name == "sm"
};
},
hasHeading() {
return false;
},
hasAltHeading() {
return false;
},
},
};
</script>
<style lang="sass">
.v-card--material
&__avatar
position: relative
top: -64px
margin-bottom: -32px
&__heading
position: relative
top: -40px
transition: .3s ease
z-index: 1
</style>

View File

@@ -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",