batch merge view

This commit is contained in:
vabene1111
2025-08-20 22:20:23 +02:00
parent a30deb4bae
commit 5e1c804fd1
28 changed files with 119 additions and 54 deletions

View File

@@ -13,17 +13,17 @@
{{ $t('BatchDeleteConfirm') }}
<v-list>
<v-list-item border v-for="i in itemsToDelete">
{{ genericModel.getLabel(i) }}
<v-list-item border v-for="item in itemsToDelete">
{{ genericModel.getLabel(item) }}
<template #append>
<v-chip prepend-icon="fa-solid fa-xmark" color="error" v-if="deletedErrored.includes(i)">{{$t('Error')}}</v-chip>
<v-chip prepend-icon="fa-solid fa-check" color="success" v-else-if="deletedItems.includes(i)">{{$t('Deleted')}}</v-chip>
<v-chip prepend-icon="fa-solid fa-circle-notch fa-spin" color="info" v-else-if="loading">{{$t('Waiting')}}</v-chip>
<v-icon icon="fa-solid fa-xmark" color="error" variant="tonal" v-if="failedItems.includes(item)"></v-icon>
<v-icon icon="fa-solid fa-check" color="success" variant="tonal" v-else-if="updatedItems.includes(item)"></v-icon>
<v-icon icon="fa-solid fa-circle-notch fa-spin" variant="tonal" color="info" v-else-if="loading"></v-icon>
<v-btn icon="fa-solid fa-up-right-from-square" :to="{name: 'IngredientEditorPage', query: {food_id: i.id}}"
v-if="genericModel.model.name == 'Food' && deletedErrored.includes(i)" size="small"></v-btn>
<v-btn icon="fa-solid fa-up-right-from-square" :to="{name: 'IngredientEditorPage', query: {unit_id: i.id}}"
v-if="genericModel.model.name == 'Unit' && deletedErrored.includes(i)" size="small"></v-btn>
<v-btn icon="fa-solid fa-up-right-from-square" :to="{name: 'IngredientEditorPage', query: {food_id: item.id}}"
v-if="genericModel.model.name == 'Food' && failedItems.includes(item)" size="small"></v-btn>
<v-btn icon="fa-solid fa-up-right-from-square" :to="{name: 'IngredientEditorPage', query: {unit_id: item.id}}"
v-if="genericModel.model.name == 'Unit' && failedItems.includes(item)" size="small"></v-btn>
</template>
</v-list-item>
</v-list>
@@ -48,7 +48,7 @@ const emit = defineEmits(['change'])
const props = defineProps({
model: {type: String as PropType<EditorSupportedModels>, required: true},
items: {type: [] as PropType<Array<EditorSupportedTypes>>, required: true},
items: {type: Array as PropType<Array<EditorSupportedTypes>>, required: true},
activator: {type: String, default: 'parent'},
})
@@ -60,8 +60,8 @@ const loading = ref(false)
const genericModel = getGenericModelFromString(props.model, t)
const itemsToDelete = ref<EditorSupportedTypes[]>([])
const deletedItems = ref<EditorSupportedTypes[]>([])
const deletedErrored = ref<EditorSupportedTypes[]>([])
const failedItems = ref<EditorSupportedTypes[]>([])
const updatedItems = ref<EditorSupportedTypes[]>([])
watch(dialog, (newValue, oldValue) => {
if(!oldValue && newValue){
@@ -78,9 +78,9 @@ function deleteAll() {
itemsToDelete.value.forEach(item => {
promises.push(genericModel.destroy(item.id!).then((r: any) => {
deletedItems.value.push(item)
updatedItems.value.push(item)
}).catch((err: any) => {
deletedErrored.value.push(item)
failedItems.value.push(item)
}))
})

View File

@@ -3,26 +3,38 @@
<v-card :loading="loading">
<v-closable-card-title
:title="$t('merge_title', {type: $t(genericModel.model.localizationKey)})"
:sub-title="genericModel.getLabel(props.source)"
:sub-title="sourceNames"
:icon="genericModel.model.icon"
v-model="dialog"
></v-closable-card-title>
<v-divider></v-divider>
<v-card-text>
{{ $t('merge_selection', {source: genericModel.getLabel(props.source), type: $t(genericModel.model.localizationKey)}) }}
{{ $t('merge_selection', {source: sourceNames, type: $t(genericModel.model.localizationKey)}) }}
<model-select :model="props.model" v-model="target" allow-create></model-select>
<v-row>
<v-col class="text-center">
<v-card color="warning" variant="tonal">
<v-card-title>{{ genericModel.getLabel(props.source) }}</v-card-title>
</v-card>
<v-icon icon="fa-solid fa-arrow-down" class="mt-4 mb-4"></v-icon>
<v-card color="success" variant="tonal">
<v-card-title v-if="!target">?</v-card-title>
<v-card-title v-else>{{ genericModel.getLabel(target) }}</v-card-title>
</v-card>
<v-col>
<v-list>
<v-list-item border v-for="item in sourceItems">
{{ genericModel.getLabel(item) }}
<template #append>
<v-icon icon="fa-solid fa-xmark" color="error" variant="tonal" v-if="failedItems.includes(item)"></v-icon>
<v-icon icon="fa-solid fa-check" color="success" variant="tonal" v-else-if="updatedItems.includes(item)"></v-icon>
<v-icon icon="fa-solid fa-circle-notch fa-spin" variant="tonal" color="info" v-else-if="loading"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-center">
<v-icon icon="fa-solid fa-arrow-down" class="mt-4 mb-4"></v-icon>
</v-list-item>
<v-list-item class="text-center" border>
<span v-if="!target">?</span>
<span v-else>{{ genericModel.getLabel(target) }}</span>
</v-list-item>
</v-list>
</v-col>
</v-row>
@@ -40,7 +52,7 @@
<script setup lang="ts">
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {PropType, ref} from "vue";
import {computed, PropType, ref, watch} from "vue";
import {EditorSupportedModels, EditorSupportedTypes, getGenericModelFromString} from "@/types/Models";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {useI18n} from "vue-i18n";
@@ -51,7 +63,7 @@ const emit = defineEmits(['change'])
const props = defineProps({
model: {type: String as PropType<EditorSupportedModels>, required: true},
source: {type: {} as PropType<EditorSupportedTypes>, required: true},
source: {type: Array as PropType<Array<EditorSupportedTypes>>, required: true},
activator: {type: String, default: 'parent'},
})
@@ -64,41 +76,62 @@ const automate = ref(false)
const genericModel = getGenericModelFromString(props.model, t)
const target = ref<null | EditorSupportedTypes>(null)
const sourceItems = ref<EditorSupportedTypes[]>([])
const failedItems = ref<EditorSupportedTypes[]>([])
const updatedItems = ref<EditorSupportedTypes[]>([])
watch(dialog, (newValue, oldValue) => {
if (!oldValue && newValue) {
sourceItems.value = JSON.parse(JSON.stringify(props.source))
}
})
/**
* generate comma seperated list of item names that act as the source
*/
const sourceNames = computed(() => {
if (sourceItems.value) {
return sourceItems.value.map(i => genericModel.getLabel(i)).join(', ')
}
return ''
})
/**
* merge source into selected target
*/
function mergeModel() {
let api = new ApiApi()
let promises: Promise<any>[] = []
if (target.value != null) {
loading.value = true
genericModel.merge(props.source, target.value).then(r => {
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS)
emit('change', target.value)
sourceItems.value.forEach(sourceItem => {
promises.push(genericModel.merge(sourceItem, target.value).then(r => {
if (automate.value && target.value != null && Object.hasOwn(props.source, 'name') && Object.hasOwn(target.value, 'name')) {
let automation = {
name: `${t('Merge') } ${props.source.name} -> ${target.value.name}`.substring(0,128),
param1: props.source.name,
param2: target.value.name,
type: genericModel.model.mergeAutomation
} as Automation
api.apiAutomationCreate({automation: automation}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}).finally(() => {
loading.value = false
dialog.value = false
})
}
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}).finally(() => {
if (!automate.value) {
loading.value = false
dialog.value = false
}
updatedItems.value.push(sourceItem)
if (automate.value && target.value != null && Object.hasOwn(sourceItem, 'name') && Object.hasOwn(sourceItem, 'name')) {
let automation = {
name: `${t('Merge')} ${sourceItem.name} -> ${target.value.name}`.substring(0, 128),
param1: sourceItem.name,
param2: target.value.name,
type: genericModel.model.mergeAutomation
} as Automation
promises.push(api.apiAutomationCreate({automation: automation}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}))
}
}).catch(err => {
updatedItems.value.push(sourceItem)
}))
})
Promise.allSettled(promises).then(() => {
loading.value = false
emit('change')
})
}
}

View File

@@ -153,6 +153,7 @@
"Meal_Type_Required": "",
"Meal_Types": "",
"Merge": "",
"MergeAutomateHelp": "",
"Merge_Keyword": "",
"Message": "",
"MissingProperties": "",

View File

@@ -148,6 +148,7 @@
"Meal_Type_Required": "Изисква се вид хранене",
"Meal_Types": "Видове хранене",
"Merge": "Обединяване",
"MergeAutomateHelp": "",
"Merge_Keyword": "Обединяване на ключова дума",
"MissingProperties": "",
"Month": "Месец",

View File

@@ -204,6 +204,7 @@
"Meal_Type_Required": "El tipus és obligatori",
"Meal_Types": "Tipus de menjars",
"Merge": "Unificar",
"MergeAutomateHelp": "",
"Merge_Keyword": "Fusioneu paraula clau",
"Message": "Missatge",
"MissingProperties": "",

View File

@@ -202,6 +202,7 @@
"Meal_Type_Required": "Druh jídla je povinný",
"Meal_Types": "Druhy jídel",
"Merge": "Spojit",
"MergeAutomateHelp": "",
"Merge_Keyword": "Sloučit štítek",
"Message": "Zpráva",
"MissingProperties": "",

View File

@@ -204,6 +204,7 @@
"Meal_Type_Required": "Måltidstype påkrævet",
"Meal_Types": "Måltidstyper",
"Merge": "Sammenflet",
"MergeAutomateHelp": "",
"Merge_Keyword": "Sammenflet nøgleord",
"Message": "Besked",
"MissingProperties": "",

View File

@@ -288,7 +288,7 @@
"Meal_Type_Required": "Mahlzeitentyp ist erforderlich",
"Meal_Types": "Mahlzeiten",
"Merge": "Zusammenführen",
"MergeAutomateHelp": "Erstelle eine Automatisierung ,die auch zukünftig erstellte Objekte mit diesem Namen durch das gewählte Objekt ersetzt.",
"MergeAutomateHelp": "Erstelle eine Automatisierung, die auch zukünftig erstellte Objekte mit diesem Namen durch das gewählte Objekt ersetzt.",
"Merge_Keyword": "Schlagworte zusammenführen",
"Message": "Nachricht",
"Messages": "Nachrichten",

View File

@@ -204,6 +204,7 @@
"Meal_Type_Required": "Το είδος του γεύματος είναι απαραίτητο",
"Meal_Types": "Είδη γευμάτων",
"Merge": "Συγχώνευση",
"MergeAutomateHelp": "",
"Merge_Keyword": "Συγχώνευση λέξης-κλειδί",
"Message": "Μήνυμα",
"MissingProperties": "",

View File

@@ -198,6 +198,7 @@
"Meal_Type_Required": "Ateriatyyppi pakollinen",
"Meal_Types": "Ateriatyypit",
"Merge": "Yhdistä",
"MergeAutomateHelp": "",
"Merge_Keyword": "Yhdistä Avainsana",
"Message": "Viesti",
"MissingProperties": "",

View File

@@ -204,6 +204,7 @@
"Meal_Type_Required": "סוג אוכל נדרש",
"Meal_Types": "סוגי אוכל",
"Merge": "איחוד",
"MergeAutomateHelp": "",
"Merge_Keyword": "איחוד מילת מפתח",
"Message": "הודעה",
"MissingProperties": "",

View File

@@ -204,6 +204,7 @@
"Meal_Type_Required": "Potreban je tip obroka",
"Meal_Types": "Tipovi obroka",
"Merge": "Spoji",
"MergeAutomateHelp": "",
"Merge_Keyword": "Spoji ključnu riječ",
"Message": "Poruka",
"MissingProperties": "",

View File

@@ -185,6 +185,7 @@
"Meal_Type_Required": "Étkezés megadása kötelező",
"Meal_Types": "Étkezések",
"Merge": "Összefűzés",
"MergeAutomateHelp": "",
"Merge_Keyword": "Kulcsszó összevonása",
"Message": "Üzenet",
"MissingProperties": "",

View File

@@ -68,6 +68,7 @@
"Manage_Books": "Կարգավորել Գրքերը",
"Meal_Plan": "Ճաշացուցակ",
"Merge": "Միացնել",
"MergeAutomateHelp": "",
"Merge_Keyword": "Միացնել բանալի բառը",
"MissingProperties": "",
"Move": "Տեղափոխել",

View File

@@ -170,6 +170,7 @@
"Meal_Type_Required": "",
"Meal_Types": "",
"Merge": "Menggabungkan",
"MergeAutomateHelp": "",
"Merge_Keyword": "Gabungkan Kata Kunci",
"Message": "",
"MissingProperties": "",

View File

@@ -203,6 +203,7 @@
"Meal_Type_Required": "",
"Meal_Types": "",
"Merge": "",
"MergeAutomateHelp": "",
"Merge_Keyword": "",
"Message": "",
"MissingProperties": "",

View File

@@ -187,6 +187,7 @@
"Meal_Type_Required": "",
"Meal_Types": "",
"Merge": "",
"MergeAutomateHelp": "",
"Merge_Keyword": "Sujungti raktažodį",
"Message": "",
"MissingProperties": "",

View File

@@ -204,6 +204,7 @@
"Meal_Type_Required": "",
"Meal_Types": "",
"Merge": "",
"MergeAutomateHelp": "",
"Merge_Keyword": "",
"Message": "",
"MissingProperties": "",

View File

@@ -194,6 +194,7 @@
"Meal_Type_Required": "Måltidstype er nødvendig",
"Meal_Types": "Måltidstyper",
"Merge": "Slå sammen",
"MergeAutomateHelp": "",
"Merge_Keyword": "Slå sammen nøkkelord",
"Message": "Melding",
"MissingProperties": "",

View File

@@ -230,6 +230,7 @@
"Meal_Type_Required": "Rodzaj posiłku jest wymagany",
"Meal_Types": "Rodzaje posiłków",
"Merge": "Scal",
"MergeAutomateHelp": "",
"Merge_Keyword": "Scal słowa kluczowe",
"Message": "Wiadomość",
"MissingProperties": "",

View File

@@ -160,6 +160,7 @@
"Meal_Type_Required": "Tipo de refeição é necessário",
"Meal_Types": "Tipos de refeições",
"Merge": "Juntar",
"MergeAutomateHelp": "",
"Merge_Keyword": "Unir palavra-chave",
"MissingProperties": "",
"Month": "Mês",

View File

@@ -285,6 +285,7 @@
"Meal_Type_Required": "Tipo de comida é obrigatório",
"Meal_Types": "Tipos de Comida",
"Merge": "Mesclar",
"MergeAutomateHelp": "",
"Merge_Keyword": "Mesclar palavra-chave",
"Message": "Mensagem",
"MissingProperties": "",

View File

@@ -179,6 +179,7 @@
"Meal_Type_Required": "Tipul mesei este necesar",
"Meal_Types": "Tipuri de mese",
"Merge": "Unire",
"MergeAutomateHelp": "",
"Merge_Keyword": "Unește cuvânt cheie",
"Message": "Mesaj",
"MissingProperties": "",

View File

@@ -241,6 +241,7 @@
"Meal_Type_Required": "Måltidstyp är obligatorisk",
"Meal_Types": "Måltidstyper",
"Merge": "Slå samman",
"MergeAutomateHelp": "",
"Merge_Keyword": "Slå samman nyckelord",
"Message": "Meddelande",
"MissingProperties": "",

View File

@@ -204,6 +204,7 @@
"Meal_Type_Required": "Yemek türü gereklidir",
"Meal_Types": "Yemek türleri",
"Merge": "Birleştir",
"MergeAutomateHelp": "",
"Merge_Keyword": "Anahtar Kelimeyi Birleştir",
"Message": "Mesaj",
"MissingProperties": "",

View File

@@ -177,6 +177,7 @@
"Meal_Type_Required": "Тип страви є обов'язковим",
"Meal_Types": "Типи страви",
"Merge": "Об'єднати",
"MergeAutomateHelp": "",
"Merge_Keyword": "Об'єднати Ключове слово",
"MissingProperties": "",
"Month": "Місяць",

View File

@@ -204,6 +204,7 @@
"Meal_Type_Required": "用餐类型是必需的",
"Meal_Types": "用餐类型",
"Merge": "合并",
"MergeAutomateHelp": "",
"Merge_Keyword": "合并关键词",
"Message": "信息",
"MissingProperties": "",

View File

@@ -57,7 +57,10 @@
<v-icon icon="fa-solid fa-ellipsis-v"></v-icon>
<v-menu activator="parent" close-on-content-click>
<v-list density="compact" class="pt-1 pb-1" activatable>
<v-list-item prepend-icon="$delete" @click="batchDeleteDialog = true">
<v-list-item prepend-icon="fa-solid fa-arrows-to-dot" @click="batchMergeDialog = true" v-if="genericModel.model.isMerge">
{{ $t('Merge') }}
</v-list-item>
<v-list-item prepend-icon="$delete" @click="batchDeleteDialog = true" v-if="!genericModel.model.disableDelete">
{{ $t('Delete_All') }}
</v-list-item>
</v-list>
@@ -75,7 +78,7 @@
</v-list-item>
<v-list-item prepend-icon="fa-solid fa-arrows-to-dot" v-if="genericModel.model.isMerge" link>
{{ $t('Merge') }}
<model-merge-dialog :model="model" :source="item"
<model-merge-dialog :model="model" :source="[item]"
@change="loadItems({page: tablePage, itemsPerPage: useUserPreferenceStore().deviceSettings.general_tableItemsPerPage, search: searchQuery})"></model-merge-dialog>
</v-list-item>
<v-list-item prepend-icon="fa-solid fa-table-list" :to="{name: 'IngredientEditorPage', query: {food_id: item.id}}"
@@ -103,6 +106,10 @@
<batch-delete-dialog :items="selectedItems" :model="props.model" v-model="batchDeleteDialog" activator="model"
@change="loadItems({page: tablePage, itemsPerPage: useUserPreferenceStore().deviceSettings.general_tableItemsPerPage, search: searchQuery})"></batch-delete-dialog>
<model-merge-dialog :model="model" :source="selectedItems" v-model="batchMergeDialog" activator="model"
@change="loadItems({page: tablePage, itemsPerPage: useUserPreferenceStore().deviceSettings.general_tableItemsPerPage, search: searchQuery})"></model-merge-dialog>
</v-container>
</template>
@@ -149,6 +156,7 @@ const tablePage = ref(1)
const selectedItems = ref([] as EditorSupportedTypes[])
const batchDeleteDialog = ref(false)
const batchMergeDialog = ref(false)
// data
const loading = ref(false);