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