refactor(frontend): 🔥 rewrite backup UI for new page base components

Removed old split code and used the composition api to to re-write the import/export functionality of mealie.
This commit is contained in:
hay-kot 2021-08-21 00:46:43 -08:00
parent 460f508f79
commit edae7bbb21
25 changed files with 535 additions and 759 deletions

View File

@ -8,6 +8,7 @@
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.testing.autoTestDiscoverOnSaveEnabled": false, "python.testing.autoTestDiscoverOnSaveEnabled": false,
"python.testing.pytestArgs": ["tests"], "python.testing.pytestArgs": ["tests"],
"python.linting.flake8Enabled": true,
"cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"], "cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"],
"i18n-ally.localesPaths": "frontend/lang/messages", "i18n-ally.localesPaths": "frontend/lang/messages",
"i18n-ally.sourceLanguage": "en-US", "i18n-ally.sourceLanguage": "en-US",
@ -15,7 +16,6 @@
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"cSpell.words": ["compression", "hkotel", "performant", "postgres", "webp"], "cSpell.words": ["compression", "hkotel", "performant", "postgres", "webp"],
"search.mode": "reuseEditor", "search.mode": "reuseEditor",
"python.linting.flake8Enabled": true,
"conventionalCommits.scopes": ["frontend", "docs", "backend"], "conventionalCommits.scopes": ["frontend", "docs", "backend"],
"editor.formatOnSave": true, "editor.formatOnSave": true,
"eslint.workingDirectories": ["./frontend"], "eslint.workingDirectories": ["./frontend"],

1
dev/scripts/openapi.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -35,7 +35,6 @@ class AppRoutes:
self.recipes_create = "/api/recipes/create" self.recipes_create = "/api/recipes/create"
self.recipes_create_from_zip = "/api/recipes/create-from-zip" self.recipes_create_from_zip = "/api/recipes/create-from-zip"
self.recipes_create_url = "/api/recipes/create-url" self.recipes_create_url = "/api/recipes/create-url"
self.recipes_summary = "/api/recipes/summary"
self.recipes_summary_uncategorized = "/api/recipes/summary/uncategorized" self.recipes_summary_uncategorized = "/api/recipes/summary/uncategorized"
self.recipes_summary_untagged = "/api/recipes/summary/untagged" self.recipes_summary_untagged = "/api/recipes/summary/untagged"
self.recipes_tag = "/api/recipes/tag" self.recipes_tag = "/api/recipes/tag"

View File

@ -10,8 +10,9 @@ export interface BackupOptions {
notifications?: boolean; notifications?: boolean;
} }
export interface ImportBackup extends BackupOptions { export interface ImportBackup {
name: string; name: string;
options: BackupOptions;
} }
export interface BackupJob { export interface BackupJob {
@ -58,7 +59,7 @@ export class BackupAPI extends BaseAPI {
/** Import a database backup file generated from Mealie. /** Import a database backup file generated from Mealie.
*/ */
async restoreDatabase(fileName: string, payload: BackupOptions) { async restoreDatabase(fileName: string, payload: BackupOptions) {
return await this.requests.post(routes.backupsFileNameImport(fileName), payload); return await this.requests.post(routes.backupsFileNameImport(fileName), {name: fileName, ...payload});
} }
/** Removes a database backup from the file system /** Removes a database backup from the file system

View File

@ -12,6 +12,10 @@ interface CreateAPIToken {
name: string; name: string;
} }
interface ResponseToken {
token: string;
}
// Code // Code
const prefix = "/api"; const prefix = "/api";
@ -28,7 +32,7 @@ const routes = {
usersIdFavoritesSlug: (id: string, slug: string) => `${prefix}/users/${id}/favorites/${slug}`, usersIdFavoritesSlug: (id: string, slug: string) => `${prefix}/users/${id}/favorites/${slug}`,
usersApiTokens: `${prefix}/users/api-tokens`, usersApiTokens: `${prefix}/users/api-tokens`,
usersApiTokensTokenId: (token_id: string) => `${prefix}/users/api-tokens/${token_id}`, usersApiTokensTokenId: (token_id: string | number) => `${prefix}/users/api-tokens/${token_id}`,
}; };
export class UserApi extends BaseCRUDAPI<UserOut, UserIn> { export class UserApi extends BaseCRUDAPI<UserOut, UserIn> {
@ -56,10 +60,10 @@ export class UserApi extends BaseCRUDAPI<UserOut, UserIn> {
} }
async createAPIToken(tokenName: CreateAPIToken) { async createAPIToken(tokenName: CreateAPIToken) {
return await this.requests.post(routes.usersApiTokens, tokenName); return await this.requests.post<ResponseToken>(routes.usersApiTokens, tokenName);
} }
async deleteApiToken(tokenId: string) { async deleteAPIToken(tokenId: string | number) {
return await this.requests.delete(routes.usersApiTokensTokenId(tokenId)); return await this.requests.delete(routes.usersApiTokensTokenId(tokenId));
} }

View File

@ -0,0 +1,20 @@
import { BaseAPI } from "./_base";
const prefix = "/api";
export class UtilsAPI extends BaseAPI {
async download(url: string) {
const { response } = await this.requests.get(url);
if (!response) {
return;
}
// @ts-ignore
const token: String = response.data.fileToken;
const tokenURL = prefix + "/utils/download?token=" + token;
window.open(tokenURL, "_blank");
return await response;
}
}

View File

@ -7,6 +7,7 @@ import { BackupAPI } from "./class-interfaces/backups";
import { UploadFile } from "./class-interfaces/upload"; import { UploadFile } from "./class-interfaces/upload";
import { CategoriesAPI } from "./class-interfaces/categories"; import { CategoriesAPI } from "./class-interfaces/categories";
import { TagsAPI } from "./class-interfaces/tags"; import { TagsAPI } from "./class-interfaces/tags";
import { UtilsAPI } from "./class-interfaces/utils";
import { ApiRequestInstance } from "~/types/api"; import { ApiRequestInstance } from "~/types/api";
class Api { class Api {
@ -19,6 +20,7 @@ class Api {
public backups: BackupAPI; public backups: BackupAPI;
public categories: CategoriesAPI; public categories: CategoriesAPI;
public tags: TagsAPI; public tags: TagsAPI;
public utils: UtilsAPI;
// Utils // Utils
public upload: UploadFile; public upload: UploadFile;
@ -44,6 +46,7 @@ class Api {
// Utils // Utils
this.upload = new UploadFile(requests); this.upload = new UploadFile(requests);
this.utils = new UtilsAPI(requests);
Object.freeze(this); Object.freeze(this);
Api.instance = this; Api.instance = this;

View File

@ -1,146 +0,0 @@
<template>
<div>
<BaseDialog
:title="$t('settings.backup.create-heading')"
:title-icon="$globals.icons.database"
:submit-text="$t('general.create')"
:loading="loading"
@submit="createBackup"
>
<template #open="{ open }">
<v-btn class="mx-2" small :color="color" @click="open">
<v-icon left> {{ $globals.icons.create }} </v-icon> {{ $t("general.custom") }}
</v-btn>
</template>
<v-card-text class="mt-6">
<v-text-field v-model="tag" dense :label="$t('settings.backup.backup-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>
<AdminBackupImportOptions v-model="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 AdminBackupImportOptions from "./AdminBackupImportOptions";
export default {
components: {
AdminBackupImportOptions,
},
props: {
color: {
type: String,
default: "primary",
},
},
data() {
return {
tag: null,
fullBackup: true,
loading: false,
options: {
recipes: true,
settings: true,
themes: true,
pages: true,
users: true,
groups: true,
},
availableTemplates: [],
selectedTemplates: [],
};
},
computed: {
switchLabel() {
if (this.fullBackup) {
return this.$t("settings.backup.full-backup");
} else return this.$t("settings.backup.partial-backup");
},
},
created() {
this.resetData();
this.getAvailableBackups();
},
methods: {
resetData() {
this.tag = null;
this.fullBackup = true;
this.loading = false;
this.options = {
recipes: true,
settings: true,
themes: true,
pages: true,
users: true,
groups: true,
notifications: true,
};
this.availableTemplates = [];
this.selectedTemplates = [];
},
updateOptions(options) {
this.options = options;
},
async getAvailableBackups() {
const response = await api.backups.requestAvailable();
response.templates.forEach((element) => {
this.availableTemplates.push(element);
});
},
async createBackup() {
this.loading = true;
const data = {
tag: this.tag,
options: {
recipes: this.options.recipes,
settings: this.options.settings,
pages: this.options.pages,
themes: this.options.themes,
users: this.options.users,
groups: this.options.groups,
notifications: this.options.notifications,
},
templates: this.selectedTemplates,
};
if (await api.backups.create(data)) {
this.$emit("created");
}
this.loading = false;
},
appendTemplate(templateName) {
if (this.selectedTemplates.includes(templateName)) {
const index = this.selectedTemplates.indexOf(templateName);
if (index !== -1) {
this.selectedTemplates.splice(index, 1);
}
} else this.selectedTemplates.push(templateName);
},
},
};
</script>

View File

@ -1,110 +0,0 @@
// TODO: Fix Download Links
<template>
<div class="text-center">
<BaseDialog
ref="baseDialog"
:title="name"
:title-icon="$globals.icons.database"
:submit-text="$t('general.import')"
:loading="loading"
@submit="raiseEvent"
>
<v-card-subtitle v-if="date" class="mb-n3 mt-3"> {{ $d(new Date(date), "medium") }} </v-card-subtitle>
<v-divider></v-divider>
<v-card-text>
<AdminBackupImportOptions class="mt-5 mb-2" @update-options="updateOptions" />
<v-divider></v-divider>
<v-checkbox
v-model="forceImport"
dense
:label="$t('settings.remove-existing-entries-matching-imported-entries')"
></v-checkbox>
</v-card-text>
<v-divider></v-divider>
<template #extra-buttons>
<!-- <TheDownloadBtn :download-url="downloadUrl">
<template #default="{ downloadFile }">
<v-btn class="mr-1" color="info" @click="downloadFile">
<v-icon left> {{ $globals.icons.download }}</v-icon>
{{ $t("general.download") }}
</v-btn>
</template>
</TheDownloadBtn> -->
</template>
</BaseDialog>
</div>
</template>
<script>
import AdminBackupImportOptions from "./AdminBackupImportOptions";
const IMPORT_EVENT = "import";
export default {
components: { AdminBackupImportOptions },
props: {
name: {
type: String,
default: "Backup Name",
},
date: {
type: String,
default: "Backup Date",
},
},
data() {
return {
loading: false,
options: {
recipes: true,
settings: true,
themes: true,
users: true,
groups: true,
},
dialog: false,
forceImport: false,
rebaseImport: false,
downloading: false,
};
},
// computed: {
// downloadUrl() {
// return API_ROUTES.backupsFileNameDownload(this.name);
// },
// },
methods: {
updateOptions(options) {
this.options = options;
},
open() {
this.dialog = true;
this.$refs.baseDialog.open();
},
close() {
this.dialog = false;
},
raiseEvent() {
const eventData = {
name: this.name,
force: this.forceImport,
rebase: this.rebaseImport,
recipes: this.options.recipes,
settings: this.options.settings,
themes: this.options.themes,
users: this.options.users,
groups: this.options.groups,
notifications: this.options.notifications,
};
this.loading = true;
this.$emit(IMPORT_EVENT, eventData);
this.loading = false;
},
},
};
</script>
<style></style>

View File

@ -1,171 +0,0 @@
<template>
<div>
<BaseDialog
ref="refImportDialog"
:title="selectedBackup.name"
:icon="$globals.icons.database"
:submit-text="$t('general.import')"
:loading="loading"
@submit="restoreBackup"
>
<v-card-subtitle v-if="selectedBackup.date" class="mb-n3 mt-3">
{{ $d(new Date(selectedBackup.date), "medium") }}
</v-card-subtitle>
<v-divider></v-divider>
<v-card-text>
<AdminBackupImportOptions v-model="importOptions" import-backup class="mt-5 mb-2" />
</v-card-text>
</BaseDialog>
<BaseDialog
ref="refDeleteConfirmation"
:title="$t('settings.backup.delete-backup')"
color="error"
:icon="$globals.icons.alertCircle"
@confirm="deleteBackup(selectedBackup.name)"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
<BaseStatCard :icon="$globals.icons.backupRestore" :color="color">
<template #after-heading>
<div class="ml-auto text-right">
<h2 class="body-3 grey--text font-weight-light">
{{ $t("settings.backup-and-exports") }}
</h2>
<h3 class="display-2 font-weight-light text--primary">
<small> {{ total }}</small>
</h3>
</div>
</template>
<div class="d-flex row py-3 justify-end">
<AppButtonUpload url="/api/backups/upload" @uploaded="refreshBackups">
<template #default="{ isSelecting, onButtonClick }">
<v-btn :loading="isSelecting" class="mx-2" small color="info" @click="onButtonClick">
<v-icon left> {{ $globals.icons.upload }} </v-icon> {{ $t("general.upload") }}
</v-btn>
</template>
</AppButtonUpload>
<AdminBackupDialog :color="color" />
<v-btn :loading="loading" class="mx-2" small color="success" @click="createBackup(null)">
<v-icon left> {{ $globals.icons.create }} </v-icon> {{ $t("general.create") }}
</v-btn>
</div>
<template #bottom>
<v-virtual-scroll height="290" item-height="70" :items="availableBackups">
<template #default="{ item }">
<v-list-item @click.prevent="openDialog(item, btnEvent.IMPORT_EVENT)">
<v-list-item-avatar>
<v-icon large dark :color="color">
{{ $globals.icons.database }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="item.name"></v-list-item-title>
<v-list-item-subtitle>
{{ $d(Date.parse(item.date), "medium") }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action class="ml-auto">
<v-btn large icon @click.stop="openDialog(item, btnEvent.DELETE_EVENT)">
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
</template>
</v-virtual-scroll>
</template>
</BaseStatCard>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api";
import AdminBackupImportOptions from "./AdminBackupImportOptions.vue";
import AdminBackupDialog from "./AdminBackupDialog.vue";
import { BackupFile } from "~/api/class-interfaces/backups";
import { useBackups } from "~/composables/use-backups";
const IMPORT_EVENT = "import";
const DELETE_EVENT = "delete";
type EVENTS = "import" | "delete";
export default defineComponent({
components: { AdminBackupImportOptions, AdminBackupDialog },
props: {
availableBackups: {
type: Array,
required: true,
},
templates: {
type: Array,
required: true,
},
},
setup() {
const refImportDialog = ref();
const refDeleteConfirmation = ref();
const { refreshBackups, importBackup, createBackup, deleteBackup } = useBackups();
return {
btnEvent: { IMPORT_EVENT, DELETE_EVENT },
refImportDialog,
refDeleteConfirmation,
refreshBackups,
importBackup,
createBackup,
deleteBackup,
};
},
data() {
return {
color: "accent",
loading: false,
selectedBackup: {
name: "",
date: "",
},
importOptions: {},
};
},
computed: {
total(): number {
return this.availableBackups.length || 0;
},
},
methods: {
openDialog(backup: BackupFile, event: EVENTS) {
this.selectedBackup = backup;
switch (event) {
case IMPORT_EVENT:
this.refImportDialog.open();
break;
case DELETE_EVENT:
this.refDeleteConfirmation.open();
break;
}
},
async restoreBackup() {
const payload = {
name: this.selectedBackup.name,
...this.importOptions,
};
await this.importBackup(this.selectedBackup.name, payload);
},
},
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,159 @@
<template>
<BaseStatCard :icon="$globals.icons.api" color="accent">
<template #after-heading>
<div class="ml-auto text-right">
<h2 class="body-3 grey--text font-weight-light">
{{ $t("settings.token.api-tokens") }}
</h2>
<h3 class="display-2 font-weight-light text--primary">
<small> {{ tokens.length }} </small>
</h3>
</div>
</template>
<template #bottom>
<v-subheader class="mb-n2">{{ $t("settings.token.active-tokens") }}</v-subheader>
<v-virtual-scroll height="210" item-height="70" :items="tokens" class="mt-2">
<template #default="{ item }">
<v-divider></v-divider>
<v-list-item @click.prevent>
<v-list-item-avatar>
<v-icon large dark color="accent">
{{ $globals.icons.api }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="item.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-action class="ml-auto">
<v-btn large icon @click.stop="deleteToken(item.id)">
<v-icon color="accent">{{ $globals.icons.delete }}</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<v-divider></v-divider>
</template>
</v-virtual-scroll>
<v-divider></v-divider>
<v-card-actions class="pb-1 pt-3">
<v-spacer></v-spacer>
<BaseDialog
:title="$t('settings.token.create-an-api-token')"
:title-icon="$globals.icons.api"
:submit-text="buttonText"
:loading="loading"
@submit="createToken(name)"
>
<v-card-text>
<v-form ref="domNewTokenForm" @submit.prevent>
<v-text-field v-model="name" :label="$t('settings.token.token-name')" required> </v-text-field>
</v-form>
<div v-if="createdToken != ''">
<v-textarea
v-model="createdToken"
class="mb-0 pb-0"
:label="$t('settings.token.api-token')"
readonly
:append-outer-icon="$globals.icons.contentCopy"
@click="copyToken"
@click:append-outer="copyToken"
>
</v-textarea>
<v-subheader class="text-center">
{{
$t(
"settings.token.copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again"
)
}}
</v-subheader>
</div>
</v-card-text>
<template #activator="{ open }">
<BaseButton create @click="open" />
</template>
</BaseDialog>
</v-card-actions>
</template>
</BaseStatCard>
</template>
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api";
import { useApiSingleton } from "~/composables/use-api";
const REFRESH_EVENT = "refresh";
export default defineComponent({
props: {
tokens: {
type: Array,
default: () => [],
},
},
setup(_, context) {
const api = useApiSingleton();
const domNewTokenForm = ref<VForm | null>(null);
const createdToken = ref("");
const name = ref("");
const loading = ref(false);
function resetCreate() {
createdToken.value = "";
loading.value = false;
name.value = "";
context.emit(REFRESH_EVENT);
}
async function createToken(name: string) {
if (loading.value) {
resetCreate();
return;
}
loading.value = true;
if (domNewTokenForm?.value?.validate()) {
console.log("Created");
return;
}
const { data } = await api.users.createAPIToken({ name });
if (data) {
createdToken.value = data.token;
}
}
async function deleteToken(id: string | number) {
const { data } = await api.users.deleteAPIToken(id);
context.emit(REFRESH_EVENT);
return data;
}
function copyToken() {
navigator.clipboard.writeText(createdToken.value).then(
() => console.log("Copied", createdToken.value),
() => console.log("Copied Failed", createdToken.value)
);
}
return { createToken, deleteToken, copyToken, createdToken, loading, name };
},
computed: {
buttonText(): any {
if (this.createdToken === "") {
return this.$t("general.create");
} else {
return this.$t("general.close");
}
},
},
});
</script>

View File

@ -1,18 +1,5 @@
<template> <template>
<BaseStatCard :icon="$globals.icons.user"> <BaseStatCard :icon="$globals.icons.user" color="accent">
<template #avatar>
<v-avatar color="accent" size="120" class="white--text headline mt-n16">
<img
v-if="!hideImage"
:src="require(`~/static/account.png`)"
@error="hideImage = true"
@load="hideImage = false"
/>
<div v-else>
{{ initials }}
</div>
</v-avatar>
</template>
<template #after-heading> <template #after-heading>
<div class="ml-auto text-right"> <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 })" /> <div class="body-3 grey--text font-weight-light" v-text="$t('user.user-id-with-value', { id: user.id })" />
@ -22,6 +9,8 @@
</h3> </h3>
</div> </div>
</template> </template>
<!-- Change Password -->
<template #actions> <template #actions>
<BaseDialog <BaseDialog
:title="$t('user.reset-password')" :title="$t('user.reset-password')"
@ -29,7 +18,7 @@
:submit-text="$t('settings.change-password')" :submit-text="$t('settings.change-password')"
:loading="loading" :loading="loading"
:top="true" :top="true"
@submit="changePassword" @submit="updatePassword"
> >
<template #activator="{ open }"> <template #activator="{ open }">
<v-btn color="info" class="mr-1" small @click="open"> <v-btn color="info" class="mr-1" small @click="open">
@ -68,12 +57,16 @@
</v-card-text> </v-card-text>
</BaseDialog> </BaseDialog>
</template> </template>
<!-- Update User -->
<template #bottom> <template #bottom>
<v-card-text> <v-card-text>
<v-form ref="userUpdate"> <v-form ref="userUpdate">
<v-text-field v-model="user.username" :label="$t('user.username')" required validate-on-blur> </v-text-field> <v-text-field v-model="userCopy.username" :label="$t('user.username')" required validate-on-blur>
<v-text-field v-model="user.fullName" :label="$t('user.full-name')" required validate-on-blur> </v-text-field> </v-text-field>
<v-text-field v-model="user.email" :label="$t('user.email')" validate-on-blur required> </v-text-field> <v-text-field v-model="userCopy.fullName" :label="$t('user.full-name')" required validate-on-blur>
</v-text-field>
<v-text-field v-model="userCopy.email" :label="$t('user.email')" validate-on-blur required> </v-text-field>
</v-form> </v-form>
</v-card-text> </v-card-text>
<v-divider></v-divider> <v-divider></v-divider>
@ -86,58 +79,83 @@
</BaseStatCard> </BaseStatCard>
</template> </template>
<script> <script lang="ts">
export default { import { ref, reactive, defineComponent } from "@nuxtjs/composition-api";
import { useApiSingleton } from "~/composables/use-api";
const events = {
UPDATE_USER: "update",
CHANGE_PASSWORD: "change-password",
UPLOAD_PHOTO: "upload-photo",
REFRESH: "refresh",
};
export default defineComponent({
props: {
user: {
type: Object,
required: true,
},
},
setup(props, context) {
const userCopy = ref({ ...props.user });
const api = useApiSingleton();
const domUpdatePassword = ref<VForm | null>(null);
const password = reactive({
current: "",
newOne: "",
newTwo: "",
});
async function updateUser() {
const { response } = await api.users.updateOne(userCopy.value.id, userCopy.value);
if (response?.status === 200) {
context.emit(events.REFRESH);
}
}
async function updatePassword() {
const { response } = await api.users.changePassword(userCopy.value.id, {
currentPassword: password.current,
newPassword: password.newOne,
});
if (response?.status === 200) {
console.log("Password Changed");
}
}
return { updateUser, updatePassword, userCopy, password, domUpdatePassword };
},
data() { data() {
return { return {
hideImage: false, hideImage: false,
passwordLoading: false, passwordLoading: false,
password: {
current: "",
newOne: "",
newTwo: "",
},
showPassword: false, showPassword: false,
loading: false, loading: false,
user: {},
}; };
}, },
watch: {
userProfileImage() {
this.hideImage = false;
},
},
methods: { methods: {
async refreshProfile() {
const [response, err] = await api.users.self();
if (err) {
return; // TODO: Log or Notifty User of Error
}
this.user = response.data;
},
openAvatarPicker() { openAvatarPicker() {
this.showAvatarPicker = true; this.showAvatarPicker = true;
}, },
selectAvatar(avatar) { selectAvatar(avatar) {
this.user.avatar = avatar; this.user.avatar = avatar;
}, },
async updateUser() { // async updateUser() {
if (!this.$refs.userUpdate.validate()) { // if (!this.$refs.userUpdate.validate()) {
return; // return;
} // }
this.loading = true; // this.loading = true;
const response = await api.users.update(this.user); // const response = await api.users.update(this.user);
if (response) { // if (response) {
this.$store.commit("setToken", response.data.access_token); // this.$store.commit("setToken", response.data.access_token);
this.refreshProfile(); // this.refreshProfile();
this.loading = false; // this.loading = false;
this.$store.dispatch("requestUserData"); // this.$store.dispatch("requestUserData");
} // }
}, // },
async changePassword() { async changePassword() {
this.paswordLoading = true; this.paswordLoading = true;
const data = { const data = {
@ -153,7 +171,7 @@ export default {
this.paswordLoading = false; this.paswordLoading = false;
}, },
}, },
}; });
</script> </script>
<style></style> <style></style>

View File

@ -1,211 +0,0 @@
<template>
<div>
<BaseStatCard :icon="$globals.icons.formatColorFill" :color="color">
<template #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 #actions>
<v-btn-toggle v-model="darkMode" color="primary " mandatory>
<v-btn small value="system">
<v-icon>{{ $globals.icons.desktopTowerMonitor }}</v-icon>
<span v-show="$vuetify.breakpoint.smAndUp" class="ml-1">
{{ $t("settings.theme.default-to-system") }}
</span>
</v-btn>
<v-btn small value="light">
<v-icon>{{ $globals.icons.weatherSunny }}</v-icon>
<span v-show="$vuetify.breakpoint.smAndUp" class="ml-1">
{{ $t("settings.theme.light") }}
</span>
</v-btn>
<v-btn small value="dark">
<v-icon>{{ $globals.icons.weatherNight }}</v-icon>
<span v-show="$vuetify.breakpoint.smAndUp" class="ml-1">
{{ $t("settings.theme.dark") }}
</span>
</v-btn>
</v-btn-toggle>
</template>
<template #bottom>
<v-virtual-scroll height="290" item-height="70" :items="availableThemes" class="mt-2">
<template #default="{ item }">
<v-divider></v-divider>
<v-list-item @click="selectedTheme = item">
<v-list-item-avatar>
<v-icon large dark :color="item.colors.primary">
{{ $globals.icons.formatColorFill }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="item.name"></v-list-item-title>
<v-row flex align-center class="mt-2 justify-space-around px-4 pb-2">
<v-sheet
v-for="(clr, index) in item.colors"
:key="index"
class="rounded flex mx-1"
:color="clr"
height="20"
>
</v-sheet>
</v-row>
</v-list-item-content>
<v-list-item-action class="ml-auto">
<v-btn large icon @click.stop="editTheme(item)">
<v-icon color="accent">{{ $globals.icons.edit }}</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<v-divider></v-divider>
</template>
</v-virtual-scroll>
<v-divider></v-divider>
<v-card-actions>
<BaseButton class="ml-auto mt-1 mb-n1" create @click="createTheme" />
</v-card-actions>
</template>
</BaseStatCard>
<BaseDialog
ref="themeDialog"
:loading="loading"
:title="modalLabel.title"
:title-icon="$globals.icons.formatColorFill"
modal-width="700"
:submit-text="modalLabel.button"
@submit="processSubmit"
@delete="deleteTheme"
>
<v-card-text class="mt-3">
<v-text-field
v-model="defaultData.name"
:label="$t('settings.theme.theme-name')"
:append-outer-icon="jsonEditor ? $globals.icons.formSelect : $globals.icons.codeBraces"
@click:append-outer="jsonEditor = !jsonEditor"
></v-text-field>
<v-row v-if="defaultData.colors && !jsonEditor" dense dflex wrap justify-content-center>
<v-col v-for="(_, key) in defaultData.colors" :key="key" cols="12" sm="6">
<BaseColorPicker v-model="defaultData.colors[key]" :button-text="labels[key]" />
</v-col>
</v-row>
<!-- <VJsoneditor v-else v-model="defaultData" height="250px" :options="jsonEditorOptions" @error="logError()" /> -->
</v-card-text>
</BaseDialog>
</div>
</template>
<script>
export default {
components: {
// VJsoneditor: () => import(/* webpackChunkName: "json-editor" */ "v-jsoneditor"),
},
data() {
return {
jsonEditor: false,
jsonEditorOptions: {
mode: "code",
search: false,
mainMenuBar: false,
},
availableThemes: [],
color: "accent",
newTheme: false,
loading: false,
defaultData: {
name: "",
colors: {
primary: "#E58325",
accent: "#00457A",
secondary: "#973542",
success: "#43A047",
info: "#4990BA",
warning: "#FF4081",
error: "#EF5350",
},
},
};
},
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) {
console.log(val);
},
get() {
return this.$vuetify.theme;
},
},
darkMode: {
set(val) {
console.log(val);
},
get() {
return false;
},
},
},
methods: {
async getAllThemes() {
this.availableThemes = await api.themes.requestAll();
},
editTheme(theme) {
this.defaultData = theme;
this.newTheme = false;
this.$refs.themeDialog.open();
},
createTheme() {
this.newTheme = true;
this.$refs.themeDialog.open();
},
async processSubmit() {
if (this.newTheme) {
await api.themes.create(this.defaultData);
} else {
await api.themes.update(this.defaultData);
}
this.getAllThemes();
},
async deleteTheme() {
await api.themes.delete(this.defaultData.id);
this.getAllThemes();
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -8,7 +8,8 @@
:outlined="btnStyle.outlined" :outlined="btnStyle.outlined"
:text="btnStyle.text" :text="btnStyle.text"
:to="to" :to="to"
@click="$emit('click')" v-on="$listeners"
@click="download ? downloadFile() : undefined"
> >
<v-icon left> <v-icon left>
<slot name="icon"> <slot name="icon">
@ -22,6 +23,7 @@
</template> </template>
<script> <script>
import { useApiSingleton } from "~/composables/use-api";
export default { export default {
name: "BaseButton", name: "BaseButton",
props: { props: {
@ -46,6 +48,15 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// Download
download: {
type: Boolean,
default: false,
},
downloadUrl: {
type: String,
default: "",
},
// Property // Property
loading: { loading: {
type: Boolean, type: Boolean,
@ -81,6 +92,11 @@ export default {
default: null, default: null,
}, },
}, },
setup() {
const api = useApiSingleton();
return { api };
},
data() { data() {
return { return {
buttonOptions: { buttonOptions: {
@ -114,6 +130,11 @@ export default {
icon: this.$globals.icons.cancel, icon: this.$globals.icons.cancel,
color: "grey", color: "grey",
}, },
download: {
text: "Download",
icon: this.$globals.icons.download,
color: "info",
},
}, },
buttonStyles: { buttonStyles: {
defaults: { defaults: {
@ -144,6 +165,8 @@ export default {
return this.buttonOptions.cancel; return this.buttonOptions.cancel;
} else if (this.save) { } else if (this.save) {
return this.buttonOptions.save; return this.buttonOptions.save;
} else if (this.download) {
return this.buttonOptions.download;
} }
return this.buttonOptions.create; return this.buttonOptions.create;
}, },
@ -163,6 +186,9 @@ export default {
setSecondary() { setSecondary() {
this.buttonStyles.defaults = this.buttonStyles.secondary; this.buttonStyles.defaults = this.buttonStyles.secondary;
}, },
downloadFile() {
this.api.utils.download(this.downloadUrl);
},
}, },
}; };
</script> </script>

View File

@ -53,6 +53,7 @@ function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance {
return requests; return requests;
} }
export const useApiSingleton = function (): Api { export const useApiSingleton = function (): Api {
const { $axios } = useContext(); const { $axios } = useContext();
const requests = getRequests($axios); const requests = getRequests($axios);

View File

@ -1,7 +1,7 @@
import { useAsync, ref } from "@nuxtjs/composition-api"; import { useAsync, ref, reactive } from "@nuxtjs/composition-api";
import { set } from "@vueuse/core"; import { set } from "@vueuse/core";
import { toastLoading, loader } from "./use-toast"; import { toastLoading, loader } from "./use-toast";
import { AllBackups, ImportBackup, BackupJob } from "~/api/class-interfaces/backups"; import { AllBackups, ImportBackup } from "~/api/class-interfaces/backups";
import { useApiSingleton } from "~/composables/use-api"; import { useApiSingleton } from "~/composables/use-api";
const backups = ref<AllBackups>({ const backups = ref<AllBackups>({
@ -15,9 +15,41 @@ function setBackups(newBackups: AllBackups | null) {
} }
} }
function optionsFactory() {
return {
tag: "",
templates: [],
options: {
recipes: true,
settings: true,
themes: true,
pages: true,
users: true,
groups: true,
notifications: true,
},
};
}
export const useBackups = function (fetch = true) { export const useBackups = function (fetch = true) {
const api = useApiSingleton(); const api = useApiSingleton();
const backupOptions = reactive(optionsFactory());
const deleteTarget = ref("");
const selected = ref<ImportBackup | null>({
name: "",
options: {
recipes: true,
settings: true,
pages: true,
themes: true,
groups: true,
users: true,
notifications: true,
},
});
function getBackups() { function getBackups() {
const backups = useAsync(async () => { const backups = useAsync(async () => {
const { data } = await api.backups.getAll(); const { data } = await api.backups.getAll();
@ -33,42 +65,33 @@ export const useBackups = function (fetch = true) {
} }
} }
async function createBackup(payload: BackupJob | null = null) { async function createBackup() {
if (payload === null) {
payload = {
tag: "",
templates: [],
options: {
recipes: true,
settings: true,
themes: true,
pages: true,
users: true,
groups: true,
notifications: true,
},
};
}
loader.info("Creating Backup..."); loader.info("Creating Backup...");
const { response } = await api.backups.createOne(backupOptions);
const { response } = await api.backups.createOne(payload);
if (response && response.status === 201) { if (response && response.status === 201) {
refreshBackups(); refreshBackups();
toastLoading.open = false; toastLoading.open = false;
Object.assign(backupOptions, optionsFactory());
} }
} }
async function deleteBackup(fileName: string) { async function deleteBackup() {
const { response } = await api.backups.deleteOne(fileName); const { response } = await api.backups.deleteOne(deleteTarget.value);
if (response && response.status === 200) { if (response && response.status === 200) {
refreshBackups(); refreshBackups();
} }
} }
async function importBackup(fileName: string, payload: ImportBackup) { async function importBackup() {
loader.info("Import Backup..."); loader.info("Import Backup...");
const { response } = await api.backups.restoreDatabase(fileName, payload);
if (!selected.value) {
return;
}
const { response } = await api.backups.restoreDatabase(selected.value.name, selected.value.options);
if (response && response.status === 200) { if (response && response.status === 200) {
refreshBackups(); refreshBackups();
@ -80,5 +103,15 @@ export const useBackups = function (fetch = true) {
refreshBackups(); refreshBackups();
} }
return { getBackups, refreshBackups, deleteBackup, backups, importBackup, createBackup }; return {
getBackups,
refreshBackups,
deleteBackup,
importBackup,
createBackup,
backups,
backupOptions,
deleteTarget,
selected,
};
}; };

View File

@ -303,7 +303,7 @@
"backup-created-at-response-export_path": "Backup Created at {path}", "backup-created-at-response-export_path": "Backup Created at {path}",
"backup-deleted": "Backup deleted", "backup-deleted": "Backup deleted",
"backup-tag": "Backup Tag", "backup-tag": "Backup Tag",
"create-heading": "Create a Backup", "create-heading": "Create A Backup",
"delete-backup": "Delete Backup", "delete-backup": "Delete Backup",
"error-creating-backup-see-log-file": "Error Creating Backup. See Log File", "error-creating-backup-see-log-file": "Error Creating Backup. See Log File",
"full-backup": "Full Backup", "full-backup": "Full Backup",
@ -411,7 +411,8 @@
"search": "Search", "search": "Search",
"site-settings": "Site Settings", "site-settings": "Site Settings",
"tags": "Tags", "tags": "Tags",
"toolbox": "Toolbox" "toolbox": "Toolbox",
"backups": "Backups"
}, },
"signup": { "signup": {
"error-signing-up": "Error Signing Up", "error-signing-up": "Error Signing Up",

View File

@ -122,6 +122,11 @@ export default defineComponent({
to: "/admin/migrations", to: "/admin/migrations",
title: this.$t("sidebar.migrations"), title: this.$t("sidebar.migrations"),
}, },
{
icon: this.$globals.icons.database,
to: "/admin/backups",
title: this.$t("sidebar.backups"),
},
], ],
bottomLinks: [ bottomLinks: [
{ {

View File

@ -246,7 +246,7 @@ export default {
themes: { themes: {
dark: { dark: {
primary: "#E58325", primary: "#E58325",
accent: "#00457A", accent: "#007A99",
secondary: "#973542", secondary: "#973542",
success: "#43A047", success: "#43A047",
info: "#1976d2", info: "#1976d2",
@ -255,7 +255,7 @@ export default {
}, },
light: { light: {
primary: "#E58325", primary: "#E58325",
accent: "#00457A", accent: "#007A99",
secondary: "#973542", secondary: "#973542",
success: "#43A047", success: "#43A047",
info: "#1976d2", info: "#1976d2",

View File

@ -0,0 +1,153 @@
// TODO: Create a new datatable below to display the import summary json files saved on server (Need to do as well).
<template>
<v-container fluid>
<section>
<BaseCardSectionTitle title="Mealie Backups"> </BaseCardSectionTitle>
<!-- Delete Dialog -->
<BaseDialog
ref="domDeleteConfirmation"
:title="$t('settings.backup.delete-backup')"
color="error"
:icon="$globals.icons.alertCircle"
@confirm="deleteBackup()"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
<!-- Import Dialog -->
<BaseDialog
ref="domImportDialog"
:title="selected.name"
:icon="$globals.icons.database"
:submit-text="$t('general.import')"
@submit="importBackup()"
>
<!-- <v-card-subtitle v-if="date" class="mb-n3 mt-3"> {{ $d(new Date(date), "medium") }} </v-card-subtitle> -->
<v-divider></v-divider>
<v-card-text>
<AdminBackupImportOptions v-model="selected.options" class="mt-5 mb-2" :import-backup="true" />
</v-card-text>
<v-divider></v-divider>
</BaseDialog>
<v-toolbar flat class="justify-between">
<BaseButton class="mr-2" @click="createBackup(null)" />
<!-- Backup Creation Dialog -->
<BaseDialog
:title="$t('settings.backup.create-heading')"
:icon="$globals.icons.database"
:submit-text="$t('general.create')"
@submit="createBackup"
>
<template #activator="{ open }">
<BaseButton secondary @click="open"> {{ $t("general.custom") }}</BaseButton>
</template>
<v-divider></v-divider>
<v-card-text>
<v-text-field v-model="backupOptions.tag" :label="$t('settings.backup.backup-tag')"> </v-text-field>
<AdminBackupImportOptions v-model="backupOptions.options" class="mt-5 mb-2" />
<v-divider class="my-3"></v-divider>
<p class="text-uppercase">Templates</p>
<v-checkbox
v-for="(template, index) in backups.templates"
:key="index"
v-model="backupOptions.templates"
:value="template"
:label="template"
></v-checkbox>
</v-card-text>
</BaseDialog>
</v-toolbar>
<v-data-table
:headers="headers"
:items="backups.imports || []"
class="elevation-0"
hide-default-footer
disable-pagination
:search="search"
@click:row="setSelected"
>
<template #item.date="{ item }">
{{ $d(Date.parse(item.date), "medium") }}
</template>
<template #item.actions="{ item }">
<BaseButton
small
class="mx-1"
delete
@click.stop="
domDeleteConfirmation.open();
deleteTarget = item.name;
"
/>
<BaseButton small download :download-url="backupsFileNameDownload(item.name)" @click.stop />
</template>
</v-data-table>
<v-divider></v-divider>
</section>
</v-container>
</template>
<script lang="ts">
import AdminBackupImportOptions from "@/components/Domain/Admin/AdminBackupImportOptions.vue";
import { defineComponent, reactive, toRefs, useContext, ref } from "@nuxtjs/composition-api";
import { useBackups } from "~/composables/use-backups";
export default defineComponent({
components: { AdminBackupImportOptions },
layout: "admin",
setup() {
const { i18n } = useContext();
const { selected, backups, backupOptions, deleteTarget, refreshBackups, importBackup, createBackup, deleteBackup } =
useBackups();
const domDeleteConfirmation = ref(null);
const domImportDialog = ref(null);
const state = reactive({
search: "",
headers: [
{ text: i18n.t("general.name"), value: "name" },
{ text: i18n.t("general.created"), value: "date" },
{ text: "", value: "actions", align: "right" },
],
});
function setSelected(data: { name: string; date: string }) {
if (selected.value === null || selected.value === undefined) {
return;
}
selected.value.name = data.name;
// @ts-ignore - Calling Child Method
domImportDialog.value.open();
}
const backupsFileNameDownload = (fileName: string) => `api/backups/${fileName}/download`;
return {
selected,
...toRefs(state),
backupOptions,
backups,
createBackup,
deleteBackup,
setSelected,
deleteTarget,
domDeleteConfirmation,
domImportDialog,
importBackup,
refreshBackups,
backupsFileNameDownload,
};
},
});
</script>
<style scoped>
</style>

View File

@ -77,7 +77,7 @@
</v-col> </v-col>
</v-row> </v-row>
<v-row class="mt-10" align-content="stretch"> <v-row class="mt-10" align-content="stretch">
<v-col cols="12" sm="12" lg="6"> <v-col>
<AdminEventViewer <AdminEventViewer
v-if="events" v-if="events"
:events="events.events" :events="events.events"
@ -86,9 +86,6 @@
@delete-item="deleteEvent" @delete-item="deleteEvent"
/> />
</v-col> </v-col>
<v-col cols="12" sm="12" lg="6">
<AdminBackupViewer v-if="backups" :available-backups="backups.imports" :templates="backups.templates" />
</v-col>
</v-row> </v-row>
</v-container> </v-container>
</template> </template>
@ -97,13 +94,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, useAsync } from "@nuxtjs/composition-api"; import { defineComponent, useAsync } from "@nuxtjs/composition-api";
import AdminEventViewer from "@/components/Domain/Admin/AdminEventViewer.vue"; import AdminEventViewer from "@/components/Domain/Admin/AdminEventViewer.vue";
import AdminBackupViewer from "@/components/Domain/Admin/AdminBackupViewer.vue";
import { useApiSingleton } from "~/composables/use-api"; import { useApiSingleton } from "~/composables/use-api";
import { useBackups } from "~/composables/use-backups";
import { useAsyncKey } from "~/composables/use-utils"; import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({ export default defineComponent({
components: { AdminEventViewer, AdminBackupViewer }, components: { AdminEventViewer },
layout: "admin", layout: "admin",
setup() { setup() {
const api = useApiSingleton(); const api = useApiSingleton();
@ -146,11 +141,10 @@ export default defineComponent({
} }
} }
const { backups } = useBackups();
const events = getEvents(); const events = getEvents();
const statistics = getStatistics(); const statistics = getStatistics();
return { statistics, events, deleteEvents, deleteEvent, backups }; return { statistics, events, deleteEvents, deleteEvent };
}, },
}); });
</script> </script>

View File

@ -1,5 +1,6 @@
// TODO: Add Loading Indicator...Maybe? // TODO: Add Loading Indicator...Maybe?
// TODO: Edit Group // TODO: Edit Group
// TODO: Migrate all stuff to setup()
<template> <template>
<v-container fluid> <v-container fluid>
<BaseCardSectionTitle title="Group Management"> </BaseCardSectionTitle> <BaseCardSectionTitle title="Group Management"> </BaseCardSectionTitle>
@ -77,9 +78,7 @@ export default defineComponent({
layout: "admin", layout: "admin",
setup() { setup() {
const api = useApiSingleton(); const api = useApiSingleton();
const { groups, refreshAllGroups, deleteGroup, createGroup } = useGroups(); const { groups, refreshAllGroups, deleteGroup, createGroup } = useGroups();
return { api, groups, refreshAllGroups, deleteGroup, createGroup }; return { api, groups, refreshAllGroups, deleteGroup, createGroup };
}, },
data() { data() {
@ -117,6 +116,3 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style scoped>
</style>

View File

@ -2,28 +2,30 @@
<v-container fluid> <v-container fluid>
<v-row> <v-row>
<v-col cols="12" sm="12" md="12" lg="6"> <v-col cols="12" sm="12" md="12" lg="6">
<UserProfileCard class="mt-14" /> <UserProfileCard :user="user" class="mt-14" @refresh="$auth.fetchUser()" />
</v-col> </v-col>
<v-col cols="12" sm="12" md="12" lg="6"> <v-col cols="12" sm="12" md="12" lg="6">
<UserThemeCard class="mt-14" /> <UserAPITokenCard :tokens="user.tokens" class="mt-14" @refresh="$auth.fetchUser()" />
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
import UserProfileCard from "~/components/Domain/User/UserProfileCard.vue"; import UserProfileCard from "~/components/Domain/User/UserProfileCard.vue";
import UserThemeCard from "~/components/Domain/User/UserThemeCard.vue"; import UserAPITokenCard from "~/components/Domain/User/UserAPITokenCard.vue";
export default defineComponent({ export default defineComponent({
components: { UserProfileCard, UserThemeCard }, components: { UserProfileCard, UserAPITokenCard },
layout: "admin", layout: "admin",
setup() { setup() {
return {}; const user = computed(() => {
return useContext().$auth.user;
});
return { user };
}, },
}); });
</script> </script>
<style scoped>
</style>

View File

@ -33,10 +33,10 @@ purge: clean ## ⚠️ Removes All Developer Data for a fresh server start
clean: clean-pyc clean-test ## 🧹 Remove all build, test, coverage and Python artifacts clean: clean-pyc clean-test ## 🧹 Remove all build, test, coverage and Python artifacts
clean-pyc: ## 🧹 Remove Python file artifacts clean-pyc: ## 🧹 Remove Python file artifacts
find . -name '*.pyc' -exec rm -f {} + find ./mealie -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} + find ./mealie -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} + find ./mealie -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -fr {} + find ./mealie -name '__pycache__' -exec rm -fr {} +
clean-test: ## 🧹 Remove test and coverage artifacts clean-test: ## 🧹 Remove test and coverage artifacts
rm -fr .tox/ rm -fr .tox/

View File

@ -11,9 +11,7 @@ router = APIRouter(tags=["Query All Recipes"])
@router.get("/api/recipes") @router.get("/api/recipes")
def get_recipe_summary( def get_recipe_summary(start=0, limit=9999, user: bool = Depends(is_logged_in)):
start=0, limit=9999, session: Session = Depends(generate_session), user: bool = Depends(is_logged_in)
):
""" """
Returns key the recipe summary data for recipes in the database. You can perform Returns key the recipe summary data for recipes in the database. You can perform
slice operations to set the skip/end amounts for recipes. All recipes are sorted by the added date. slice operations to set the skip/end amounts for recipes. All recipes are sorted by the added date.