mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 04:10:06 -05:00
241 lines
11 KiB
Vue
241 lines
11 KiB
Vue
<template>
|
|
<model-editor-base
|
|
:loading="loading || fileApiLoading"
|
|
:dialog="dialog"
|
|
@save="saveRecipe"
|
|
@delete="deleteObject"
|
|
@close="emit('close'); editingObjChanged = false"
|
|
:is-update="isUpdate()"
|
|
:is-changed="editingObjChanged"
|
|
:model-class="modelClass"
|
|
:object-name="editingObjName()">
|
|
|
|
<v-card-text class="pa-0">
|
|
<v-tabs v-model="tab" :disabled="loading || fileApiLoading" grow>
|
|
<v-tab value="recipe">{{ $t('Recipe') }}</v-tab>
|
|
<v-tab value="steps">{{ $t('Steps') }}</v-tab>
|
|
<v-tab value="properties">{{ $t('Properties') }}</v-tab>
|
|
<v-tab value="settings">{{ $t('Miscellaneous') }}</v-tab>
|
|
</v-tabs>
|
|
</v-card-text>
|
|
<v-card-text v-if="!isSpaceAtRecipeLimit(useUserPreferenceStore().activeSpace)">
|
|
<v-tabs-window v-model="tab">
|
|
<v-tabs-window-item value="recipe">
|
|
|
|
<v-form :disabled="loading || fileApiLoading">
|
|
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
|
|
<v-textarea :label="$t('Description')" v-model="editingObj.description" clearable counter="512" rows="2" auto-grow></v-textarea>
|
|
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-file-upload v-model="file"
|
|
:title="(mobile) ? $t('Select_File') : $t('DragToUpload')"
|
|
:browse-text="$t('Select_File')"
|
|
:divider-text="$t('or')"
|
|
:density="(mobile) ? 'compact' : 'comfortable'"
|
|
>
|
|
</v-file-upload>
|
|
</v-col>
|
|
<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>
|
|
<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>
|
|
<v-col cols="12" md="6">
|
|
<v-text-field :label="$t('WorkingTime')" v-model="editingObj.workingTime"></v-text-field>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-text-field :label="$t('Servings')" v-model="editingObj.servings"></v-text-field>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-text-field :label="$t('ServingsText')" v-model="editingObj.servingsText"></v-text-field>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- <closable-help-alert :text="$t('RecipeStepsHelp')" :action-text="$t('Steps')" @click="tab='steps'"></closable-help-alert>-->
|
|
<v-btn @click="tab='steps'" class="float-right" variant="tonal" append-icon="fa-solid fa-arrow-right">{{$t('Steps')}} </v-btn>
|
|
</v-form>
|
|
|
|
</v-tabs-window-item>
|
|
<v-tabs-window-item value="steps">
|
|
<v-form :disabled="loading || fileApiLoading">
|
|
<v-row v-for="(s,i ) in editingObj.steps" :key="s.id">
|
|
<v-col>
|
|
<step-editor v-model="editingObj.steps[i]" v-model:recipe="editingObj" :step-index="i" @delete="deleteStepAtIndex(i)"></step-editor>
|
|
</v-col>
|
|
</v-row>
|
|
<v-row>
|
|
<v-col class="text-center">
|
|
<v-btn-group density="compact">
|
|
<v-btn color="success" prepend-icon="fa-solid fa-plus" @click="addStep()">{{ $t('Add_Step') }}</v-btn>
|
|
<v-btn color="warning" @click="dialogStepManager = true">
|
|
<v-icon icon="fa-solid fa-arrow-down-1-9"></v-icon>
|
|
</v-btn>
|
|
</v-btn-group>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
</v-form>
|
|
</v-tabs-window-item>
|
|
<v-tabs-window-item value="properties">
|
|
<v-form :disabled="loading || fileApiLoading">
|
|
<closable-help-alert :text="$t('PropertiesFoodHelp')"></closable-help-alert>
|
|
<properties-editor v-model="editingObj.properties" :amount-for="$t('Serving')"></properties-editor>
|
|
</v-form>
|
|
</v-tabs-window-item>
|
|
<v-tabs-window-item value="settings">
|
|
<v-form :disabled="loading || fileApiLoading">
|
|
<v-checkbox :label="$t('show_ingredient_overview')"
|
|
v-model="editingObj.showIngredientOverview"></v-checkbox>
|
|
|
|
<v-text-field :label="$t('Imported_From')" v-model="editingObj.sourceUrl"></v-text-field>
|
|
<v-checkbox :label="$t('Private_Recipe')" persistent-hint v-model="editingObj._private"></v-checkbox>
|
|
<model-select mode="tags" model="User" :label="$t('Private_Recipe')" :hint="$t('Private_Recipe_Help')" persistent-hint v-model="editingObj.shared"
|
|
append-to-body v-if="editingObj._private"></model-select>
|
|
|
|
</v-form>
|
|
</v-tabs-window-item>
|
|
</v-tabs-window>
|
|
</v-card-text>
|
|
<v-card-text v-if="isSpaceAtRecipeLimit(useUserPreferenceStore().activeSpace)">
|
|
<v-alert color="warning" icon="fa-solid fa-triangle-exclamation">
|
|
{{$t('SpaceLimitReached')}}
|
|
<v-btn color="success" variant="flat" :to="{name: 'SpaceSettings'}">{{ $t('SpaceSettings') }}</v-btn>
|
|
</v-alert>
|
|
</v-card-text>
|
|
</model-editor-base>
|
|
|
|
<v-dialog max-width="600px" v-model="dialogStepManager">
|
|
<v-card>
|
|
<v-card-title>{{ $t('Steps') }}</v-card-title>
|
|
<v-list>
|
|
<vue-draggable handle=".drag-handle" v-model="editingObj.steps" :on-sort="sortSteps">
|
|
<v-list-item v-for="(s,i) in editingObj.steps" :key="s.id">
|
|
<v-chip color="primary">{{ i + 1 }}</v-chip>
|
|
{{ s.name }}
|
|
<template #append>
|
|
<v-icon class="drag-handle" icon="$dragHandle"></v-icon>
|
|
</template>
|
|
</v-list-item>
|
|
</vue-draggable>
|
|
</v-list>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
|
|
import {onMounted, PropType, ref, shallowRef} from "vue";
|
|
import {Ingredient, Recipe, Step} from "@/openapi";
|
|
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
|
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
|
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
|
import StepEditor from "@/components/inputs/StepEditor.vue";
|
|
import {VueDraggable} from "vue-draggable-plus";
|
|
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";
|
|
import {isSpaceAtRecipeLimit} from "@/utils/logic_utils";
|
|
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
|
import SpaceSettings from "@/components/settings/SpaceSettings.vue";
|
|
|
|
|
|
const props = defineProps({
|
|
item: {type: {} as PropType<Recipe>, required: false, default: null},
|
|
itemId: {type: [Number, String], required: false, default: undefined},
|
|
itemDefaults: {type: {} as PropType<Recipe>, required: false, default: {} as Recipe},
|
|
dialog: {type: Boolean, default: false}
|
|
})
|
|
|
|
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)
|
|
|
|
const {fileApiLoading, updateRecipeImage} = useFileApi()
|
|
const file = shallowRef<File | null>(null)
|
|
|
|
onMounted(() => {
|
|
setupState(props.item, props.itemId, {
|
|
newItemFunction: () => {
|
|
editingObj.value.steps = [] as Step[]
|
|
editingObj.value.internal = true //TODO make database default after v2
|
|
},
|
|
itemDefaults: props.itemDefaults,
|
|
})
|
|
})
|
|
|
|
/**
|
|
* save recipe via normal saveMethod and update image afterward if it was changed
|
|
*/
|
|
function saveRecipe() {
|
|
saveObject().then(() => {
|
|
if (file.value != null && editingObj.value.id) {
|
|
updateRecipeImage(editingObj.value.id, file.value).then(r => {
|
|
file.value = null
|
|
setupState(props.item, props.itemId)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* remove image if delete was manually triggered
|
|
*/
|
|
function deleteImage() {
|
|
updateRecipeImage(editingObj.value.id!, null).then(r => {
|
|
setupState(props.item, props.itemId)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* add a new step to the recipe
|
|
*/
|
|
function addStep() {
|
|
editingObj.value.steps.push({
|
|
ingredients: [] as Ingredient[],
|
|
time: 0,
|
|
showIngredientsTable: useUserPreferenceStore().userSettings.showStepIngredients
|
|
} as Step)
|
|
}
|
|
|
|
/**
|
|
* called by draggable in step manager dialog when steps are sorted
|
|
*/
|
|
function sortSteps() {
|
|
editingObj.value.steps.forEach((value, index) => {
|
|
value.order = index
|
|
})
|
|
}
|
|
|
|
/**
|
|
* delete a step at the given index of the steps array of the editingObject
|
|
* @param index index to delete at
|
|
*/
|
|
function deleteStepAtIndex(index: number) {
|
|
editingObj.value.steps.splice(index, 1)
|
|
}
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
|
|
</style> |