Chore/general UI cleanup (#764)

* unify look and feel + button validators

* Fixes #741

* add github script to mealei-next

* feat(frontend): 💄 improve user-flow for creating ingredients and units in editor

Creating a unit/food in the recipe editor will not automatically assign that to the auto-complete element on the ingredient. It also no longer needs a dialog and will show at the bottom of the menu at all times.

* fix whitespace issue with slot

* add security check to properties

* fix event refresh on delete

* remove depreciated page

* improve API token flow

* hide recipe data if not advanced user

* misc adds

Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-10-30 15:46:44 -08:00 committed by GitHub
parent 2afaf70a03
commit 909bc85205
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 177 additions and 172 deletions

View file

@ -0,0 +1,32 @@
import json
import requests
from pydantic import BaseModel
class GithubIssue(BaseModel):
url: str
number: int
title: str
def get_issues_by_label(label="fixed-pending-release") -> list[GithubIssue]:
issues_url = f"https://api.github.com/repos/hay-kot/mealie/issues?labels={label}"
response = requests.get(issues_url)
issues = json.loads(response.text)
return [GithubIssue(**issue) for issue in issues]
def format_markdown_list(issues: list[GithubIssue]) -> str:
return "\n".join(f"- [{issue.number}]({issue.url}) - {issue.title}" for issue in issues)
def main() -> None:
issues = get_issues_by_label()
print(format_markdown_list(issues))
if __name__ == "__main__":
main()

View file

@ -16,6 +16,10 @@
- User/Group settings are now completely separated from the Administration page.
- All settings and configurations pages now have some sort of self-documenting help text. Additional text or descriptions are welcome from PRs
- Site settings now has status on whether or not some ENV variables have been configured correctly.
- Server Side Bare URL will let you know if the BASE_URL env variable has been set
- Secure Site let's you know if you're serving via HTTPS or accessing by localhost. accessing without a secure site will render some of the features unusable.
- Email Configuration Status will let you know if all the email settings have been provided and offer a way to send test emails.
### 👨‍👩‍👧‍👦 Users and Groups
- Recipes are now only viewable by group members
@ -33,10 +37,17 @@
- Add Recipes or Notes to a specific day
### 🥙 Recipes
- You can now import multiple URLs at a time pre-tagged using the bulk importer. This task runs in the background so no need to wait for it to finish.
- Foods/Units for Ingredients are now supported (toggle inside your recipe settings)
- You can no use Natural Language Processing (NLP) to process ingredients and attempt to parse them into amounts, units, and foods. There additional is a "Brute Force" processor that can be used to use a pattern matching parser to try and determine ingredients. **Note** if you are processing a Non-English language you will have terrible results with the NLP and will likely need to use the Bruce Force processor.
- Common Food and Units come pre-packaged with Mealie
- Recipes can now scale when Food/Units are properly defined
- Landscape and Portrait views is now available
- Users with the advanced flag turned on will not be able to manage recipe data in bulk and perform the following actions:
- Set Categories
- Set Tags
- Delete Recipes
- Export Recipes
### ⚠️ Other things to know...
- Themes have been depreciated for specific users. You can still set specific themes for your site through ENV variables. This approach should yield much better results for performance and some weirdness users have experienced.

View file

@ -30,6 +30,7 @@ export interface AdminStatistics {
export interface CheckAppConfig {
emailReady: boolean;
baseUrlSet: boolean;
isSiteSecure: boolean;
}
export class AdminAboutAPI extends BaseAPI {

View file

@ -15,6 +15,7 @@
:hint="hint"
:solo="solo"
:return-object="returnObject"
:prepend-inner-icon="$globals.icons.tags"
:flat="flat"
v-bind="$attrs"
@input="emitChange"

View file

@ -5,7 +5,7 @@
v-model="value.title"
dense
hide-details
class="mx-1 mb-4"
class="mx-1 mt-3 mb-4"
placeholder="Section Title"
style="max-width: 500px"
>
@ -29,6 +29,7 @@
<v-col v-if="!disableAmount && units" sm="12" md="3" cols="12">
<v-autocomplete
v-model="value.unit"
:search-input.sync="unitSearch"
hide-details
dense
solo
@ -38,14 +39,19 @@
class="mx-1"
placeholder="Choose Unit"
>
<template #no-data>
<RecipeIngredientUnitDialog class="mx-2" block small />
<template #append-item>
<div class="px-2">
<BaseButton block small @click="createAssignUnit()"></BaseButton>
</div>
</template>
</v-autocomplete>
</v-col>
<!-- Foods Input -->
<v-col v-if="!disableAmount && foods" m="12" md="3" cols="12" class="">
<v-autocomplete
v-model="value.food"
:search-input.sync="foodSearch"
hide-details
dense
solo
@ -55,8 +61,10 @@
class="mx-1 py-0"
placeholder="Choose Food"
>
<template #no-data>
<RecipeIngredientFoodDialog class="mx-2" block small />
<template #append-item>
<div class="px-2">
<BaseButton block small @click="createAssignFood()"></BaseButton>
</div>
</template>
</v-autocomplete>
</v-col>
@ -86,15 +94,12 @@
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from "@nuxtjs/composition-api";
import RecipeIngredientUnitDialog from "./RecipeIngredientUnitDialog.vue";
import RecipeIngredientFoodDialog from "./RecipeIngredientFoodDialog.vue";
import { defineComponent, reactive, ref, toRefs } from "@nuxtjs/composition-api";
import { useFoods } from "~/composables/use-recipe-foods";
import { useUnits } from "~/composables/use-recipe-units";
import { validators } from "~/composables/use-validators";
export default defineComponent({
components: { RecipeIngredientUnitDialog, RecipeIngredientFoodDialog },
props: {
value: {
type: Object,
@ -108,8 +113,28 @@ export default defineComponent({
setup(props) {
const { value } = props;
const { foods } = useFoods();
// ==================================================
// Foods
const { foods, workingFoodData, actions: foodActions } = useFoods();
const foodSearch = ref("");
async function createAssignFood() {
workingFoodData.name = foodSearch.value;
await foodActions.createOne();
value.food = foods.value?.find((food) => food.name === foodSearch.value);
}
// ==================================================
// Units
const { units, workingUnitData, actions: unitActions } = useUnits();
const unitSearch = ref("");
async function createAssignUnit() {
workingUnitData.name = unitSearch.value;
await unitActions.createOne();
value.unit = units.value?.find((unit) => unit.name === unitSearch.value);
console.log(value.unit);
}
const state = reactive({
showTitle: false,
@ -126,13 +151,17 @@ export default defineComponent({
}
return {
workingUnitData,
unitActions,
validators,
foods,
units,
...toRefs(state),
createAssignFood,
createAssignUnit,
foods,
foodSearch,
toggleTitle,
unitActions,
units,
unitSearch,
validators,
workingUnitData,
};
},
});

View file

@ -1,38 +0,0 @@
<template>
<BaseDialog
title="Create Food"
:icon="$globals.icons.foods"
:keep-open="!validForm"
@submit="actions.createOne(domCreateFoodForm)"
>
<v-card-text>
<v-form ref="domCreateFoodForm">
<v-text-field v-model="workingFoodData.name" label="Name" :rules="[validators.required]"></v-text-field>
<v-text-field v-model="workingFoodData.description" label="Description"></v-text-field>
</v-form>
</v-card-text>
<template #activator="{ open }">
<BaseButton
v-bind="$attrs"
@click="
actions.resetWorking();
open();
"
></BaseButton>
</template>
</BaseDialog>
</template>
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api";
import { useFoods } from "~/composables/use-recipe-foods";
import { validators } from "~/composables/use-validators";
export default defineComponent({
setup() {
const domCreateFoodForm = ref(null);
const { workingFoodData, actions, validForm } = useFoods();
return { validators, workingFoodData, actions, domCreateFoodForm, validForm };
},
});
</script>

View file

@ -1,40 +0,0 @@
<template>
<BaseDialog
title="Create Unit"
:icon="$globals.icons.units"
:keep-open="!validForm"
@submit="actions.createOne(domCreateUnitForm)"
>
<v-card-text>
<v-form ref="domCreateUnitForm">
<v-text-field v-model="workingUnitData.name" label="Name" :rules="[validators.required]"></v-text-field>
<v-text-field v-model="workingUnitData.abbreviation" label="Abbreviation"></v-text-field>
<v-text-field v-model="workingUnitData.description" label="Description"></v-text-field>
<v-switch v-model="workingUnitData.fraction" label="Display as Fraction"></v-switch>
</v-form>
</v-card-text>
<template #activator="{ open }">
<BaseButton
v-bind="$attrs"
@click="
actions.resetWorking();
open();
"
></BaseButton>
</template>
</BaseDialog>
</template>
<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api";
import { useUnits } from "~/composables/use-recipe-units";
import { validators } from "~/composables/use-validators";
export default defineComponent({
setup() {
const domCreateUnitForm = ref(null);
const { workingUnitData, actions, validForm } = useUnits();
return { validators, workingUnitData, actions, validForm, domCreateUnitForm };
},
});
</script>

View file

@ -1,5 +1,5 @@
<template>
<v-navigation-drawer :value="value" clipped app width="240px">
<v-navigation-drawer class="d-flex flex-column" :value="value" clipped app width="240px">
<!-- User Profile -->
<template v-if="$auth.user">
<v-list-item two-line to="/user/profile" exact>
@ -101,8 +101,8 @@
</template>
<!-- Bottom Navigation Links -->
<template v-if="bottomLinks">
<v-list class="fixedBottom" nav dense>
<template v-if="bottomLinks" #append>
<v-list nav dense>
<v-list-item-group v-model="bottomSelected" color="primary">
<template v-for="nav in bottomLinks">
<v-list-item
@ -175,12 +175,6 @@ export default defineComponent({
</script>
<style>
.fixedBottom {
position: fixed !important;
bottom: 0 !important;
width: 100%;
}
@media print {
.no-print {
display: none;

View file

@ -6,7 +6,7 @@
</v-icon>
{{ title }}
</v-card-title>
<v-card-text class="pt-2">
<v-card-text v-if="$slots.default" class="pt-2">
<p class="pb-0 mb-0">
<slot />
</p>

View file

@ -86,11 +86,6 @@ export default defineComponent({
to: "/admin/toolbox/tags",
title: i18n.t("sidebar.categories"),
},
{
icon: $globals.icons.broom,
to: "/admin/toolbox/organize",
title: i18n.t("settings.organize"),
},
],
},
{

View file

@ -137,7 +137,7 @@ export default defineComponent({
const { response } = await api.events.deleteEvents();
if (response && response.status === 200) {
events.value = { events: [], total: 0 };
refreshEvents();
}
}

View file

@ -2,29 +2,25 @@
<v-container fluid class="narrow-container">
<BasePageTitle divider>
<template #header>
<v-img
max-height="200"
max-width="150"
class="mb-2"
:src="require('~/static/svgs/admin-site-settings.svg')"
></v-img>
<v-img max-height="200" max-width="150" :src="require('~/static/svgs/admin-site-settings.svg')"></v-img>
</template>
<template #title> {{ $t("settings.site-settings") }} </template>
</BasePageTitle>
<section>
<BaseCardSectionTitle :icon="$globals.icons.cog" title="General Configuration"> </BaseCardSectionTitle>
<v-card class="mb-4">
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" title="General Configuration">
</BaseCardSectionTitle>
<v-card v-for="(check, idx) in simpleChecks" :key="idx" class="mb-4">
<v-list-item>
<v-list-item-avatar>
<v-icon :color="getColor(appConfig.baseUrlSet)">
{{ appConfig.baseUrlSet ? $globals.icons.checkboxMarkedCircle : $globals.icons.close }}
<v-icon :color="getColor(check.status)">
{{ check.status ? $globals.icons.checkboxMarkedCircle : $globals.icons.close }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title :class="getTextClass(appConfig.baseUrlSet)"> Server Side Base URL </v-list-item-title>
<v-list-item-subtitle :class="getTextClass(appConfig.baseUrlSet)">
{{ appConfig.baseUrlSet ? "Ready" : "Not Ready - `BASE_URL` still default on API Server" }}
<v-list-item-title :class="getTextClass(check.status)"> {{ check.text }} </v-list-item-title>
<v-list-item-subtitle :class="getTextClass(check.status)">
{{ check.status ? check.successText : check.errorText }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
@ -82,6 +78,13 @@ import { CheckAppConfig } from "~/api/admin/admin-about";
import { useAdminApi, useApiSingleton } from "~/composables/use-api";
import { validators } from "~/composables/use-validators";
interface SimpleCheck {
status: boolean;
text: string;
successText: string;
errorText: string;
}
export default defineComponent({
layout: "admin",
setup() {
@ -96,6 +99,7 @@ export default defineComponent({
const appConfig = ref<CheckAppConfig>({
emailReady: false,
baseUrlSet: false,
isSiteSecure: false,
});
const api = useApiSingleton();
@ -107,6 +111,29 @@ export default defineComponent({
if (data) {
appConfig.value = data;
}
appConfig.value.isSiteSecure = isLocalhostorHttps();
});
function isLocalhostorHttps() {
return window.location.hostname === "localhost" || window.location.protocol === "https:";
}
const simpleChecks = computed<SimpleCheck[]>(() => {
return [
{
status: appConfig.value.baseUrlSet,
text: "Server Side Base URL",
errorText: "Error - `BASE_URL` still default on API Server",
successText: "Server Side URL does not match the default",
},
{
status: appConfig.value.isSiteSecure,
text: "Secure Site",
errorText: "Error - Serve via localhost or secure with https.",
successText: "Site is accessed by localhost or https",
},
];
});
async function testEmail() {
@ -147,6 +174,7 @@ export default defineComponent({
}
return {
simpleChecks,
getColor,
getTextClass,
appConfig,

View file

@ -1,24 +0,0 @@
<template>
<v-container fluid>
<BaseCardSectionTitle title="Organize Recipes"> </BaseCardSectionTitle>
</v-container>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
layout: "admin",
setup() {
return {};
},
head() {
return {
title: this.$t("settings.organize") as string,
};
},
});
</script>
<style scoped>
</style>

View file

@ -27,6 +27,7 @@
<v-text-field
v-model="recipeUrl"
:label="$t('new-recipe.recipe-url')"
:prepend-inner-icon="$globals.icons.link"
validate-on-blur
autofocus
filled
@ -40,7 +41,7 @@
</v-card-text>
<v-card-actions class="justify-center">
<div style="width: 250px">
<BaseButton rounded block type="submit" :loading="loading" />
<BaseButton :disabled="recipeUrl === null" rounded block type="submit" :loading="loading" />
</div>
</v-card-actions>
</v-card>
@ -86,6 +87,7 @@
<v-text-field
v-model="newRecipeName"
:label="$t('recipe.recipe-name')"
:prepend-inner-icon="$globals.icons.primary"
validate-on-blur
autofocus
filled
@ -100,7 +102,13 @@
</v-card-text>
<v-card-actions class="justify-center">
<div style="width: 250px">
<BaseButton rounded block :loading="loading" @click="createByName(newRecipeName)" />
<BaseButton
:disabled="newRecipeName === ''"
rounded
block
:loading="loading"
@click="createByName(newRecipeName)"
/>
</div>
</v-card-actions>
</v-card>
@ -131,7 +139,14 @@
</v-card-text>
<v-card-actions class="justify-center">
<div style="width: 250px">
<BaseButton large rounded block :loading="loading" @click="createByZip" />
<BaseButton
:disabled="newRecipeZip === null"
large
rounded
block
:loading="loading"
@click="createByZip"
/>
</div>
</v-card-actions>
</v-card>
@ -142,7 +157,7 @@
<v-tab-item value="debug" eager>
<v-form ref="domUrlForm" @submit.prevent="debugUrl(recipeUrl)">
<v-card flat>
<v-card-title class="headline"> Recipe Importer </v-card-title>
<v-card-title class="headline"> Recipe Debugger </v-card-title>
<v-card-text>
Grab the URL of the recipe you want to debug and paste it here. The URL will be scraped by the recipe
scraper and the results will be displayed. If you don't see any data returned, the site you are trying
@ -151,6 +166,7 @@
v-model="recipeUrl"
:label="$t('new-recipe.recipe-url')"
validate-on-blur
:prepend-inner-icon="$globals.icons.link"
autofocus
filled
clearable
@ -163,7 +179,14 @@
</v-card-text>
<v-card-actions class="justify-center">
<div style="width: 250px">
<BaseButton rounded block type="submit" color="info" :loading="loading">
<BaseButton
:disabled="recipeUrl === null"
rounded
block
type="submit"
color="info"
:loading="loading"
>
<template #icon>
{{ $globals.icons.robot }}
</template>

View file

@ -20,9 +20,7 @@
class="mb-0 pb-0"
:label="$t('settings.token.api-token')"
readonly
:append-outer-icon="$globals.icons.contentCopy"
@click="copyToken"
@click:append-outer="copyToken"
rows="3"
>
</v-textarea>
<v-subheader class="text-center">
@ -34,13 +32,14 @@
</v-subheader>
</template>
</v-card-text>
<v-expand-transition>
<v-card-actions v-show="name != ''">
<v-spacer></v-spacer>
<v-card-actions>
<BaseButton v-if="createdToken" cancel @click="resetCreate()"> Close </BaseButton>
<BaseButton v-else :cancel="false" @click="createToken(name)"> Generate </BaseButton>
<v-spacer></v-spacer>
<AppButtonCopy v-if="createdToken" :icon="false" color="info" :copy-text="createdToken"> </AppButtonCopy>
<BaseButton v-else key="generate-button" :disabled="name == ''" @click="createToken(name)">
Generate
</BaseButton>
</v-card-actions>
</v-expand-transition>
</v-card>
</section>
<BaseCardSectionTitle class="mt-10" title="Active Tokens"> </BaseCardSectionTitle>
@ -117,14 +116,7 @@ export default defineComponent({
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, user, resetCreate };
return { createToken, deleteToken, createdToken, loading, name, user, resetCreate };
},
head() {
return {

View file

@ -108,7 +108,7 @@
See who's in your group and manage their permissions.
</UserProfileLinkCard>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Manage Recipe Data', to: '/user/group/recipe-data' }"
:image="require('~/static/svgs/manage-recipes.svg')"

View file

@ -24,7 +24,8 @@ async def get_events(session: Session = Depends(generate_session)):
async def delete_events(session: Session = Depends(generate_session)):
""" Get event from the Database """
db = get_database(session)
return db.events.delete_all()
db.events.delete_all()
return {"message": "All events deleted"}
@router.delete("/{id}")