mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	Feature/move label editor (#1069)
* update default color * move labels editor
This commit is contained in:
		| @@ -29,7 +29,6 @@ export default defineComponent({ | |||||||
|  |  | ||||||
|     Based on -> https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color |     Based on -> https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color | ||||||
|     */ |     */ | ||||||
|  |  | ||||||
|     const ACCESSIBILITY_THRESHOLD = 0.179; |     const ACCESSIBILITY_THRESHOLD = 0.179; | ||||||
|  |  | ||||||
|     function pickTextColorBasedOnBgColorAdvanced(bgColor: string, lightColor: string, darkColor: string) { |     function pickTextColorBasedOnBgColorAdvanced(bgColor: string, lightColor: string, darkColor: string) { | ||||||
| @@ -53,4 +52,4 @@ export default defineComponent({ | |||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -24,11 +24,11 @@ | |||||||
|                 value: 'new', |                 value: 'new', | ||||||
|                 to: '/group/data/units', |                 to: '/group/data/units', | ||||||
|               }, |               }, | ||||||
|               // { |               { | ||||||
|               //   text: 'Labels', |                 text: 'Labels', | ||||||
|               //   value: 'new', |                 value: 'new', | ||||||
|               //   to: '/group/data/labels', |                 to: '/group/data/labels', | ||||||
|               // }, |               }, | ||||||
|             ]" |             ]" | ||||||
|           > |           > | ||||||
|           </BaseOverflowButton> |           </BaseOverflowButton> | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ | |||||||
|     <!-- Delete Dialog --> |     <!-- Delete Dialog --> | ||||||
|     <BaseDialog |     <BaseDialog | ||||||
|       v-model="deleteDialog" |       v-model="deleteDialog" | ||||||
|       :title="$tc('general.delete')" |       :title="$tc('general.confirm')" | ||||||
|       :icon="$globals.icons.alertCircle" |       :icon="$globals.icons.alertCircle" | ||||||
|       color="error" |       color="error" | ||||||
|       @confirm="deleteFood" |       @confirm="deleteFood" | ||||||
|   | |||||||
							
								
								
									
										198
									
								
								frontend/pages/group/data/labels.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								frontend/pages/group/data/labels.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <!-- Create New Dialog --> | ||||||
|  |     <BaseDialog v-model="state.createDialog" title="New Label" :icon="$globals.icons.tags" @submit="createLabel"> | ||||||
|  |       <v-card-text> | ||||||
|  |         <MultiPurposeLabel :label="createLabelData" /> | ||||||
|  |  | ||||||
|  |         <div class="mt-4"> | ||||||
|  |           <v-text-field v-model="createLabelData.name" :label="$t('general.name')"> </v-text-field> | ||||||
|  |           <InputColor v-model="createLabelData.color" /> | ||||||
|  |         </div> | ||||||
|  |       </v-card-text> | ||||||
|  |     </BaseDialog> | ||||||
|  |  | ||||||
|  |     <!-- Edit Dialog --> | ||||||
|  |     <BaseDialog | ||||||
|  |       v-model="state.editDialog" | ||||||
|  |       :icon="$globals.icons.tags" | ||||||
|  |       title="Edit Label" | ||||||
|  |       :submit-text="$tc('general.save')" | ||||||
|  |       @submit="editSaveLabel" | ||||||
|  |     > | ||||||
|  |       <v-card-text v-if="editLabel"> | ||||||
|  |         <MultiPurposeLabel :label="editLabel" /> | ||||||
|  |         <div class="mt-4"> | ||||||
|  |           <v-text-field v-model="editLabel.name" :label="$t('general.name')"> </v-text-field> | ||||||
|  |           <InputColor v-model="editLabel.color" /> | ||||||
|  |         </div> | ||||||
|  |       </v-card-text> | ||||||
|  |     </BaseDialog> | ||||||
|  |  | ||||||
|  |     <!-- Delete Dialog --> | ||||||
|  |     <BaseDialog | ||||||
|  |       v-model="state.deleteDialog" | ||||||
|  |       :title="$tc('general.confirm')" | ||||||
|  |       :icon="$globals.icons.alertCircle" | ||||||
|  |       color="error" | ||||||
|  |       @confirm="deleteLabel" | ||||||
|  |     > | ||||||
|  |       <v-card-text> | ||||||
|  |         {{ $t("general.confirm-delete-generic") }} | ||||||
|  |       </v-card-text> | ||||||
|  |     </BaseDialog> | ||||||
|  |  | ||||||
|  |     <!-- Recipe Data Table --> | ||||||
|  |     <BaseCardSectionTitle :icon="$globals.icons.tags" section title="Labels"> </BaseCardSectionTitle> | ||||||
|  |     <CrudTable | ||||||
|  |       :table-config="tableConfig" | ||||||
|  |       :headers.sync="tableHeaders" | ||||||
|  |       :data="labels" | ||||||
|  |       :bulk-actions="[]" | ||||||
|  |       @delete-one="deleteEventHandler" | ||||||
|  |       @edit-one="editEventHandler" | ||||||
|  |     > | ||||||
|  |       <template #button-row> | ||||||
|  |         <BaseButton create @click="state.createDialog = true"> | ||||||
|  |           <template #icon> {{ $globals.icons.tags }} </template> | ||||||
|  |           Create | ||||||
|  |         </BaseButton> | ||||||
|  |       </template> | ||||||
|  |       <template #item.name="{ item }"> | ||||||
|  |         <MultiPurposeLabel v-if="item" :label="item"> | ||||||
|  |           {{ item.name }} | ||||||
|  |         </MultiPurposeLabel> | ||||||
|  |       </template> | ||||||
|  |     </CrudTable> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, reactive, ref } from "@nuxtjs/composition-api"; | ||||||
|  | import { validators } from "~/composables/use-validators"; | ||||||
|  | import { useUserApi } from "~/composables/api"; | ||||||
|  | import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue"; | ||||||
|  | import { MultiPurposeLabelSummary } from "~/types/api-types/labels"; | ||||||
|  |  | ||||||
|  | export default defineComponent({ | ||||||
|  |   components: { MultiPurposeLabel }, | ||||||
|  |   setup() { | ||||||
|  |     const userApi = useUserApi(); | ||||||
|  |     const tableConfig = { | ||||||
|  |       hideColumns: true, | ||||||
|  |       canExport: true, | ||||||
|  |     }; | ||||||
|  |     const tableHeaders = [ | ||||||
|  |       { | ||||||
|  |         text: "Id", | ||||||
|  |         value: "id", | ||||||
|  |         show: false, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         text: "Name", | ||||||
|  |         value: "name", | ||||||
|  |         show: true, | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     const state = reactive({ | ||||||
|  |       createDialog: false, | ||||||
|  |       editDialog: false, | ||||||
|  |       deleteDialog: false, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // ============================================================ | ||||||
|  |     // Labels | ||||||
|  |  | ||||||
|  |     const labels = ref([] as MultiPurposeLabelSummary[]); | ||||||
|  |  | ||||||
|  |     async function refreshLabels() { | ||||||
|  |       const { data } = await userApi.multiPurposeLabels.getAll(); | ||||||
|  |       labels.value = data ?? []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Create | ||||||
|  |  | ||||||
|  |     const createLabelData = ref({ | ||||||
|  |       groupId: "", | ||||||
|  |       id: "", | ||||||
|  |       name: "", | ||||||
|  |       color: "", | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     async function createLabel() { | ||||||
|  |       await userApi.multiPurposeLabels.createOne(createLabelData.value); | ||||||
|  |       createLabelData.value = { | ||||||
|  |         groupId: "", | ||||||
|  |         id: "", | ||||||
|  |         name: "", | ||||||
|  |         color: "", | ||||||
|  |       }; | ||||||
|  |       refreshLabels(); | ||||||
|  |       state.createDialog = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Delete | ||||||
|  |  | ||||||
|  |     const deleteTarget = ref<MultiPurposeLabelSummary | null>(null); | ||||||
|  |  | ||||||
|  |     function deleteEventHandler(item: MultiPurposeLabelSummary) { | ||||||
|  |       state.deleteDialog = true; | ||||||
|  |       deleteTarget.value = item; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function deleteLabel() { | ||||||
|  |       if (!deleteTarget.value) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const { data } = await userApi.multiPurposeLabels.deleteOne(deleteTarget.value.id); | ||||||
|  |       if (data) { | ||||||
|  |         refreshLabels(); | ||||||
|  |       } | ||||||
|  |       state.deleteDialog = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Edit | ||||||
|  |  | ||||||
|  |     const editLabel = ref<MultiPurposeLabelSummary | null>(null); | ||||||
|  |  | ||||||
|  |     function editEventHandler(item: MultiPurposeLabelSummary) { | ||||||
|  |       state.editDialog = true; | ||||||
|  |       editLabel.value = item; | ||||||
|  |  | ||||||
|  |       if (!editLabel.value.color) { | ||||||
|  |         editLabel.value.color = "#E0E0E0"; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function editSaveLabel() { | ||||||
|  |       if (!editLabel.value) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const { data } = await userApi.multiPurposeLabels.updateOne(editLabel.value.id, editLabel.value); | ||||||
|  |       if (data) { | ||||||
|  |         refreshLabels(); | ||||||
|  |       } | ||||||
|  |       state.editDialog = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     refreshLabels(); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       state, | ||||||
|  |       tableConfig, | ||||||
|  |       tableHeaders, | ||||||
|  |       labels, | ||||||
|  |       validators, | ||||||
|  |  | ||||||
|  |       deleteEventHandler, | ||||||
|  |       deleteLabel, | ||||||
|  |       editLabel, | ||||||
|  |       editEventHandler, | ||||||
|  |       editSaveLabel, | ||||||
|  |       createLabel, | ||||||
|  |       createLabelData, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| @@ -21,7 +21,7 @@ | |||||||
|     <!-- Delete Dialog --> |     <!-- Delete Dialog --> | ||||||
|     <BaseDialog |     <BaseDialog | ||||||
|       v-model="deleteDialog" |       v-model="deleteDialog" | ||||||
|       :title="$tc('general.delete')" |       :title="$tc('general.confirm')" | ||||||
|       :icon="$globals.icons.alertCircle" |       :icon="$globals.icons.alertCircle" | ||||||
|       color="error" |       color="error" | ||||||
|       @confirm="deleteFood" |       @confirm="deleteFood" | ||||||
|   | |||||||
| @@ -178,7 +178,7 @@ | |||||||
|  |  | ||||||
|     <v-lazy> |     <v-lazy> | ||||||
|       <div class="d-flex justify-end mt-10"> |       <div class="d-flex justify-end mt-10"> | ||||||
|         <ButtonLink to="/shopping-lists/labels" text="Manage Labels" :icon="$globals.icons.tags" /> |         <ButtonLink to="/group/data/labels" text="Manage Labels" :icon="$globals.icons.tags" /> | ||||||
|       </div> |       </div> | ||||||
|     </v-lazy> |     </v-lazy> | ||||||
|   </v-container> |   </v-container> | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| <template> | <template> | ||||||
|   <v-container v-if="shoppingLists" class="narrow-container"> |   <v-container v-if="shoppingLists" class="narrow-container"> | ||||||
|     <BaseDialog v-model="createDialog" :title="$t('shopping-list.create-shopping-list')" @submit="createOne"> |     <BaseDialog v-model="createDialog" :title="$tc('shopping-list.create-shopping-list')" @submit="createOne"> | ||||||
|       <v-card-text> |       <v-card-text> | ||||||
|         <v-text-field v-model="createName" autofocus :label="$t('shopping-list.new-list')"> </v-text-field> |         <v-text-field v-model="createName" autofocus :label="$t('shopping-list.new-list')"> </v-text-field> | ||||||
|       </v-card-text> |       </v-card-text> | ||||||
|     </BaseDialog> |     </BaseDialog> | ||||||
|  |  | ||||||
|     <BaseDialog v-model="deleteDialog" :title="$t('general.confirm')" color="error" @confirm="deleteOne"> |     <BaseDialog v-model="deleteDialog" :title="$tc('general.confirm')" color="error" @confirm="deleteOne"> | ||||||
|       <v-card-text> Are you sure you want to delete this item?</v-card-text> |       <v-card-text> Are you sure you want to delete this item?</v-card-text> | ||||||
|     </BaseDialog> |     </BaseDialog> | ||||||
|     <BasePageTitle divider> |     <BasePageTitle divider> | ||||||
| @@ -33,11 +33,11 @@ | |||||||
|       </v-card> |       </v-card> | ||||||
|     </section> |     </section> | ||||||
|     <div class="d-flex justify-end mt-10"> |     <div class="d-flex justify-end mt-10"> | ||||||
|       <ButtonLink to="/shopping-lists/labels" text="Manage Labels" :icon="$globals.icons.tags" /> |       <ButtonLink to="/group/data/labels" text="Manage Labels" :icon="$globals.icons.tags" /> | ||||||
|     </div> |     </div> | ||||||
|   </v-container> |   </v-container> | ||||||
| </template> | </template> | ||||||
|    |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, useAsync, reactive, toRefs } from "@nuxtjs/composition-api"; | import { defineComponent, useAsync, reactive, toRefs } from "@nuxtjs/composition-api"; | ||||||
| import { useUserApi } from "~/composables/api"; | import { useUserApi } from "~/composables/api"; | ||||||
|   | |||||||
| @@ -1,266 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <v-container class="narrow-container"> |  | ||||||
|     <BaseDialog v-model="createDialog" title="New Label" :icon="$globals.icons.tags" @submit="createLabel"> |  | ||||||
|       <v-card-text> |  | ||||||
|         <v-text-field v-model="createLabelData.name" :label="$t('general.name')"> </v-text-field> |  | ||||||
|       </v-card-text> |  | ||||||
|     </BaseDialog> |  | ||||||
|  |  | ||||||
|     <BaseDialog |  | ||||||
|       v-model="deleteDialog" |  | ||||||
|       :title="$t('general.confirm')" |  | ||||||
|       :icon="$globals.icons.alert" |  | ||||||
|       color="error" |  | ||||||
|       @confirm="confirmDelete" |  | ||||||
|     > |  | ||||||
|       <v-card-text> |  | ||||||
|         {{ $t("general.confirm-delete-generic") }} |  | ||||||
|       </v-card-text> |  | ||||||
|     </BaseDialog> |  | ||||||
|  |  | ||||||
|     <BasePageTitle divider> |  | ||||||
|       <template #header> |  | ||||||
|         <v-img max-height="100" max-width="100" :src="require('~/static/svgs/shopping-cart.svg')"></v-img> |  | ||||||
|       </template> |  | ||||||
|       <template #title> Shopping Lists Labels </template> |  | ||||||
|     </BasePageTitle> |  | ||||||
|     <BaseButton create @click="createDialog = true" /> |  | ||||||
|  |  | ||||||
|     <section v-if="labels" class="mt-4"> |  | ||||||
|       <v-text-field v-model="searchInput" :label="$t('sidebar.search')" clearable> |  | ||||||
|         <template #prepend> |  | ||||||
|           <v-icon>{{ $globals.icons.search }}</v-icon> |  | ||||||
|         </template> |  | ||||||
|       </v-text-field> |  | ||||||
|  |  | ||||||
|       <v-sheet v-for="(label, index) in results" :key="label.id"> |  | ||||||
|         <div class="d-flex px-2 py-2 pt-3"> |  | ||||||
|           <MultiPurposeLabel :label="label" /> |  | ||||||
|  |  | ||||||
|           <div class="ml-auto"> |  | ||||||
|             <v-btn v-if="!isOpen[label.id]" class="mx-1" icon @click.prevent="deleteLabel(label.id)"> |  | ||||||
|               <v-icon> |  | ||||||
|                 {{ $globals.icons.delete }} |  | ||||||
|               </v-icon> |  | ||||||
|             </v-btn> |  | ||||||
|             <v-btn v-if="!isOpen[label.id]" class="mx-1" icon @click="toggleIsOpen(label)"> |  | ||||||
|               <v-icon> |  | ||||||
|                 {{ $globals.icons.edit }} |  | ||||||
|               </v-icon> |  | ||||||
|             </v-btn> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         <v-card-text v-if="isOpen[label.id]"> |  | ||||||
|           <div class="d-md-flex" style="gap: 30px"> |  | ||||||
|             <v-text-field v-model="labels[index].name" :label="$t('general.name')"> </v-text-field> |  | ||||||
|             <div style="max-width: 300px"> |  | ||||||
|               <InputColor v-model="labels[index].color" /> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|           <div class="d-flex justify-end"> |  | ||||||
|             <BaseButtonGroup |  | ||||||
|               :buttons="[ |  | ||||||
|                 { |  | ||||||
|                   icon: $globals.icons.delete, |  | ||||||
|                   text: 'Delete', |  | ||||||
|                   event: 'delete', |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   icon: $globals.icons.close, |  | ||||||
|                   text: 'Cancel', |  | ||||||
|                   event: 'cancel', |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   icon: $globals.icons.save, |  | ||||||
|                   text: 'Save', |  | ||||||
|                   event: 'save', |  | ||||||
|                 }, |  | ||||||
|               ]" |  | ||||||
|               @cancel="resetToLastGoodValue(label, index)" |  | ||||||
|               @save="updateLabel(label)" |  | ||||||
|               @delete="deleteLabel(label.id)" |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|         </v-card-text> |  | ||||||
|         <v-divider></v-divider> |  | ||||||
|       </v-sheet> |  | ||||||
|     </section> |  | ||||||
|   </v-container> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts"> |  | ||||||
| import { defineComponent, ref, useAsync, computed } from "@nuxtjs/composition-api"; |  | ||||||
| import Fuse from "fuse.js"; |  | ||||||
| import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue"; |  | ||||||
| import { useUserApi } from "~/composables/api"; |  | ||||||
| import { useAsyncKey } from "~/composables/use-utils"; |  | ||||||
| import { MultiPurposeLabelSummary } from "~/types/api-types/labels"; |  | ||||||
| export default defineComponent({ |  | ||||||
|   components: { MultiPurposeLabel }, |  | ||||||
|   setup() { |  | ||||||
|     // ========================================================== |  | ||||||
|     // API Operations |  | ||||||
|  |  | ||||||
|     const api = useUserApi(); |  | ||||||
|  |  | ||||||
|     const deleteDialog = ref(false); |  | ||||||
|     const deleteTargetId = ref(""); |  | ||||||
|  |  | ||||||
|     async function confirmDelete() { |  | ||||||
|       await api.multiPurposeLabels.deleteOne(deleteTargetId.value); |  | ||||||
|       refreshLabels(); |  | ||||||
|       deleteTargetId.value = ""; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function deleteLabel(itemId: string) { |  | ||||||
|       deleteTargetId.value = itemId; |  | ||||||
|       deleteDialog.value = true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const createDialog = ref(false); |  | ||||||
|  |  | ||||||
|     const createLabelData = ref({ |  | ||||||
|       name: "", |  | ||||||
|       color: "", |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     async function createLabel() { |  | ||||||
|       createLabelData.value.color = getRandomHex(); |  | ||||||
|       const { data } = await api.multiPurposeLabels.createOne(createLabelData.value); |  | ||||||
|       if (data) { |  | ||||||
|         refreshLabels(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function updateLabel(label: MultiPurposeLabelSummary) { |  | ||||||
|       const { data } = await api.multiPurposeLabels.updateOne(label.id, label); |  | ||||||
|       if (data) { |  | ||||||
|         refreshLabels(); |  | ||||||
|         toggleIsOpen(label); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const labels = useAsync(async () => { |  | ||||||
|       const { data } = await api.multiPurposeLabels.getAll(); |  | ||||||
|       return data; |  | ||||||
|     }, useAsyncKey()); |  | ||||||
|  |  | ||||||
|     async function refreshLabels() { |  | ||||||
|       const { data } = await api.multiPurposeLabels.getAll(); |  | ||||||
|       labels.value = data ?? []; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // ========================================================== |  | ||||||
|     // Component Helpers |  | ||||||
|  |  | ||||||
|     const lastGoodValue = ref<{ [key: string]: MultiPurposeLabelSummary }>({}); |  | ||||||
|  |  | ||||||
|     function saveLastGoodValue(label: MultiPurposeLabelSummary) { |  | ||||||
|       lastGoodValue.value[label.id] = { ...label }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function resetToLastGoodValue(label: MultiPurposeLabelSummary, index: number) { |  | ||||||
|       const lgv = lastGoodValue.value[label.id]; |  | ||||||
|  |  | ||||||
|       if (lgv && labels.value) { |  | ||||||
|         labels.value[index] = lgv; |  | ||||||
|         labels.value = [...labels.value]; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       toggleIsOpen(label); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const isOpen = ref<{ [key: string]: boolean }>({}); |  | ||||||
|  |  | ||||||
|     function toggleIsOpen(label: MultiPurposeLabelSummary) { |  | ||||||
|       isOpen.value[label.id] = !isOpen.value[label.id]; |  | ||||||
|  |  | ||||||
|       if (isOpen.value[label.id]) { |  | ||||||
|         saveLastGoodValue(label); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       isOpen.value = { ...isOpen.value }; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // ========================================================== |  | ||||||
|     // Color Generators |  | ||||||
|  |  | ||||||
|     function getRandomHex() { |  | ||||||
|       const letters = "BCDEF".split(""); |  | ||||||
|       let color = "#"; |  | ||||||
|       for (let i = 0; i < 6; i++) { |  | ||||||
|         color += letters[Math.floor(Math.random() * letters.length)]; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return color; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setRandomHex(labelIndex: number) { |  | ||||||
|       if (!labels.value) { |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       labels.value[labelIndex].color = getRandomHex(); |  | ||||||
|       labels.value = [...labels.value]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // ========================================================== |  | ||||||
|     // Search / Filter |  | ||||||
|  |  | ||||||
|     const searchInput = ref(""); |  | ||||||
|  |  | ||||||
|     const labelNames = computed(() => { |  | ||||||
|       return labels.value?.map((label) => label.name) ?? []; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     const fuseOpts = { |  | ||||||
|       shouldSort: true, |  | ||||||
|       threshold: 0.5, |  | ||||||
|       location: 0, |  | ||||||
|       distance: 100, |  | ||||||
|       findAllMatches: true, |  | ||||||
|       maxPatternLength: 32, |  | ||||||
|       minMatchCharLength: 2, |  | ||||||
|       keys: ["name"], |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     const fuse = computed(() => { |  | ||||||
|       return new Fuse(labelNames.value, fuseOpts); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     const results = computed(() => { |  | ||||||
|       if (!searchInput.value) { |  | ||||||
|         return labels.value; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const foundName = fuse.value.search(searchInput.value).map((result) => result.item); |  | ||||||
|  |  | ||||||
|       return labels.value?.filter((label) => foundName.includes(label.name)) ?? []; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|       saveLastGoodValue, |  | ||||||
|       resetToLastGoodValue, |  | ||||||
|       deleteDialog, |  | ||||||
|       deleteTargetId, |  | ||||||
|       confirmDelete, |  | ||||||
|       createLabelData, |  | ||||||
|       createLabel, |  | ||||||
|       createDialog, |  | ||||||
|       results, |  | ||||||
|       searchInput, |  | ||||||
|       updateLabel, |  | ||||||
|       deleteLabel, |  | ||||||
|       setRandomHex, |  | ||||||
|       toggleIsOpen, |  | ||||||
|       isOpen, |  | ||||||
|       labels, |  | ||||||
|       refreshLabels, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|   head: { |  | ||||||
|     title: "Shopping List Labels", |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -6,7 +6,7 @@ from pydantic import UUID4 | |||||||
|  |  | ||||||
| class MultiPurposeLabelCreate(CamelModel): | class MultiPurposeLabelCreate(CamelModel): | ||||||
|     name: str |     name: str | ||||||
|     color: str = "" |     color: str = "#E0E0E0" | ||||||
|  |  | ||||||
|  |  | ||||||
| class MultiPurposeLabelSave(MultiPurposeLabelCreate): | class MultiPurposeLabelSave(MultiPurposeLabelCreate): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user