mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	ux: unify UI based on user-feedback (#1216)
* unify UI based on user-feedback * fix layout shify error * implement drag and drop animation
This commit is contained in:
		| @@ -24,19 +24,21 @@ | ||||
|             </div> | ||||
|           </v-expand-transition> | ||||
|         </RecipeCardImage> | ||||
|         <v-card-title class="my-n3 mb-n6"> | ||||
|         <v-card-title class="my-n3 px-2 mb-n6"> | ||||
|           <div class="headerClass"> | ||||
|             {{ name }} | ||||
|           </div> | ||||
|         </v-card-title> | ||||
|  | ||||
|         <slot name="actions"> | ||||
|           <v-card-actions> | ||||
|             <RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always /> | ||||
|             <RecipeRating :value="rating" :name="name" :slug="slug" :small="true" /> | ||||
|           <v-card-actions class="px-1"> | ||||
|             <RecipeFavoriteBadge v-if="loggedIn" class="absolute" :slug="slug" show-always /> | ||||
|  | ||||
|             <RecipeRating class="pb-1" :value="rating" :name="name" :slug="slug" :small="true" /> | ||||
|             <v-spacer></v-spacer> | ||||
|             <RecipeChips :truncate="true" :items="tags" :title="false" :limit="2" :small="true" url-prefix="tags" /> | ||||
|             <RecipeContextMenu | ||||
|               color="grey darken-2" | ||||
|               :slug="slug" | ||||
|               :name="name" | ||||
|               :recipe-id="recipeId" | ||||
|   | ||||
| @@ -40,6 +40,7 @@ | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api"; | ||||
| import colors from "vuetify/lib/util/colors"; | ||||
| import { useUserApi } from "~/composables/api"; | ||||
|  | ||||
| export interface ContextMenuIncludes { | ||||
| @@ -91,7 +92,7 @@ export default defineComponent({ | ||||
|     }, | ||||
|     color: { | ||||
|       type: String, | ||||
|       default: "primary", | ||||
|       default: colors.grey.darken2, | ||||
|     }, | ||||
|     slug: { | ||||
|       type: String, | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
|       </template> | ||||
|  | ||||
|       <v-card> | ||||
|         <v-app-bar dark color="primary" class="mt-n1 mb-3"> | ||||
|         <v-app-bar dense dark color="primary" class="mb-2"> | ||||
|           <v-icon large left> | ||||
|             {{ $globals.icons.createAlt }} | ||||
|           </v-icon> | ||||
| @@ -21,34 +21,26 @@ | ||||
|             v-model="inputText" | ||||
|             outlined | ||||
|             rows="12" | ||||
|             hide-details | ||||
|             :placeholder="$t('new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list')" | ||||
|           > | ||||
|           </v-textarea> | ||||
|           <v-tooltip top> | ||||
|             <template #activator="{ on, attrs }"> | ||||
|               <v-btn outlined color="info" small v-bind="attrs" @click="trimAllLines" v-on="on"> | ||||
|                 Trim Whitespace | ||||
|               </v-btn> | ||||
|             </template> | ||||
|             <span> Trim leading and trailing whitespace as well as blank lines </span> | ||||
|           </v-tooltip> | ||||
|  | ||||
|           <v-tooltip top> | ||||
|             <template #activator="{ on, attrs }"> | ||||
|               <v-btn class="ml-1" outlined color="info" small v-bind="attrs" @click="removeFirstCharacter" v-on="on"> | ||||
|                 Trim Prefix | ||||
|               </v-btn> | ||||
|           <v-divider></v-divider> | ||||
|           <template v-for="(util, idx) in utilities"> | ||||
|             <v-list-item :key="util.id" dense class="py-1"> | ||||
|               <v-list-item-title> | ||||
|                 <v-list-item-subtitle class="wrap-word"> | ||||
|                   {{ util.description }} | ||||
|                 </v-list-item-subtitle> | ||||
|               </v-list-item-title> | ||||
|               <BaseButton small color="info" @click="util.action"> | ||||
|                 <template #icon> {{ $globals.icons.robot }}</template> | ||||
|                 Run | ||||
|               </BaseButton> | ||||
|             </v-list-item> | ||||
|             <v-divider :key="`divider-${idx}`" class="mx-2"></v-divider> | ||||
|           </template> | ||||
|             <span> Trim first character from each line </span> | ||||
|           </v-tooltip> | ||||
|           <v-tooltip top> | ||||
|             <template #activator="{ on, attrs }"> | ||||
|               <v-btn class="ml-1" outlined color="info" small v-bind="attrs" @click="splitByNumberedLine" v-on="on"> | ||||
|                 Split By Numbered Line | ||||
|               </v-btn> | ||||
|             </template> | ||||
|             <span> Attempts to split a paragraph by matching 1) or 1. patterns </span> | ||||
|           </v-tooltip> | ||||
|         </v-card-text> | ||||
|  | ||||
|         <v-divider></v-divider> | ||||
| @@ -64,7 +56,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import { reactive, toRefs, defineComponent } from "@nuxtjs/composition-api"; | ||||
| import { reactive, toRefs, defineComponent, useContext } from "@nuxtjs/composition-api"; | ||||
| export default defineComponent({ | ||||
|   setup(_, context) { | ||||
|     const state = reactive({ | ||||
| @@ -78,7 +70,7 @@ export default defineComponent({ | ||||
|  | ||||
|     function removeFirstCharacter() { | ||||
|       state.inputText = splitText() | ||||
|         .map((line) => line.substr(1)) | ||||
|         .map((line) => line.substring(1)) | ||||
|         .join("\n"); | ||||
|     } | ||||
|  | ||||
| @@ -109,7 +101,28 @@ export default defineComponent({ | ||||
|       state.dialog = false; | ||||
|     } | ||||
|  | ||||
|     const { i18n } = useContext(); | ||||
|  | ||||
|     const utilities = [ | ||||
|       { | ||||
|         id: "trim-whitespace", | ||||
|         description: i18n.tc("new-recipe.trim-whitespace-description"), | ||||
|         action: trimAllLines, | ||||
|       }, | ||||
|       { | ||||
|         id: "trim-prefix", | ||||
|         description: i18n.tc("new-recipe.trim-prefix-description"), | ||||
|         action: removeFirstCharacter, | ||||
|       }, | ||||
|       { | ||||
|         id: "split-by-numbered-line", | ||||
|         description: i18n.tc("new-recipe.split-by-numbered-line-description"), | ||||
|         action: splitByNumberedLine, | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|     return { | ||||
|       utilities, | ||||
|       splitText, | ||||
|       trimAllLines, | ||||
|       removeFirstCharacter, | ||||
|   | ||||
| @@ -21,18 +21,12 @@ | ||||
|           type="number" | ||||
|           placeholder="Quantity" | ||||
|         > | ||||
|           <v-icon | ||||
|             v-if="$listeners && $listeners.delete" | ||||
|             slot="prepend" | ||||
|             class="mr-n1" | ||||
|             color="error" | ||||
|             @click="$emit('delete')" | ||||
|           > | ||||
|             {{ $globals.icons.delete }} | ||||
|           <v-icon v-if="$listeners && $listeners.delete" slot="prepend" class="mr-n1 handle"> | ||||
|             {{ $globals.icons.arrowUpDown }} | ||||
|           </v-icon> | ||||
|         </v-text-field> | ||||
|       </v-col> | ||||
|       <v-col v-if="!disableAmount && units" sm="12" md="3" cols="12"> | ||||
|       <v-col v-if="!disableAmount" sm="12" md="3" cols="12"> | ||||
|         <v-autocomplete | ||||
|           v-model="value.unit" | ||||
|           :search-input.sync="unitSearch" | ||||
| @@ -40,7 +34,7 @@ | ||||
|           dense | ||||
|           solo | ||||
|           return-object | ||||
|           :items="units" | ||||
|           :items="units || []" | ||||
|           item-text="name" | ||||
|           class="mx-1" | ||||
|           placeholder="Choose Unit" | ||||
| @@ -59,7 +53,7 @@ | ||||
|       </v-col> | ||||
|  | ||||
|       <!-- Foods Input --> | ||||
|       <v-col v-if="!disableAmount && foods" m="12" md="3" cols="12" class=""> | ||||
|       <v-col v-if="!disableAmount" m="12" md="3" cols="12" class=""> | ||||
|         <v-autocomplete | ||||
|           v-model="value.food" | ||||
|           :search-input.sync="foodSearch" | ||||
| @@ -67,7 +61,7 @@ | ||||
|           dense | ||||
|           solo | ||||
|           return-object | ||||
|           :items="foods" | ||||
|           :items="foods || []" | ||||
|           item-text="name" | ||||
|           class="mx-1 py-0" | ||||
|           placeholder="Choose Food" | ||||
| @@ -85,28 +79,34 @@ | ||||
|         </v-autocomplete> | ||||
|       </v-col> | ||||
|       <v-col sm="12" md="" cols="12"> | ||||
|         <div class="d-flex"> | ||||
|           <v-text-field v-model="value.note" hide-details dense solo class="mx-1" placeholder="Notes"> | ||||
|           <v-icon v-if="disableAmount" slot="prepend" class="mr-n1" color="error" @click="$emit('delete')"> | ||||
|             {{ $globals.icons.delete }} | ||||
|             <v-icon v-if="disableAmount && $listeners && $listeners.delete" slot="prepend" class="mr-n1 handle"> | ||||
|               {{ $globals.icons.arrowUpDown }} | ||||
|             </v-icon> | ||||
|  | ||||
|           <template slot="append-outer"> | ||||
|           </v-text-field> | ||||
|           <BaseButtonGroup | ||||
|             hover | ||||
|             :large="false" | ||||
|               class="handle my-auto" | ||||
|             class="my-auto" | ||||
|             :buttons="[ | ||||
|               { | ||||
|                   icon: $globals.icons.arrowUpDown, | ||||
|                   text: '', | ||||
|                 icon: $globals.icons.delete, | ||||
|                 text: 'Delete', | ||||
|                 event: 'delete', | ||||
|               }, | ||||
|               { | ||||
|                 icon: $globals.icons.dotsVertical, | ||||
|                 text: 'Menu', | ||||
|                 event: 'open', | ||||
|                 children: contextMenuOptions, | ||||
|               }, | ||||
|             ]" | ||||
|             @toggle-section="toggleTitle" | ||||
|             @toggle-original="toggleOriginalText" | ||||
|             @delete="$emit('delete')" | ||||
|           /> | ||||
|           </template> | ||||
|         </v-text-field> | ||||
|         </div> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
|     <p v-if="showOriginalText" class="text-caption">Original Text: {{ value.originalText }}</p> | ||||
|   | ||||
| @@ -71,11 +71,17 @@ | ||||
|       :disabled="!edit" | ||||
|       :value="value" | ||||
|       handle=".handle" | ||||
|       v-bind="{ | ||||
|         animation: 200, | ||||
|         group: 'description', | ||||
|         ghostClass: 'ghost', | ||||
|       }" | ||||
|       @input="updateIndex" | ||||
|       @start="drag = true" | ||||
|       @end="drag = false" | ||||
|     > | ||||
|       <div v-for="(step, index) in value" :key="step.id"> | ||||
|       <TransitionGroup type="transition" :name="!drag ? 'flip-list' : ''"> | ||||
|         <div v-for="(step, index) in value" :key="step.id" class="list-group-item"> | ||||
|           <v-app-bar | ||||
|             v-if="showTitleEditor[step.id]" | ||||
|             class="primary mx-1 mt-6" | ||||
| @@ -109,17 +115,20 @@ | ||||
|               @click="toggleDisabled(index)" | ||||
|             > | ||||
|               <v-card-title :class="{ 'pb-0': !isChecked(index) }"> | ||||
|               <v-btn v-if="edit" fab x-small color="white" class="mr-2" elevation="0" @click="value.splice(index, 1)"> | ||||
|                 <v-icon size="24" color="error">{{ $globals.icons.delete }}</v-icon> | ||||
|               </v-btn> | ||||
|  | ||||
|                 <span class="handle"> | ||||
|                   <v-icon v-if="edit" size="26" class="pb-1">{{ $globals.icons.arrowUpDown }}</v-icon> | ||||
|                   {{ $t("recipe.step-index", { step: index + 1 }) }} | ||||
|  | ||||
|                 </span> | ||||
|                 <template v-if="edit"> | ||||
|                 <v-icon class="handle ml-auto mr-2">{{ $globals.icons.arrowUpDown }}</v-icon> | ||||
|                 <div> | ||||
|                   <div class="ml-auto"> | ||||
|                     <BaseButtonGroup | ||||
|                       :large="false" | ||||
|                       :buttons="[ | ||||
|                         { | ||||
|                           icon: $globals.icons.delete, | ||||
|                           text: $tc('general.delete'), | ||||
|                           event: 'delete', | ||||
|                         }, | ||||
|                         { | ||||
|                           icon: $globals.icons.dotsVertical, | ||||
|                           text: '', | ||||
| @@ -137,22 +146,22 @@ | ||||
|                               text: 'Merge Above', | ||||
|                               event: 'merge-above', | ||||
|                             }, | ||||
|                         ], | ||||
|                       }, | ||||
|                             { | ||||
|                               icon: previewStates[index] ? $globals.icons.edit : $globals.icons.eye, | ||||
|                         text: previewStates[index] ? $tc('general.edit') : 'Preview Markdown', | ||||
|                               text: previewStates[index] ? 'Edit Markdown' : 'Preview Markdown', | ||||
|                               event: 'preview-step', | ||||
|                             }, | ||||
|                           ], | ||||
|                         }, | ||||
|                       ]" | ||||
|                       @merge-above="mergeAbove(index - 1, index)" | ||||
|                       @toggle-section="toggleShowTitle(step.id)" | ||||
|                       @link-ingredients="openDialog(index, step.ingredientReferences, step.text)" | ||||
|                       @preview-step="togglePreviewState(index)" | ||||
|                       @delete="value.splice(index, 1)" | ||||
|                     /> | ||||
|                   </div> | ||||
|                 </template> | ||||
|  | ||||
|                 <v-fade-transition> | ||||
|                   <v-icon v-show="isChecked(index)" size="24" class="ml-auto" color="success"> | ||||
|                     {{ $globals.icons.checkboxMarkedCircle }} | ||||
| @@ -181,6 +190,7 @@ | ||||
|             </v-card> | ||||
|           </v-hover> | ||||
|         </div> | ||||
|       </TransitionGroup> | ||||
|     </draggable> | ||||
|   </section> | ||||
| </template> | ||||
| @@ -481,7 +491,10 @@ export default defineComponent({ | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const drag = ref(false); | ||||
|  | ||||
|     return { | ||||
|       drag, | ||||
|       togglePreviewState, | ||||
|       toggleCollapseSection, | ||||
|       previewStates, | ||||
| @@ -521,4 +534,23 @@ export default defineComponent({ | ||||
| .markdown >>> ol > li { | ||||
|   display: list-item; | ||||
| } | ||||
|  | ||||
| .flip-list-move { | ||||
|   transition: transform 0.5s; | ||||
| } | ||||
| .no-move { | ||||
|   transition: transform 0s; | ||||
| } | ||||
| .ghost { | ||||
|   opacity: 0.5; | ||||
| } | ||||
| .list-group { | ||||
|   min-height: 38px; | ||||
| } | ||||
| .list-group-item { | ||||
|   cursor: move; | ||||
| } | ||||
| .list-group-item i { | ||||
|   cursor: pointer; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,29 +1,27 @@ | ||||
| <template> | ||||
|   <div v-if="value.length > 0 || edit"> | ||||
|   <div v-if="value.length > 0 || edit" class="mt-8"> | ||||
|     <h2 class="my-4">{{ $t("recipe.note") }}</h2> | ||||
|     <v-card v-for="(note, index) in value" :key="'note' + index" class="mt-1"> | ||||
|       <div v-if="edit"> | ||||
|     <div v-for="(note, index) in value" :key="'note' + index" class="mt-1"> | ||||
|       <v-card v-if="edit"> | ||||
|         <v-card-text> | ||||
|           <v-row align="center"> | ||||
|             <v-btn fab x-small color="white" class="mr-2" elevation="0" @click="removeByIndex(value, index)"> | ||||
|               <v-icon color="error">{{ $globals.icons.delete }}</v-icon> | ||||
|           <div class="d-flex align-center"> | ||||
|             <v-text-field v-model="value[index]['title']" :label="$t('recipe.title')" /> | ||||
|             <v-btn icon class="mr-2" elevation="0" @click="removeByIndex(value, index)"> | ||||
|               <v-icon>{{ $globals.icons.delete }}</v-icon> | ||||
|             </v-btn> | ||||
|             <v-text-field v-model="value[index]['title']" :label="$t('recipe.title')"></v-text-field> | ||||
|           </v-row> | ||||
|  | ||||
|           <v-textarea v-model="value[index]['text']" auto-grow :placeholder="$t('recipe.note')"> </v-textarea> | ||||
|         </v-card-text> | ||||
|           </div> | ||||
|           <v-textarea v-model="value[index]['text']" auto-grow :placeholder="$t('recipe.note')" /> | ||||
|         </v-card-text> | ||||
|       </v-card> | ||||
|       <div v-else> | ||||
|         <v-card-title class="py-2"> | ||||
|         <v-card-title class="text-subtitle-1 font-weight-medium py-1"> | ||||
|           {{ note.title }} | ||||
|         </v-card-title> | ||||
|         <v-divider class="mx-2"></v-divider> | ||||
|         <v-card-text> | ||||
|           <VueMarkdown :source="note.text"> </VueMarkdown> | ||||
|         </v-card-text> | ||||
|       </div> | ||||
|     </v-card> | ||||
|     </div> | ||||
|  | ||||
|     <div v-if="edit" class="d-flex justify-end"> | ||||
|       <BaseButton class="ml-auto my-2" @click="addNote"> {{ $t("general.new") }}</BaseButton> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <v-item-group> | ||||
|     <template v-for="btn in buttons"> | ||||
|       <v-menu v-if="btn.children" :key="'menu-' + btn.event" active-class="pa-0" offset-x left> | ||||
|       <v-menu v-if="btn.children" :key="'menu-' + btn.event" active-class="pa-0" offset-y top left> | ||||
|         <template #activator="{ on, attrs }"> | ||||
|           <v-btn tile :large="large" icon v-bind="attrs" v-on="on"> | ||||
|             <v-icon> | ||||
|   | ||||
| @@ -215,7 +215,10 @@ | ||||
|     "upload-a-recipe": "Upload a Recipe", | ||||
|     "upload-individual-zip-file": "Upload an individual .zip file exported from another Mealie instance.", | ||||
|     "url-form-hint": "Copy and paste a link from your favorite recipe website", | ||||
|     "view-scraped-data": "View Scraped Data" | ||||
|     "view-scraped-data": "View Scraped Data", | ||||
|     "trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines", | ||||
|     "trim-prefix-description": "Trim first character from each line", | ||||
|     "split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns" | ||||
|   }, | ||||
|   "page": { | ||||
|     "404-page-not-found": "404 Page not found", | ||||
|   | ||||
| @@ -127,14 +127,29 @@ | ||||
|           <!-- Advanced Editor --> | ||||
|           <div v-if="form"> | ||||
|             <h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2> | ||||
|             <draggable v-if="recipe.recipeIngredient.length > 0" v-model="recipe.recipeIngredient" handle=".handle"> | ||||
|             <draggable | ||||
|               v-if="recipe.recipeIngredient.length > 0" | ||||
|               v-model="recipe.recipeIngredient" | ||||
|               handle=".handle" | ||||
|               v-bind="{ | ||||
|                 animation: 200, | ||||
|                 group: 'description', | ||||
|                 disabled: false, | ||||
|                 ghostClass: 'ghost', | ||||
|               }" | ||||
|               @start="drag = true" | ||||
|               @end="drag = false" | ||||
|             > | ||||
|               <TransitionGroup type="transition" :name="!drag ? 'flip-list' : ''"> | ||||
|                 <RecipeIngredientEditor | ||||
|                   v-for="(ingredient, index) in recipe.recipeIngredient" | ||||
|                   :key="ingredient.referenceId" | ||||
|                   v-model="recipe.recipeIngredient[index]" | ||||
|                   class="list-group-item" | ||||
|                   :disable-amount="recipe.settings.disableAmount" | ||||
|                   @delete="recipe.recipeIngredient.splice(index, 1)" | ||||
|                 /> | ||||
|               </TransitionGroup> | ||||
|             </draggable> | ||||
|             <v-skeleton-loader v-else boilerplate elevation="2" type="list-item"> </v-skeleton-loader> | ||||
|             <div class="d-flex justify-end mt-2"> | ||||
| @@ -820,8 +835,11 @@ export default defineComponent({ | ||||
|       return "Parse ingredients"; | ||||
|     }); | ||||
|  | ||||
|     const drag = ref(false); | ||||
|  | ||||
|     return { | ||||
|       // Wake Lock | ||||
|       drag, | ||||
|       wakeIsSupported, | ||||
|       isActive, | ||||
|       lockScreen, | ||||
| @@ -857,3 +875,24 @@ export default defineComponent({ | ||||
|   head: {}, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="css"> | ||||
| .flip-list-move { | ||||
|   transition: transform 0.5s; | ||||
| } | ||||
| .no-move { | ||||
|   transition: transform 0s; | ||||
| } | ||||
| .ghost { | ||||
|   opacity: 0.5; | ||||
| } | ||||
| .list-group { | ||||
|   min-height: 38px; | ||||
| } | ||||
| .list-group-item { | ||||
|   cursor: move; | ||||
| } | ||||
| .list-group-item i { | ||||
|   cursor: pointer; | ||||
| } | ||||
| </style> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user