From 4e8e2d751095fd2020fcae94191e0ff73da57c4d Mon Sep 17 00:00:00 2001
From: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Date: Sun, 11 Dec 2022 15:16:55 -0600
Subject: [PATCH] Feat/recipe timeline event UI (#1831)
* added new icons
* added timeline badge and dialog to action menu
* more icons
* implemented timeline dialog using temporary API
* added route for fetching all timeline events
* formalized API call and added mobile-friendly view
* cleaned tags
* improved last made UI for mobile
* added event context menu with placeholder methods
* adjusted default made this date
set time to 1 minute before midnight
adjusted display to properly interpret UTC
* fixed local date display
* implemented update/delete routes
* fixed formating for long subjects
* added api error handling
* made everything localizable
* fixed weird formatting
* removed unnecessary async
* combined mobile/desktop views w/ conditional attrs
---
.../Domain/Recipe/RecipeActionMenu.vue | 5 +-
.../Domain/Recipe/RecipeDialogTimeline.vue | 245 ++++++++++++++++
.../Domain/Recipe/RecipeLastMade.vue | 271 +++++++++---------
.../Domain/Recipe/RecipeTimelineBadge.vue | 53 ++++
.../Recipe/RecipeTimelineContextMenu.vue | 206 +++++++++++++
frontend/lang/messages/en-US.json | 16 +-
frontend/lib/api/user/recipes/recipe.ts | 22 +-
frontend/lib/icons/icons.ts | 8 +
8 files changed, 692 insertions(+), 134 deletions(-)
create mode 100644 frontend/components/Domain/Recipe/RecipeDialogTimeline.vue
create mode 100644 frontend/components/Domain/Recipe/RecipeTimelineBadge.vue
create mode 100644 frontend/components/Domain/Recipe/RecipeTimelineContextMenu.vue
diff --git a/frontend/components/Domain/Recipe/RecipeActionMenu.vue b/frontend/components/Domain/Recipe/RecipeActionMenu.vue
index f2eaf976..a0745335 100644
--- a/frontend/components/Domain/Recipe/RecipeActionMenu.vue
+++ b/frontend/components/Domain/Recipe/RecipeActionMenu.vue
@@ -22,6 +22,7 @@
+
@@ -70,7 +71,6 @@
:key="index"
:fab="$vuetify.breakpoint.xs"
:small="$vuetify.breakpoint.xs"
- class="mx-1"
:color="btn.color"
@click="emitHandler(btn.event)"
>
@@ -85,6 +85,7 @@
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
+import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
import { Recipe } from "~/lib/api/types/recipe";
const SAVE_EVENT = "save";
@@ -94,7 +95,7 @@ const JSON_EVENT = "json";
const OCR_EVENT = "ocr";
export default defineComponent({
- components: { RecipeContextMenu, RecipeFavoriteBadge },
+ components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimelineBadge },
props: {
recipe: {
required: true,
diff --git a/frontend/components/Domain/Recipe/RecipeDialogTimeline.vue b/frontend/components/Domain/Recipe/RecipeDialogTimeline.vue
new file mode 100644
index 00000000..e2075972
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipeDialogTimeline.vue
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+ {{ $globals.icons.calendar }}
+ {{ new Date(event.timestamp+"Z").toLocaleDateString($i18n.locale) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $globals.icons.calendar }}
+ {{ new Date(event.timestamp+"Z").toLocaleDateString($i18n.locale) }}
+
+
+
+ {{ event.subject }}
+
+
+
+
+
+
+
+
+
+
+ {{ event.subject }}
+
+ {{ event.eventMessage }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("recipe.timeline-is-empty") }}
+
+
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipeLastMade.vue b/frontend/components/Domain/Recipe/RecipeLastMade.vue
index 948650d0..61805cba 100644
--- a/frontend/components/Domain/Recipe/RecipeLastMade.vue
+++ b/frontend/components/Domain/Recipe/RecipeLastMade.vue
@@ -1,143 +1,158 @@
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $globals.icons.calendar }}
-
- Last Made {{ value ? new Date(value).toLocaleDateString($i18n.locale) : "Never" }}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $globals.icons.chefHat }}
I Made This
+
+
+
+ {{ $globals.icons.calendar }}
+
+ Last Made {{ value ? new Date(value+"Z").toLocaleDateString($i18n.locale) : $t("general.never") }}
+
+
-
+
+
-
+ const actions: Promise
[] = [];
+
+ // the user only selects the date, so we set the time to end of day local time
+ // we choose the end of day so it always comes after "new recipe" events
+ newTimelineEvent.value.timestamp = new Date(newTimelineEvent.value.timestamp + "T23:59:59").toISOString();
+ 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
+ // we remove the trailing "Z" since this is how the API returns it
+ context.emit(
+ "input", newTimelineEvent.value.timestamp
+ .substring(0, newTimelineEvent.value.timestamp.length - 1)
+ );
+ }
+
+ await Promise.allSettled(actions);
+
+ // reset form
+ newTimelineEvent.value.eventMessage = "";
+ madeThisDialog.value = false;
+ domMadeThisForm.value?.reset();
+ }
+
+ return {
+ ...toRefs(state),
+ domMadeThisForm,
+ madeThisDialog,
+ newTimelineEvent,
+ createTimelineEvent,
+ };
+ },
+});
+
diff --git a/frontend/components/Domain/Recipe/RecipeTimelineBadge.vue b/frontend/components/Domain/Recipe/RecipeTimelineBadge.vue
new file mode 100644
index 00000000..d699f0ff
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipeTimelineBadge.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+ {{ $globals.icons.timelineText }}
+
+
+
+
+ {{ $t('recipe.open-timeline') }}
+
+
+
+
diff --git a/frontend/components/Domain/Recipe/RecipeTimelineContextMenu.vue b/frontend/components/Domain/Recipe/RecipeTimelineContextMenu.vue
new file mode 100644
index 00000000..cbd22810
--- /dev/null
+++ b/frontend/components/Domain/Recipe/RecipeTimelineContextMenu.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("events.event-delete-confirmation") }}
+
+
+
+
+
+ {{ icon }}
+
+
+
+
+
+ {{ item.icon }}
+
+ {{ item.title }}
+
+
+
+
+
+
+
diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json
index fdcee71d..9b500322 100644
--- a/frontend/lang/messages/en-US.json
+++ b/frontend/lang/messages/en-US.json
@@ -52,6 +52,9 @@
"apprise-url": "Apprise URL",
"database": "Database",
"delete-event": "Delete Event",
+ "event-delete-confirmation": "Are you sure you want to delete this event?",
+ "event-deleted": "Event Deleted",
+ "event-updated": "Event Updated",
"new-notification-form-description": "Mealie uses the Apprise library to generate notifications. They offer many options for services to use for notifications. Refer to their wiki for a comprehensive guide on how to create the URL for your service. If available, selecting the type of your notification may include extra features.",
"new-version": "New version available!",
"notification": "Notification",
@@ -97,9 +100,11 @@
"keyword": "Keyword",
"link-copied": "Link Copied",
"loading-recipes": "Loading Recipes",
+ "message": "Message",
"monday": "Monday",
"name": "Name",
"new": "New",
+ "never": "Never",
"no": "No",
"no-recipe-found": "No Recipe Found",
"ok": "OK",
@@ -120,6 +125,7 @@
"sort": "Sort",
"sort-alphabetically": "Alphabetical",
"status": "Status",
+ "subject": "Subject",
"submit": "Submit",
"success-count": "Success: {count}",
"sunday": "Sunday",
@@ -277,6 +283,7 @@
"carbohydrate-content": "Carbohydrate",
"categories": "Categories",
"comment-action": "Comment",
+ "comment": "Comment",
"comments": "Comments",
"delete-confirmation": "Are you sure you want to delete this recipe?",
"delete-recipe": "Delete Recipe",
@@ -360,7 +367,14 @@
"decrease-scale-label": "Decrease Scale by 1",
"increase-scale-label": "Increase Scale by 1",
"locked": "Locked",
- "public-link": "Public Link"
+ "public-link": "Public Link",
+ "edit-timeline-event": "Edit Timeline Event",
+ "timeline": "Timeline",
+ "timeline-is-empty": "Nothing on the timeline yet. Try making this recipe!",
+ "open-timeline": "Open Timeline",
+ "made-this": "I Made This",
+ "how-did-it-turn-out": "How did it turn out?",
+ "user-made-this": "{user} made this"
},
"search": {
"advanced-search": "Advanced Search",
diff --git a/frontend/lib/api/user/recipes/recipe.ts b/frontend/lib/api/user/recipes/recipe.ts
index abaef9f5..fa3fb3ec 100644
--- a/frontend/lib/api/user/recipes/recipe.ts
+++ b/frontend/lib/api/user/recipes/recipe.ts
@@ -11,8 +11,10 @@ import {
UpdateImageResponse,
RecipeZipTokenResponse,
RecipeTimelineEventIn,
+ RecipeTimelineEventOut,
+ RecipeTimelineEventUpdate,
} from "~/lib/api/types/recipe";
-import { ApiRequestInstance } from "~/lib/api/types/non-generated";
+import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generated";
export type Parser = "nlp" | "brute";
@@ -47,7 +49,7 @@ const routes = {
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}`,
+ recipesSlugTimelineEventId: (slug: string, id: string) => `${prefix}/recipes/${slug}/timeline/events/${id}`,
};
export class RecipeAPI extends BaseCRUDAPI {
@@ -132,6 +134,20 @@ export class RecipeAPI extends BaseCRUDAPI {
}
async createTimelineEvent(recipeSlug: string, payload: RecipeTimelineEventIn) {
- return await this.requests.post(routes.recipesSlugTimelineEvent(recipeSlug), payload);
+ return await this.requests.post(routes.recipesSlugTimelineEvent(recipeSlug), payload);
+ }
+
+ async updateTimelineEvent(recipeSlug: string, eventId: string, payload: RecipeTimelineEventUpdate) {
+ return await this.requests.put(routes.recipesSlugTimelineEventId(recipeSlug, eventId), payload);
+ }
+
+ async deleteTimelineEvent(recipeSlug: string, eventId: string) {
+ return await this.requests.delete(routes.recipesSlugTimelineEventId(recipeSlug, eventId));
+ }
+
+ async getAllTimelineEvents(recipeSlug: string, page = 1, perPage = -1, params = {} as any) {
+ return await this.requests.get>(routes.recipesSlugTimelineEvent(recipeSlug), {
+ params: { page, perPage, ...params },
+ });
}
}
diff --git a/frontend/lib/icons/icons.ts b/frontend/lib/icons/icons.ts
index c44bfbc9..c65a3955 100644
--- a/frontend/lib/icons/icons.ts
+++ b/frontend/lib/icons/icons.ts
@@ -37,6 +37,7 @@ import {
mdiHeartOutline,
mdiDotsHorizontal,
mdiCheckboxBlankOutline,
+ mdiCommentTextMultiple,
mdiCommentTextMultipleOutline,
mdiDownload,
mdiFile,
@@ -60,6 +61,7 @@ import {
mdiAlert,
mdiCheckboxMarkedCircle,
mdiInformation,
+ mdiInformationVariant,
mdiBellAlert,
mdiRefreshCircle,
mdiMenu,
@@ -122,6 +124,8 @@ import {
mdiCursorMove,
mdiText,
mdiTextBoxOutline,
+ mdiTimelineText,
+ mdiMessageText,
mdiChefHat,
mdiContentDuplicate,
} from "@mdi/js";
@@ -165,6 +169,7 @@ export const icons = {
codeBraces: mdiCodeJson,
codeJson: mdiCodeJson,
cog: mdiCog,
+ commentTextMultiple: mdiCommentTextMultiple,
commentTextMultipleOutline: mdiCommentTextMultipleOutline,
contentCopy: mdiContentCopy,
database: mdiDatabase,
@@ -194,10 +199,12 @@ export const icons = {
home: mdiHome,
import: mdiImport,
information: mdiInformation,
+ informationVariant: mdiInformationVariant,
link: mdiLink,
lock: mdiLock,
logout: mdiLogout,
menu: mdiMenu,
+ messageText: mdiMessageText,
newBox: mdiNewBox,
notificationClearAll: mdiNotificationClearAll,
openInNew: mdiOpenInNew,
@@ -220,6 +227,7 @@ export const icons = {
sortClockDescending: mdiSortClockDescending,
star: mdiStar,
testTube: mdiTestTube,
+ timelineText: mdiTimelineText,
tools: mdiTools,
potSteam: mdiPotSteam,
translate: mdiTranslate,