mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-04 21:15:22 +02:00
feat: "I Made This" Dialog (#1801)
* added chef hat * removed unnecessary log * modified recipe and recipe timeline event schema changed timeline event "message" -> "event_message" added "last made" timestamp to recipe * added "I made this" dialog to recipe action menu * added missing field and re-ran code-gen * moved dialog out of context menu and refactored removed references in action menu and context menu refactored dialog to be triggered by a button instead added route to update recipe last made timestamp added visual for last made timestamp to recipe header and title * added sorting by last made * switched event type to comment * replaced alter column with pydantic alias * added tests for event message alias
This commit is contained in:
parent
f0e6496001
commit
a2dcdc1adf
20 changed files with 275 additions and 9 deletions
|
@ -0,0 +1,28 @@
|
||||||
|
"""added recipe last made timestamp
|
||||||
|
|
||||||
|
Revision ID: 1923519381ad
|
||||||
|
Revises: 2ea7a807915c
|
||||||
|
Create Date: 2022-11-03 13:10:24.811134
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "1923519381ad"
|
||||||
|
down_revision = "2ea7a807915c"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column("recipes", sa.Column("last_made", sa.DateTime(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column("recipes", "last_made")
|
||||||
|
# ### end Alembic commands ###
|
|
@ -49,6 +49,12 @@
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
|
<v-list-item-title>{{ $t("general.updated") }}</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<v-list-item @click="sortRecipes(EVENTS.lastMade)">
|
||||||
|
<v-icon left>
|
||||||
|
{{ $globals.icons.chefHat }}
|
||||||
|
</v-icon>
|
||||||
|
<v-list-item-title>{{ "Last Made" }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
|
@ -186,6 +192,7 @@ export default defineComponent({
|
||||||
rating: "rating",
|
rating: "rating",
|
||||||
created: "created",
|
created: "created",
|
||||||
updated: "updated",
|
updated: "updated",
|
||||||
|
lastMade: "lastMade",
|
||||||
shuffle: "shuffle",
|
shuffle: "shuffle",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -303,6 +310,9 @@ export default defineComponent({
|
||||||
case EVENTS.updated:
|
case EVENTS.updated:
|
||||||
setter("update_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending);
|
setter("update_at", $globals.icons.sortClockAscending, $globals.icons.sortClockDescending);
|
||||||
break;
|
break;
|
||||||
|
case EVENTS.lastMade:
|
||||||
|
setter("last_made", $globals.icons.sortCalendarAscending, $globals.icons.sortCalendarDescending);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Unknown Event", sortType);
|
console.log("Unknown Event", sortType);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
readonly
|
readonly
|
||||||
v-on="on"
|
v-on="on"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<v-date-picker v-model="newMealdate" no-title @input="pickerMenu = false"></v-date-picker>
|
<v-date-picker v-model="newMealdate" no-title @input="pickerMenu = false"></v-date-picker>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
|
|
@ -84,7 +84,6 @@ export default defineComponent({
|
||||||
return props.value;
|
return props.value;
|
||||||
},
|
},
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
console.log(val);
|
|
||||||
context.emit("input", val);
|
context.emit("input", val);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
143
frontend/components/Domain/Recipe/RecipeLastMade.vue
Normal file
143
frontend/components/Domain/Recipe/RecipeLastMade.vue
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<BaseDialog
|
||||||
|
v-model="madeThisDialog"
|
||||||
|
:icon="$globals.icons.chefHat"
|
||||||
|
title="I Made This"
|
||||||
|
:submit-text="$tc('general.save')"
|
||||||
|
@submit="createTimelineEvent"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form ref="domMadeThisForm">
|
||||||
|
<v-textarea
|
||||||
|
v-model="newTimelineEvent.eventMessage"
|
||||||
|
autofocus
|
||||||
|
label="Comment"
|
||||||
|
hint="How did it turn out?"
|
||||||
|
persistent-hint
|
||||||
|
rows="4"
|
||||||
|
></v-textarea>
|
||||||
|
<v-menu
|
||||||
|
v-model="datePickerMenu"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
transition="scale-transition"
|
||||||
|
offset-y
|
||||||
|
max-width="290px"
|
||||||
|
min-width="auto"
|
||||||
|
>
|
||||||
|
<template #activator="{ on, attrs }">
|
||||||
|
<v-text-field
|
||||||
|
v-model="newTimelineEvent.timestamp"
|
||||||
|
:prepend-icon="$globals.icons.calendar"
|
||||||
|
v-bind="attrs"
|
||||||
|
readonly
|
||||||
|
v-on="on"
|
||||||
|
></v-text-field>
|
||||||
|
</template>
|
||||||
|
<v-date-picker v-model="newTimelineEvent.timestamp" no-title @input="datePickerMenu = false"></v-date-picker>
|
||||||
|
</v-menu>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</BaseDialog>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<v-chip
|
||||||
|
label
|
||||||
|
color="accent custom-transparent"
|
||||||
|
class="ma-1"
|
||||||
|
style="height:100%;"
|
||||||
|
>
|
||||||
|
<v-icon left>
|
||||||
|
{{ $globals.icons.calendar }}
|
||||||
|
</v-icon>
|
||||||
|
Last Made {{ value ? new Date(value).toLocaleDateString($i18n.locale) : "Never" }}
|
||||||
|
</v-chip>
|
||||||
|
<BaseButton @click="madeThisDialog = true">
|
||||||
|
<template #icon> {{ $globals.icons.chefHat }} </template>
|
||||||
|
I Made This
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, ref, toRefs, useContext, } from "@nuxtjs/composition-api";
|
||||||
|
import { whenever } from "@vueuse/core";
|
||||||
|
import { VForm } from "~/types/vuetify";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { RecipeTimelineEventIn } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
recipeSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, context) {
|
||||||
|
const madeThisDialog = ref(false);
|
||||||
|
const userApi = useUserApi();
|
||||||
|
const { $auth } = useContext();
|
||||||
|
const domMadeThisForm = ref<VForm>();
|
||||||
|
const newTimelineEvent = ref<RecipeTimelineEventIn>({
|
||||||
|
// @ts-expect-error - TS doesn't like the $auth global user attribute
|
||||||
|
// eslint-disable-next-line
|
||||||
|
subject: `${$auth.user.fullName} made this`,
|
||||||
|
eventType: "comment",
|
||||||
|
eventMessage: "",
|
||||||
|
timestamp: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({datePickerMenu: false});
|
||||||
|
|
||||||
|
whenever(
|
||||||
|
() => madeThisDialog.value,
|
||||||
|
() => {
|
||||||
|
// Set timestamp to now
|
||||||
|
newTimelineEvent.value.timestamp = new Date().toISOString().substring(0, 10);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function createTimelineEvent() {
|
||||||
|
if (!newTimelineEvent.value.timestamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: Promise<any>[] = []
|
||||||
|
|
||||||
|
// the user only selects the date, so we set the time to noon
|
||||||
|
newTimelineEvent.value.timestamp += "T12:00:00";
|
||||||
|
actions.push(userApi.recipes.createTimelineEvent(props.recipeSlug, newTimelineEvent.value));
|
||||||
|
|
||||||
|
// we also update the recipe's last made value
|
||||||
|
if (!props.value || newTimelineEvent.value.timestamp > props.value) {
|
||||||
|
const payload = {lastMade: newTimelineEvent.value.timestamp};
|
||||||
|
actions.push(userApi.recipes.patchOne(props.recipeSlug, payload));
|
||||||
|
|
||||||
|
// update recipe in parent so the user can see it
|
||||||
|
context.emit("input", newTimelineEvent.value.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(actions)
|
||||||
|
|
||||||
|
// reset form
|
||||||
|
newTimelineEvent.value.eventMessage = "";
|
||||||
|
madeThisDialog.value = false;
|
||||||
|
domMadeThisForm.value?.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
domMadeThisForm,
|
||||||
|
madeThisDialog,
|
||||||
|
newTimelineEvent,
|
||||||
|
createTimelineEvent,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -10,6 +10,14 @@
|
||||||
<v-divider class="my-2"></v-divider>
|
<v-divider class="my-2"></v-divider>
|
||||||
<SafeMarkdown :source="recipe.description" />
|
<SafeMarkdown :source="recipe.description" />
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
<div v-if="user.id" class="d-flex justify-center mt-5">
|
||||||
|
<RecipeLastMade
|
||||||
|
v-model="recipe.lastMade"
|
||||||
|
:recipe-slug="recipe.slug"
|
||||||
|
class="d-flex justify-center flex-wrap"
|
||||||
|
:class="true ? undefined : 'force-bottom'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="d-flex justify-center mt-5">
|
<div class="d-flex justify-center mt-5">
|
||||||
<RecipeTimeCard
|
<RecipeTimeCard
|
||||||
class="d-flex justify-center flex-wrap"
|
class="d-flex justify-center flex-wrap"
|
||||||
|
@ -58,6 +66,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useContext, computed, ref, watch, useRouter } from "@nuxtjs/composition-api";
|
import { defineComponent, useContext, computed, ref, watch, useRouter } from "@nuxtjs/composition-api";
|
||||||
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
|
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
|
||||||
|
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
|
||||||
import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue";
|
import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue";
|
||||||
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
||||||
import { useStaticRoutes } from "~/composables/api";
|
import { useStaticRoutes } from "~/composables/api";
|
||||||
|
@ -69,6 +78,7 @@ export default defineComponent({
|
||||||
RecipeTimeCard,
|
RecipeTimeCard,
|
||||||
RecipeActionMenu,
|
RecipeActionMenu,
|
||||||
RecipeRating,
|
RecipeRating,
|
||||||
|
RecipeLastMade,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
recipe: {
|
recipe: {
|
||||||
|
|
|
@ -5,6 +5,14 @@
|
||||||
{{ recipe.name }}
|
{{ recipe.name }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<SafeMarkdown :source="recipe.description" />
|
<SafeMarkdown :source="recipe.description" />
|
||||||
|
<div v-if="user.id" class="pb-2 d-flex justify-center flex-wrap">
|
||||||
|
<RecipeLastMade
|
||||||
|
v-model="recipe.lastMade"
|
||||||
|
:recipe-slug="recipe.slug"
|
||||||
|
class="d-flex justify-center flex-wrap"
|
||||||
|
:class="true ? undefined : 'force-bottom'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="pb-2 d-flex justify-center flex-wrap">
|
<div class="pb-2 d-flex justify-center flex-wrap">
|
||||||
<RecipeTimeCard
|
<RecipeTimeCard
|
||||||
class="d-flex justify-center flex-wrap"
|
class="d-flex justify-center flex-wrap"
|
||||||
|
@ -48,11 +56,13 @@ import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import { Recipe } from "~/lib/api/types/recipe";
|
import { Recipe } from "~/lib/api/types/recipe";
|
||||||
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
|
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
|
||||||
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
||||||
|
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
RecipeRating,
|
RecipeRating,
|
||||||
RecipeTimeCard,
|
RecipeTimeCard,
|
||||||
|
RecipeLastMade,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
recipe: {
|
recipe: {
|
||||||
|
|
|
@ -103,6 +103,7 @@ export interface RecipeSummary {
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
|
lastMade?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
|
@ -88,6 +88,7 @@ export interface RecipeSummary {
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
|
lastMade?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
|
@ -396,6 +396,7 @@ export interface RecipeSummary {
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
|
lastMade?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
|
@ -118,6 +118,7 @@ export interface RecipeSummary {
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
|
lastMade?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
|
@ -211,6 +211,7 @@ export interface Recipe {
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
|
lastMade?: string;
|
||||||
recipeInstructions?: RecipeStep[];
|
recipeInstructions?: RecipeStep[];
|
||||||
nutrition?: Nutrition;
|
nutrition?: Nutrition;
|
||||||
settings?: RecipeSettings;
|
settings?: RecipeSettings;
|
||||||
|
@ -286,6 +287,7 @@ export interface RecipeSummary {
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
|
lastMade?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCommentCreate {
|
export interface RecipeCommentCreate {
|
||||||
recipeId: string;
|
recipeId: string;
|
||||||
|
@ -345,7 +347,7 @@ export interface RecipeTimelineEventCreate {
|
||||||
userId: string;
|
userId: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
eventType: TimelineEventType;
|
eventType: TimelineEventType;
|
||||||
message?: string;
|
eventMessage?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
recipeId: string;
|
recipeId: string;
|
||||||
|
@ -354,7 +356,7 @@ export interface RecipeTimelineEventIn {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
eventType: TimelineEventType;
|
eventType: TimelineEventType;
|
||||||
message?: string;
|
eventMessage?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
}
|
}
|
||||||
|
@ -362,7 +364,7 @@ export interface RecipeTimelineEventOut {
|
||||||
userId: string;
|
userId: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
eventType: TimelineEventType;
|
eventType: TimelineEventType;
|
||||||
message?: string;
|
eventMessage?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
recipeId: string;
|
recipeId: string;
|
||||||
|
@ -372,7 +374,7 @@ export interface RecipeTimelineEventOut {
|
||||||
}
|
}
|
||||||
export interface RecipeTimelineEventUpdate {
|
export interface RecipeTimelineEventUpdate {
|
||||||
subject: string;
|
subject: string;
|
||||||
message?: string;
|
eventMessage?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeToolCreate {
|
export interface RecipeToolCreate {
|
||||||
|
|
|
@ -192,6 +192,7 @@ export interface RecipeSummary {
|
||||||
dateUpdated?: string;
|
dateUpdated?: string;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updateAt?: string;
|
updateAt?: string;
|
||||||
|
lastMade?: string;
|
||||||
}
|
}
|
||||||
export interface RecipeCategory {
|
export interface RecipeCategory {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ParsedIngredient,
|
ParsedIngredient,
|
||||||
UpdateImageResponse,
|
UpdateImageResponse,
|
||||||
RecipeZipTokenResponse,
|
RecipeZipTokenResponse,
|
||||||
|
RecipeTimelineEventIn,
|
||||||
} from "~/lib/api/types/recipe";
|
} from "~/lib/api/types/recipe";
|
||||||
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
import { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
|
@ -44,6 +45,9 @@ const routes = {
|
||||||
|
|
||||||
recipesSlugComments: (slug: string) => `${prefix}/recipes/${slug}/comments`,
|
recipesSlugComments: (slug: string) => `${prefix}/recipes/${slug}/comments`,
|
||||||
recipesSlugCommentsId: (slug: string, id: number) => `${prefix}/recipes/${slug}/comments/${id}`,
|
recipesSlugCommentsId: (slug: string, id: number) => `${prefix}/recipes/${slug}/comments/${id}`,
|
||||||
|
|
||||||
|
recipesSlugTimelineEvent: (slug: string) => `${prefix}/recipes/${slug}/timeline/events`,
|
||||||
|
recipesSlugTimelineEventId: (slug: string, id: number) => `${prefix}/recipes/${slug}/timeline/events/${id}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
||||||
|
@ -126,4 +130,8 @@ export class RecipeAPI extends BaseCRUDAPI<CreateRecipe, Recipe, Recipe> {
|
||||||
|
|
||||||
return await this.requests.post(routes.recipesCreateFromOcr, formData);
|
return await this.requests.post(routes.recipesCreateFromOcr, formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createTimelineEvent(recipeSlug: string, payload: RecipeTimelineEventIn) {
|
||||||
|
return await this.requests.post(routes.recipesSlugTimelineEvent(recipeSlug), payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,6 +122,7 @@ import {
|
||||||
mdiCursorMove,
|
mdiCursorMove,
|
||||||
mdiText,
|
mdiText,
|
||||||
mdiTextBoxOutline,
|
mdiTextBoxOutline,
|
||||||
|
mdiChefHat,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
|
|
||||||
export const icons = {
|
export const icons = {
|
||||||
|
@ -157,6 +158,7 @@ export const icons = {
|
||||||
check: mdiCheck,
|
check: mdiCheck,
|
||||||
checkboxBlankOutline: mdiCheckboxBlankOutline,
|
checkboxBlankOutline: mdiCheckboxBlankOutline,
|
||||||
checkboxMarkedCircle: mdiCheckboxMarkedCircle,
|
checkboxMarkedCircle: mdiCheckboxMarkedCircle,
|
||||||
|
chefHat: mdiChefHat,
|
||||||
clipboardCheck: mdiClipboardCheck,
|
clipboardCheck: mdiClipboardCheck,
|
||||||
clockOutline: mdiClockTimeFourOutline,
|
clockOutline: mdiClockTimeFourOutline,
|
||||||
codeBraces: mdiCodeJson,
|
codeBraces: mdiCodeJson,
|
||||||
|
|
|
@ -99,6 +99,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
|
||||||
# Time Stamp Properties
|
# Time Stamp Properties
|
||||||
date_added = sa.Column(sa.Date, default=datetime.date.today)
|
date_added = sa.Column(sa.Date, default=datetime.date.today)
|
||||||
date_updated = sa.Column(sa.DateTime)
|
date_updated = sa.Column(sa.DateTime)
|
||||||
|
last_made = sa.Column(sa.DateTime)
|
||||||
|
|
||||||
# Shopping List Refs
|
# Shopping List Refs
|
||||||
shopping_list_refs = orm.relationship(
|
shopping_list_refs = orm.relationship(
|
||||||
|
|
|
@ -98,6 +98,7 @@ class RecipeSummary(MealieModel):
|
||||||
|
|
||||||
created_at: datetime.datetime | None
|
created_at: datetime.datetime | None
|
||||||
update_at: datetime.datetime | None
|
update_at: datetime.datetime | None
|
||||||
|
last_made: datetime.datetime | None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4, Field
|
||||||
|
|
||||||
from mealie.schema._mealie.mealie_model import MealieModel
|
from mealie.schema._mealie.mealie_model import MealieModel
|
||||||
from mealie.schema.response.pagination import PaginationBase
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
@ -20,7 +20,7 @@ class RecipeTimelineEventIn(MealieModel):
|
||||||
subject: str
|
subject: str
|
||||||
event_type: TimelineEventType
|
event_type: TimelineEventType
|
||||||
|
|
||||||
message: str | None = None
|
message: str | None = Field(alias="eventMessage")
|
||||||
image: str | None = None
|
image: str | None = None
|
||||||
|
|
||||||
timestamp: datetime = datetime.now()
|
timestamp: datetime = datetime.now()
|
||||||
|
@ -36,7 +36,7 @@ class RecipeTimelineEventCreate(RecipeTimelineEventIn):
|
||||||
|
|
||||||
class RecipeTimelineEventUpdate(MealieModel):
|
class RecipeTimelineEventUpdate(MealieModel):
|
||||||
subject: str
|
subject: str
|
||||||
message: str | None = None
|
message: str | None = Field(alias="eventMessage")
|
||||||
image: str | None = None
|
image: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -176,6 +176,52 @@ def test_delete_timeline_event(api_client: TestClient, unique_user: TestUser, re
|
||||||
assert event_response.status_code == 404
|
assert event_response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeline_event_message_alias(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
||||||
|
# create an event using aliases
|
||||||
|
recipe = recipes[0]
|
||||||
|
new_event_data = {
|
||||||
|
"userId": unique_user.user_id,
|
||||||
|
"subject": random_string(),
|
||||||
|
"eventType": "info",
|
||||||
|
"eventMessage": random_string(), # eventMessage is the correct alias for the message
|
||||||
|
}
|
||||||
|
|
||||||
|
event_response = api_client.post(
|
||||||
|
api_routes.recipes_slug_timeline_events(recipe.slug),
|
||||||
|
json=new_event_data,
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
new_event = RecipeTimelineEventOut.parse_obj(event_response.json())
|
||||||
|
assert str(new_event.user_id) == new_event_data["userId"]
|
||||||
|
assert str(new_event.event_type) == new_event_data["eventType"]
|
||||||
|
assert new_event.message == new_event_data["eventMessage"]
|
||||||
|
|
||||||
|
# fetch the new event
|
||||||
|
event_response = api_client.get(
|
||||||
|
api_routes.recipes_slug_timeline_events_item_id(recipe.slug, new_event.id), headers=unique_user.token
|
||||||
|
)
|
||||||
|
assert event_response.status_code == 200
|
||||||
|
|
||||||
|
event = RecipeTimelineEventOut.parse_obj(event_response.json())
|
||||||
|
assert event == new_event
|
||||||
|
|
||||||
|
# update the event message
|
||||||
|
new_subject = random_string()
|
||||||
|
new_message = random_string()
|
||||||
|
updated_event_data = {"subject": new_subject, "eventMessage": new_message}
|
||||||
|
|
||||||
|
event_response = api_client.put(
|
||||||
|
api_routes.recipes_slug_timeline_events_item_id(recipe.slug, new_event.id),
|
||||||
|
json=updated_event_data,
|
||||||
|
headers=unique_user.token,
|
||||||
|
)
|
||||||
|
assert event_response.status_code == 200
|
||||||
|
|
||||||
|
updated_event = RecipeTimelineEventOut.parse_obj(event_response.json())
|
||||||
|
assert updated_event.subject == new_subject
|
||||||
|
assert updated_event.message == new_message
|
||||||
|
|
||||||
|
|
||||||
def test_create_recipe_with_timeline_event(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
def test_create_recipe_with_timeline_event(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
||||||
# make sure when the recipes fixture was created that all recipes have at least one event
|
# make sure when the recipes fixture was created that all recipes have at least one event
|
||||||
for recipe in recipes:
|
for recipe in recipes:
|
||||||
|
|
|
@ -4,7 +4,7 @@ from mealie.core.config import get_app_settings
|
||||||
from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
|
from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
|
||||||
|
|
||||||
ALEMBIC_VERSIONS = [
|
ALEMBIC_VERSIONS = [
|
||||||
{"version_num": "2ea7a807915c"},
|
{"version_num": "1923519381ad"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue