many editor improvements (and more)

This commit is contained in:
vabene1111
2025-01-04 17:59:56 +01:00
parent c691d6028b
commit c75d265686
40 changed files with 256 additions and 61 deletions

View File

@@ -28,7 +28,7 @@
</v-list-item>
<v-divider v-if="mealPlanGridItem.plan_entries.length > 0"></v-divider>
<v-list-item v-for="p in mealPlanGridItem.plan_entries" link>
<v-list-item v-for="p in mealPlanGridItem.plan_entries" :key="p.id" @click="clickMealPlan(p)" link>
<template #prepend>
<v-avatar :image="p.recipe.image" v-if="p.recipe?.image"></v-avatar>
<v-avatar image="../../assets/recipe_no_image.svg" v-else></v-avatar>
@@ -40,7 +40,20 @@
<v-list-item-subtitle>
{{ p.mealType.name }}
</v-list-item-subtitle>
<model-edit-dialog model="MealPlan" :item="p"></model-edit-dialog>
<model-edit-dialog model="MealPlan" :item="p" v-if="!p.recipe"></model-edit-dialog>
<template #append>
<v-btn icon variant="plain">
<v-icon icon="$menu"></v-icon>
<v-menu activator="parent">
<v-list>
<v-list-item prepend-icon="$edit">
{{$t('Edit')}}
<model-edit-dialog model="MealPlan" :item="p"></model-edit-dialog>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</template>
</v-list-item>
</v-list>
@@ -62,7 +75,9 @@ import {useMealPlanStore} from "@/stores/MealPlanStore";
import {DateTime} from "luxon";
import {homePageCols} from "@/utils/breakpoint_utils";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import {useRouter} from "vue-router";
const router = useRouter()
const {name} = useDisplay()
const loading = ref(false)
@@ -119,6 +134,12 @@ onMounted(() => {
})
})
function clickMealPlan(plan: MealPlan){
if(plan.recipe){
router.push( {name: 'view_recipe', params: {id: plan.recipe.id}})
}
}
</script>

View File

@@ -1,7 +1,12 @@
<template>
<span v-if="ingredient.amount && !Number.isNaN(ingredient.amount)">{{$n(ingredient.amount)}}</span>
<span class="ms-1" v-if="ingredient.unit">{{ ingredient.unit.name}}</span>
<span class="ms-1" v-if="ingredient.food">{{ ingredient.food.name}}</span>
<template v-if="ingredient.isHeader">
<span class="font-weight-bold">{{ ingredient.note}}</span>
</template>
<template v-else>
<span v-if="ingredient.amount && !Number.isNaN(ingredient.amount)">{{$n(ingredient.amount)}}</span>
<span class="ms-1" v-if="ingredient.unit">{{ ingredient.unit.name}}</span>
<span class="ms-1" v-if="ingredient.food">{{ ingredient.food.name}}</span>
</template>
</template>

View File

@@ -8,20 +8,51 @@
<!-- </v-table>-->
<v-data-table :items="ingredients" hide-default-footer hide-default-header :headers="tableHeaders" density="compact" v-if="ingredients.length > 0" @click:row="handleRowClick" items-per-page="0">
<template v-slot:item.checked="{ item }">
<v-checkbox-btn v-model="item.checked" color="success"></v-checkbox-btn>
</template>
<template v-slot:item.amount="{ item }">
{{ item.amount * props.ingredientFactor }}
</template>
<!-- <v-data-table :items="ingredients" hide-default-footer hide-default-header :headers="tableHeaders" density="compact" v-if="ingredients.length > 0" @click:row="handleRowClick"-->
<!-- items-per-page="0">-->
<!-- <template v-slot:item.checked="{ item }">-->
<!-- <v-checkbox-btn v-model="item.checked" color="success" v-if="!item.isHeader"></v-checkbox-btn>-->
<!-- </template>-->
<!-- <template v-slot:item.amount="{ item }">-->
<!-- <template v-if="item.isHeader"><p style="width: 100px"><b>{{ item.note }}</b></p></template>-->
<!-- <template v-else>{{ item.amount * props.ingredientFactor }}</template>-->
<!-- </template>-->
<template v-slot:item.note="{ item }">
<v-icon class="far fa-comment float-right" v-if="item.note != '' && item.note != undefined">
<v-tooltip activator="parent" open-on-click location="start">{{ item.note }}</v-tooltip>
</v-icon>
</template>
</v-data-table>
<!-- <template v-slot:item.note="{ item }">-->
<!-- <v-icon class="far fa-comment float-right" v-if="item.note != '' && item.note != undefined">-->
<!-- <v-tooltip activator="parent" open-on-click location="start">{{ item.note }}</v-tooltip>-->
<!-- </v-icon>-->
<!-- </template>-->
<!-- </v-data-table>-->
<v-table density="compact">
<tbody>
<tr v-for="i in ingredients" :key="i.id" @click="i.checked = !i.checked">
<template v-if="i.isHeader">
<td colspan="5" class="font-weight-bold">{{ i.note }}</td>
</template>
<template v-else>
<td style="width: 1%; text-wrap: nowrap" class="pa-0">
<v-checkbox-btn v-model="i.checked" color="success" v-if="!i.isHeader"></v-checkbox-btn>
</td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1">{{ i.amount * props.ingredientFactor }}</td>
<td style="width: 1%; text-wrap: nowrap" class="pr-1">
<template v-if="i.unit"> {{ i.unit.name }}</template>
</td>
<td>
<template v-if="i.food"> {{ i.food.name }}</template>
</td>
<td style="width: 1%; text-wrap: nowrap">
<v-icon class="far fa-comment float-right" v-if="i.note != '' && i.note != undefined">
<v-tooltip activator="parent" open-on-click location="start">{{ i.note }}</v-tooltip>
</v-icon>
</td>
</template>
</tr>
</tbody>
</v-table>
</template>

View File

@@ -1,11 +1,8 @@
<template>
<!-- TODO label is not showing for some reason, for now in placeholder -->
<!-- TODO support density prop -->
<v-input :hint="props.hint" persistent-hint :label="props.label">
<template #prepend>
<slot name="prepend">
</slot>
<slot name="prepend"></slot>
</template>
<!-- TODO resolve-on-load false for now, race condition with model class, make prop once better solution is found -->
<Multiselect
@@ -160,7 +157,8 @@ async function createObject(object: any, select$: Multiselect) {
</script>
<style src="@vueform/multiselect/themes/default.css"></style>
<style scoped>
<!-- style can't be scoped (for whatever reason) -->
<style>
.material-multiselect {
--ms-bg: rgba(210, 210, 210, 0.1);
--ms-border-color: 0;

View File

@@ -5,7 +5,7 @@
<v-card variant="outlined">
<template #title>
<v-card-title>
<v-chip color="primary">{{ props.stepIndex + 1 }}</v-chip>
<v-chip color="primary">{{$t('Step')}} {{ props.stepIndex + 1 }}</v-chip>
{{ step.name }}
</v-card-title>
</template>
@@ -54,18 +54,21 @@
<vue-draggable v-model="step.ingredients" handle=".drag-handle" :on-sort="sortIngredients" v-if="!mobile">
<v-row v-for="(ingredient, index) in step.ingredients" dense>
<v-col cols="2">
<v-number-input :id="`id_input_amount_${step.id}_${index}`" :label="$t('Amount')" v-model="ingredient.amount" inset control-variant="stacked"
hide-details
:min="0"></v-number-input>
<v-text-field :id="`id_input_amount_${step.id}_${index}`" :label="$t('Amount')" type="number" v-model="ingredient.amount" density="compact" hide-details>
<template #prepend>
<v-icon icon="$dragHandle" class="drag-handle cursor-grab"></v-icon>
</template>
</v-text-field>
</v-col>
<v-col cols="3">
<model-select model="Unit" v-model="ingredient.unit" allow-create hide-details></model-select>
<model-select model="Unit" v-model="ingredient.unit" density="compact" allow-create hide-details></model-select>
</v-col>
<v-col cols="3">
<model-select model="Food" v-model="ingredient.food" allow-create hide-details></model-select>
<model-select model="Food" v-model="ingredient.food" density="compact" allow-create hide-details></model-select>
</v-col>
<v-col cols="3" @keydown.tab="event => handleIngredientNoteTab(event, index)">
<v-text-field :label="$t('Note')" v-model="ingredient.note" hide-details></v-text-field>
<v-text-field :label="$t('Note')" v-model="ingredient.note" density="compact" hide-details></v-text-field>
</v-col>
<v-col cols="1">
<v-btn variant="plain" icon>
@@ -76,7 +79,7 @@
</v-list>
</v-menu>
</v-btn>
<v-icon icon="$dragHandle" class="drag-handle"></v-icon>
</v-col>
</v-row>
</vue-draggable>
@@ -152,10 +155,21 @@
<v-card-text>
<v-form>
<v-number-input v-model="step.ingredients[editingIngredientIndex].amount" inset control-variant="stacked" autofocus :label="$t('Amount')"
:min="0"></v-number-input>
<model-select model="Unit" v-model="step.ingredients[editingIngredientIndex].unit" :label="$t('Unit')" allow-create></model-select>
<model-select model="Food" v-model="step.ingredients[editingIngredientIndex].food" :label="$t('Food')" allow-create></model-select>
<v-text-field :label="$t('Note')" v-model="step.ingredients[editingIngredientIndex].note"></v-text-field>
:min="0" v-if="!step.ingredients[editingIngredientIndex].isHeader"></v-number-input>
<model-select model="Unit" v-model="step.ingredients[editingIngredientIndex].unit" :label="$t('Unit')" v-if="!step.ingredients[editingIngredientIndex].isHeader"
allow-create></model-select>
<model-select model="Food" v-model="step.ingredients[editingIngredientIndex].food" :label="$t('Food')" v-if="!step.ingredients[editingIngredientIndex].isHeader"
allow-create></model-select>
<v-text-field :label="(step.ingredients[editingIngredientIndex].isHeader) ?$t('Headline') : $t('Note')"
v-model="step.ingredients[editingIngredientIndex].note"></v-text-field>
<v-checkbox
v-model="step.ingredients[editingIngredientIndex].isHeader"
:label="$t('Headline')"
:hint="$t('HeaderWarning')"
persistent-hint
@update:modelValue="step.ingredients[editingIngredientIndex].unit = null; step.ingredients[editingIngredientIndex].food = null; step.ingredients[editingIngredientIndex].amount = 0"
></v-checkbox>
</v-form>
</v-card-text>
<v-card-actions>
@@ -169,8 +183,8 @@
</template>
<script setup lang="ts">
import {nextTick, ref, useTemplateRef} from 'vue'
import {ApiApi, Ingredient, ParsedIngredient, Step} from "@/openapi";
import {nextTick, onMounted, ref} from 'vue'
import {ApiApi, Ingredient, ParsedIngredient, Step, Unit} from "@/openapi";
import StepMarkdownEditor from "@/components/inputs/StepMarkdownEditor.vue";
import {VNumberInput} from 'vuetify/labs/VNumberInput'
import ModelSelect from "@/components/inputs/ModelSelect.vue";
@@ -178,6 +192,8 @@ import {useDisplay} from "vuetify";
import {VueDraggable} from "vue-draggable-plus";
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import IngredientString from "@/components/display/IngredientString.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
const emit = defineEmits(['delete'])
@@ -199,7 +215,24 @@ const dialogIngredientParser = ref(false)
const editingIngredientIndex = ref(Number)
const ingredientTextInput = ref("")
const ingredientDialogAmountRef = useTemplateRef('ref_input_amount_dialog')
const defaultUnit = ref<null | Unit>(null)
onMounted(() => {
let api = new ApiApi()
if (useUserPreferenceStore().userSettings.defaultUnit) {
api.apiUnitList({query: useUserPreferenceStore().userSettings.defaultUnit}).then(r => {
r.results.forEach(u => {
if (u.name == useUserPreferenceStore().userSettings.defaultUnit) {
defaultUnit.value = u
}
})
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
})
/**
* sort function called by draggable when ingredient table is sorted
@@ -252,7 +285,13 @@ function handleIngredientNoteTab(event: KeyboardEvent, index: number) {
* insert a new ingredient and focus its first input
*/
function insertAndFocusIngredient() {
step.value.ingredients.push({} as Ingredient)
let ingredient = {} as Ingredient
if (defaultUnit.value != null) {
ingredient.unit = defaultUnit.value
}
step.value.ingredients.push(ingredient)
nextTick(() => {
if (mobile.value) {
editingIngredientIndex.value = step.value.ingredients.length - 1

View File

@@ -1,27 +1,62 @@
<template>
<mavon-editor v-model="steep.instruction" :autofocus="false"
style="z-index: auto" :id="'id_instruction_' + steep.id"
<mavon-editor v-model="step.instruction" :autofocus="false"
style="z-index: auto" :id="'id_instruction_' + step.id"
:language="'en'"
:toolbars="md_editor_toolbars" :defaultOpen="'edit'">
<template #left-toolbar-after>
<span class="op-icon-divider"></span>
<button
type="button"
@click="steep.instruction+= ' {{ scale(100) }}'"
@click="step.instruction+= ' {{ scale(100) }}'"
class="op-icon fas fa-calculator"
aria-hidden="true"
:title="$t('ScalableNumber')"
></button>
<button class="op-icon fa-solid fa-code">
<v-menu activator="parent">
<v-list density="compact">
<v-list-item
v-for="template in templates"
@click="step.instruction+= template.template"
>
<ingredient-string :ingredient="template.ingredient"></ingredient-string>
</v-list-item>
</v-list>
</v-menu>
</button>
</template>
</mavon-editor>
</template>
<script setup lang="ts">
import {Step} from "@/openapi";
import {Ingredient, Step} from "@/openapi";
import 'mavon-editor/dist/css/index.css'
import IngredientString from "@/components/display/IngredientString.vue";
import {computed} from "vue";
const steep = defineModel<Step>({required: true})
const step = defineModel<Step>({required: true})
type IngredientTemplate = {
name: string,
ingredient: Ingredient,
template: string,
}
const templates = computed(() => {
let templateList: IngredientTemplate[] = []
step.value.ingredients.forEach((ingredient, index) => {
if (!ingredient.isHeader && ingredient.food != null)
templateList.push({
name: ingredient.food.name,
ingredient: ingredient,
template: `{{ ingredients[${index}] }}{# ${ingredient.food.name} #}`
} as IngredientTemplate)
})
return templateList
})
const md_editor_toolbars = {
bold: true,

View File

@@ -26,24 +26,25 @@
<v-textarea :label="$t('Description')" v-model="editingObj.description" clearable counter="512" rows="2"></v-textarea>
<v-row>
<v-col cols="12" md="6">
<v-col cols="12" md="6" >
<v-file-upload v-model="file" @update:modelValue="updateUserFileName"
:title="$t('DragToUpload')"
:title="(mobile) ? $t('Select_File') : $t('DragToUpload')"
:browse-text="$t('Select_File')"
:divider-text="$t('or')"
></v-file-upload>
:density="(mobile) ? 'compact' : 'comfortable'"
>
</v-file-upload>
</v-col>
<v-col cols="12" md="6">
<v-label>{{ $t('Image') }}</v-label>
<v-img style="max-height: 150px" class="mb-2" :src="editingObj.image">
<v-btn color="delete" class="float-right" prepend-icon="$delete" v-if="editingObj.image" @click="deleteImage()">{{ $t('Delete') }}</v-btn>
<v-col cols="12" md="6" v-if="editingObj.image">
<v-img style="max-height: 180px" cover class="mb-2" :src="editingObj.image">
<v-btn color="delete" class="float-right mt-2 mr-2" prepend-icon="$delete" v-if="editingObj.image" @click="deleteImage()">{{ $t('Delete') }}</v-btn>
</v-img>
</v-col>
</v-row>
<v-label>{{ $t('Keywords') }}</v-label>
<ModelSelect mode="tags" v-model="editingObj.keywords" model="Keyword"></ModelSelect>
<v-row>
<model-select mode="tags" v-model="editingObj.keywords" model="Keyword" allow-create></model-select>
<v-row dense>
<v-col cols="12" md="6">
<v-text-field :label="$t('WaitingTime')" v-model="editingObj.waitingTime"></v-text-field>
</v-col>
@@ -95,8 +96,8 @@
<v-text-field :label="$t('Imported_From')" v-model="editingObj.sourceUrl"></v-text-field>
<v-checkbox :label="$t('Private_Recipe')" :hint="$t('Private_Recipe_Help')" persistent-hint v-model="editingObj._private"></v-checkbox>
<ModelSelect mode="tags" model="User" :label="$t('Private_Recipe')" :hint="$t('Private_Recipe_Help')" persistent-hint v-model="editingObj.shared"
append-to-body></ModelSelect>
<model-select mode="tags" model="User" :label="$t('Private_Recipe')" :hint="$t('Private_Recipe_Help')" persistent-hint v-model="editingObj.shared"
append-to-body></model-select>
</v-form>
</v-tabs-window-item>
@@ -137,6 +138,7 @@ import PropertiesEditor from "@/components/inputs/PropertiesEditor.vue";
import {useFileApi} from "@/composables/useFileApi";
import {VFileUpload} from 'vuetify/labs/VFileUpload'
import ClosableHelpAlert from "@/components/display/ClosableHelpAlert.vue";
import {useDisplay} from "vuetify";
const props = defineProps({
@@ -149,6 +151,8 @@ const emit = defineEmits(['create', 'save', 'delete', 'close'])
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<Recipe>('Recipe', emit)
// object specific data (for selects/display)
const {mobile} = useDisplay()
const tab = ref("recipe")
const dialogStepManager = ref(false)
@@ -190,7 +194,7 @@ function deleteImage() {
/**
* add a new step to the recipe
*/
function addStep(){
function addStep() {
editingObj.value.steps.push({
ingredients: [] as Ingredient[],
time: 0,
@@ -210,7 +214,7 @@ function sortSteps() {
* delete a step at the given index of the steps array of the editingObject
* @param index index to delete at
*/
function deleteStepAtIndex(index: number){
function deleteStepAtIndex(index: number) {
editingObj.value.steps.splice(index, 1)
}