auto import multiple urls

This commit is contained in:
vabene1111
2022-03-10 17:35:11 +01:00
parent 92b536b32c
commit 7426bb4e76
2 changed files with 98 additions and 453 deletions

View File

@@ -21,22 +21,66 @@
<b-collapse id="id_accordion_url" visible accordion="url_import_accordion"
role="tabpanel" v-model="collapse_visible.url">
<b-card-body>
<b-input-group>
<b-input v-model="website_url" placeholder="Website URL"
@paste="loadRecipe()"></b-input>
<b-input-group-append>
<b-button variant="primary" @click="loadRecipe()"><i
class="fas fa-search"></i>
</b-button>
</b-input-group-append>
</b-input-group>
<a href="#" @click="clearRecentImports()">Clear recent imports</a>
<ul>
<li v-for="x in recent_urls" v-bind:key="x">
<a href="#" @click="website_url=x; loadRecipe()">{{ x }}</a>
</li>
</ul>
<div class="row">
<div class="col col-md-12">
<b-checkbox v-model="import_multiple" switch><span
v-if="import_multiple">Multiple Recipes</span><span
v-if="!import_multiple">Single Recipe</span></b-checkbox>
</div>
</div>
<div class="row">
<div class="col col-md-12">
<b-input-group>
<b-input v-model="website_url" placeholder="Website URL"></b-input>
<b-input-group-append>
<b-button variant="primary" @click="loadRecipe(false,undefined)"
v-if="!import_multiple"><i
class="fas fa-search fa-fw"></i>
</b-button>
<b-button variant="primary"
@click="website_url_list.push(website_url); website_url=''"
v-if="import_multiple"><i
class="fas fa-plus fa-fw"></i>
</b-button>
</b-input-group-append>
</b-input-group>
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div v-if="!import_multiple">
<a href="#" @click="clearRecentImports()">Clear recent imports</a>
<ul>
<li v-for="x in recent_urls" v-bind:key="x">
<a href="#"
@click="website_url=x; loadRecipe(false, undefined)">{{
x
}}</a>
</li>
</ul>
</div>
<div v-if="import_multiple">
<a href="#" @click="website_url_list = []"
v-if="website_url_list.length > 0">Clear import list</a>
<ul>
<li v-for="x in website_url_list" v-bind:key="x">
{{ x }}
</li>
</ul>
<b-button :disabled="website_url_list.length < 1"
@click="autoImport()">Import
</b-button>
</div>
</div>
</div>
</b-card-body>
</b-collapse>
</b-card>
@@ -112,24 +156,6 @@
</b-collapse>
</b-card>
<!-- ADVANCED OPTIONS -->
<b-card no-body>
<b-card-header header-tag="header" class="p-1" role="tab">
<b-button block v-b-toggle.id_accordion_advanced_options variant="info"
:disabled="recipe_json === undefined">Advanced Options
</b-button>
</b-card-header>
<b-collapse id="id_accordion_advanced_options" visible accordion="url_import_accordion"
role="tabpanel" v-model="collapse_visible.advanced_options">
<b-card-body v-if="recipe_json !== undefined">
<import-view-advanced-mapping :recipe="recipe_json" :recipe_tree="recipe_tree" :recipe_images="recipe_images" :recipe_html="recipe_html"
@change="recipe_json = $event"></import-view-advanced-mapping>
</b-card-body>
</b-collapse>
</b-card>
<!-- IMPORT -->
<b-card no-body>
<b-card-header header-tag="header" class="p-1" role="tab">
@@ -184,7 +210,8 @@
:placeholder="$t('paste_json')" style="font-size: 12px">
</b-textarea>
</div>
<b-button @click="loadRecipe()" variant="primary"><i class="fas fa-code"></i>
<b-button @click="loadRecipe(false, undefined)" variant="primary"><i
class="fas fa-code"></i>
{{ $t('Import') }}
</b-button>
@@ -226,7 +253,6 @@ import {ApiApiFactory} from "@/utils/openapi/api";
import draggable from "vuedraggable";
import {INTEGRATIONS} from "@/utils/integration";
import ImportViewStepEditor from "@/apps/ImportView/ImportViewStepEditor";
import ImportViewAdvancedMapping from "@/apps/ImportView/ImportViewAdvancedMapping";
Vue.use(BootstrapVue)
@@ -237,7 +263,6 @@ export default {
ToastMixin,
],
components: {
ImportViewAdvancedMapping,
ImportViewStepEditor,
draggable,
},
@@ -253,6 +278,12 @@ export default {
// URL import
LS_IMPORT_RECENT: 'import_recent_urls', //TODO use central helper to manage all local storage keys (and maybe even access)
website_url: '',
website_url_list: [
'https://madamedessert.de/schokoladenpudding-rezept-mit-echter-schokolade/',
'https://www.essen-und-trinken.de/rezepte/58294-rzpt-schokoladenpudding',
'https://www.chefkoch.de/rezepte/1825781296124455/Schokoladenpudding-selbst-gemacht.html'
],
import_multiple: false,
recent_urls: [],
source_data: '',
recipe_json: undefined,
@@ -274,19 +305,21 @@ export default {
this.tab_index = 0 //TODO add ability to pass open tab via get parameter
if (window.BOOKMARKLET_IMPORT_ID !== -1) {
this.loadRecipe(window.BOOKMARKLET_IMPORT_ID)
this.loadRecipe(false, window.BOOKMARKLET_IMPORT_ID)
}
},
methods: {
/**
* Import recipe based on the data configured by the client
* @param action: action to perform after import (options are: edit, view, import)
* @param data: if parameter is passed ignore global application state and import form data variable
*/
importRecipe: function (action) {
importRecipe: function (action, data) {
let apiFactory = new ApiApiFactory()
apiFactory.createRecipe(this.recipe_json).then(response => { // save recipe
let recipe_json = data !== undefined ? data : this.recipe_json
apiFactory.createRecipe(recipe_json).then(response => { // save recipe
let recipe = response.data
apiFactory.imageRecipe(response.data.id, undefined, this.recipe_json.image).then(response => { // save recipe image
apiFactory.imageRecipe(response.data.id, undefined, recipe_json.image).then(response => { // save recipe image
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
this.afterImportAction(action, recipe)
}).catch(e => {
@@ -303,6 +336,7 @@ export default {
* edit: edit imported recipe
* view: view imported recipe
* import: restart the importer
* nothing: do nothing
* @param recipe: recipe that was imported
*/
afterImportAction: function (action, recipe) {
@@ -316,14 +350,17 @@ export default {
case 'import':
location.reload();
break;
case 'nothing':
break;
}
},
/**
* Requests the recipe to be loaded form the source (url/data) from the server
* Updates all variables to contain what they need to render either simple preview or manual mapping mode
* @param silent do not open the options tab after loading the recipe
* @param bookmarklet id of bookmarklet import to load instead of url, default undefined
*/
loadRecipe: function (bookmarklet) {
console.log(this.website_url)
loadRecipe: function (silent, bookmarklet) {
// keep list of recently imported urls
if (this.website_url !== '') {
if (this.recent_urls.length > 5) {
@@ -351,7 +388,7 @@ export default {
payload['bookmarklet'] = bookmarklet
}
axios.post(resolveDjangoUrl('api_recipe_from_source'), payload,).then((response) => {
return axios.post(resolveDjangoUrl('api_recipe_from_source'), payload,).then((response) => {
this.recipe_json = response.data['recipe_json'];
this.$set(this.recipe_json, 'unused_keywords', this.recipe_json.keywords.filter(k => k.id === undefined))
@@ -361,12 +398,28 @@ export default {
this.recipe_html = response.data['recipe_html'];
this.recipe_images = response.data['recipe_images'] !== undefined ? response.data['recipe_images'] : [];
this.tab_index = 0 // open tab 0 with import wizard
this.collapse_visible.options = true // open options collapse
if (!silent) {
this.tab_index = 0 // open tab 0 with import wizard
this.collapse_visible.options = true // open options collapse
}
return this.recipe_json
}).catch((err) => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH, err.response.data.msg)
})
},
/**
* Function to automatically import multiple urls
* Takes input from website_url_list
*/
autoImport: function () {
this.website_url_list.forEach(r => {
this.website_url = r
this.loadRecipe(true, undefined).then((recipe_json) => {
this.importRecipe('nothing', recipe_json) //TODO handle feedback of what was imported and what not
})
})
this.website_url_list = []
},
/**
* Import recipes with uploaded files and app integration
*/

View File

@@ -1,408 +0,0 @@
<template>
<div>
<!-- recipe preview before Import -->
<div class="container-fluid" v-if="recipe_json" id="manage_tree">
<div class="row">
<div class="col" style="max-width:50%">
<!-- start of preview card -->
<div class="card card-border-primary">
<div class="card-header">
<h3>Preview Recipe Data</h3>
<div class='small text-muted'>Drag recipe attributes from the right into the
appropriate box below.
</div>
</div>
<div class="card-body p-2">
<div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-name>
<div class="row px-3" style="justify-content:space-between;">
Name
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.name=''"
title="Clear Contents"></i>
</div>
<div class="small text-muted">Text dragged here will be appended to the
name.
</div>
</div>
<b-collapse id="collapse-name" visible class="mt-2">
<div class="card-body drop-zone" @drop="replacePreview('name', $event)"
@dragover.prevent @dragenter.prevent>
<div class="card-text">{{ recipe_json.name }}</div>
</div>
</b-collapse>
</div>
<div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-description>
<div class="row px-3" style="justify-content:space-between;">
Description
<i class="fas fa-eraser" style="cursor:pointer;"
@click="recipe_json.description=''" title="Clear Contents"></i>
</div>
<div class="small text-muted">Text dragged here will be appended to the
description.
</div>
</div>
<b-collapse id="collapse-description" visible class="mt-2">
<div class="card-body drop-zone" @drop="replacePreview('description', $event)"
@dragover.prevent @dragenter.prevent>
<div class="card-text">{{ recipe_json.description }}</div>
</div>
</b-collapse>
</div>
<div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-kw>
<div class="row px-3" style="justify-content:space-between;">
Keywords
<i class="fas fa-eraser" style="cursor:pointer;"
@click="recipe_json.keywords=[]" title="Clear Contents"></i>
</div>
<div class="small text-muted">Keywords dragged here will be appended to
current list
</div>
</div>
<b-collapse id="collapse-kw" visible class="mt-2">
<div class="card-body drop-zone" @drop="replacePreview('keywords', $event)"
@dragover.prevent @dragenter.prevent>
<div v-for="kw in recipe_json.keywords" v-bind:key="kw.name">
<div class="card-text">{{ kw.text }}</div>
</div>
</div>
</b-collapse>
</div>
<div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-image
style="display:flex; justify-content:space-between;">
Image
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.image=''"
title="Clear Contents"></i>
</div>
<b-collapse id="collapse-image" visible class="mt-2">
<div class="card-body m-0 p-0 drop-zone" @drop="replacePreview('image', $event)"
@dragover.prevent @dragenter.prevent>
<img class="card-img" v-bind:src="recipe_json.image" alt="Recipe Image">
</div>
</b-collapse>
</div>
<div class="row mb-2">
<div class="col">
<div class="card">
<div class="card-header p-1"
style="display:flex; justify-content:space-between;">
Servings
<i class="fas fa-eraser" style="cursor:pointer;"
@click="recipe_json.servings=''"
title="Clear Contents"></i>
</div>
<div class="card-body p-2 drop-zone" @drop="replacePreview('servings', $event)"
@dragover.prevent @dragenter.prevent>
<div class="card-text">{{ recipe_json.servings }}</div>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header p-1"
style="display:flex; justify-content:space-between;">
Prep Time
<i class="fas fa-eraser" style="cursor:pointer;"
@click="recipe_json.working_time=''"
title="Clear Contents"></i>
</div>
<div class="card-body p-2 drop-zone" @drop="replacePreview('prepTime', $event)"
@dragover.prevent @dragenter.prevent>
<div class="card-text">{{ recipe_json.working_time }}</div>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header p-1"
style="display:flex; justify-content:space-between;">
Cook Time
<i class="fas fa-eraser" style="cursor:pointer;"
@click="recipe_json.waiting_time=''"
title="Clear Contents"></i>
</div>
<div class="card-body p-2 drop-zone" @drop="replacePreview('cookTime', $event)"
@dragover.prevent @dragenter.prevent>
<div class="card-text">{{ recipe_json.waiting_time }}</div>
</div>
</div>
</div>
</div>
<div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-ing>
<div class="row px-3" style="display:flex; justify-content:space-between;">
Ingredients
<i class="fas fa-eraser" style="cursor:pointer;"
@click="recipe_json.recipeIngredient=[]"
title="Clear Contents"></i>
</div>
<div class="small text-muted">Ingredients dragged here will be appended to
current list.
</div>
</div>
<b-collapse id="collapse-ing" visible class="mt-2">
<div class="card-body drop-zone" @drop="replacePreview('ingredients', $event)"
@dragover.prevent @dragenter.prevent>
<ul class="list-group list-group">
<div v-for="i in recipe_json.recipeIngredient" v-bind:key="i.note">
<li class="row border-light">
<div class="col-sm-1 border">{{ i.amount }}</div>
<div class="col-sm border">{{ i.unit.text }}</div>
<div class="col-sm border">{{ i.ingredient.text }}</div>
<div class="col-sm border">{{ i.note }}</div>
</li>
</div>
</ul>
</div>
</b-collapse>
</div>
<div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-instructions>
<div class="row px-3" style="justify-content:space-between;">
Instructions
<i class="fas fa-eraser" style="cursor:pointer;"
@click="recipe_json.recipeInstructions=''"
title="Clear Contents"></i>
</div>
<div class="small text-muted">Recipe instructions dragged here will be
appended to current instructions.
</div>
</div>
<b-collapse id="collapse-instructions" visible class="mt-2">
<div class="card-body drop-zone" @drop="replacePreview('instructions', $event)"
@dragover.prevent @dragenter.prevent>
<div class="card-text">{{ recipe_json.recipeInstructions }}</div>
</div>
</b-collapse>
</div>
</div>
</div>
<br/>
<!-- end of preview card -->
<button class="btn btn-primary shadow-none" type="button"
style="margin-bottom: 2vh"
id="id_btn_json"><i class="fas fa-code"></i> Import
</button>
</div>
<!-- start of source data -->
<div class="col" style="max-width:50%;">
<div class="card card-border-primary sticky-top" style="z-index: 100;">
<div class="card-header">
<h3>Discovered Attributes</h3>
<div class='small text-muted'>
Drag recipe attributes from below into the appropriate box on the left. Click
any node to display its full properties.
</div>
</div>
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-outline-info btn-sm active" @click="preview_type='json'">
<input type="radio" autocomplete="off" checked> json
</label>
<label class="btn btn-outline-info btn-sm" @click="preview_type='html'">
<input type="radio" autocomplete="off"> html
</label>
</div>
<i :class="[show_blank ? 'fa-chevron-up' : 'fa-chevron-down', 'fas']"
style="cursor:pointer;"
@click="show_blank=!show_blank"
title="Show Blank Field"></i>
<div class="card-body p-1">
<div class="card card-border-primary" v-if="show_blank">
<div class="card-header">
<div class="row px-3" style="justify-content:space-between;">
Blank Field
<i class="fas fa-eraser justify-content-end" style="cursor:pointer;"
@click="blank_field=''" title="Clear Contents"></i>
</div>
<div class="small text-muted">Items dragged to Blank Field will be
appended.
</div>
</div>
<div class="card-body drop-zone"
@drop="replacePreview('blank', $event)"
@dragover.prevent
@dragenter.prevent
draggable
@dragstart="htmlDragStart($event)">
{{ blank_field }}
</div>
</div>
<!-- start of json data -->
<!-- eslint-disable vue/no-deprecated-scope-attribute -->
<v-jstree v-if="preview_type=='json'" :data="recipe_tree"
text-field-name="name"
collapse:true
draggable
@item-drag-start="itemDragStart"
@item-click="itemClick">
<template scope="_">
<div class="col" @click.ctrl="customItemClickWithCtrl">
<div class="row clearfix" style="width:100%">
<div class="col-es" style="align-right">
<button
style="border: 0px; background-color: transparent; cursor: pointer;"
@click="deleteNode(_.vm, _.model, $event)"><i
class="fas fa-minus-square" style="color:red"></i></button>
</div>
<div class="col overflow-hidden">
<i :class="_.vm.themeIconClasses" role="presentation"
v-if="!_.model.loading"></i>
{{ _.model.name }}
</div>
</div>
</div>
</template>
</v-jstree>
<!-- eslint-disable vue/no-deprecated-scope-attribute -->
<!-- start of html data -->
<div v-if="preview_type=='html'">
<ul class="list-group list-group-flush" v-for="(txt, key) in recipe_html"
v-bind:key="key">
<div class="list-group-item bg-light m-0 small"
draggable
@dragstart="htmlDragStart($event)"
style="display:flex; justify-content:space-between;">
{{ txt }}
<i class="fas fa-minus-square" style="cursor:pointer; color:red"
@click="$delete(recipe_html, key)" title="Delete Text"></i>
</div>
</ul>
</div>
</div>
</div>
</div>
<!-- end of json tree -->
</div>
</div>
<!-- end of recipe preview before Import -->
</div>
</template>
<script>
import {StandardToasts} from "@/utils/utils";
import VJstree from 'vue-jstree'
export default {
name: "ImportViewAdvancedMapping",
components: {
VJstree,
},
props: {
recipe: undefined,
recipe_html: undefined,
recipe_tree: undefined,
recipe_images: undefined,
},
data() {
return {
recipe_json: undefined,
preview_type: 'json',
show_blank: false,
blank_field: '',
}
},
watch: {
recipe_json: function () {
this.$emit('change', this.recipe_json)
},
},
mounted() {
this.recipe_json = this.recipe //TODO check if changed not only if mounted, same for step editor
},
methods: {
deleteNode: function (node, item, e) {
e.stopPropagation()
var index = node.parentItem.indexOf(item)
node.parentItem.splice(index, 1)
},
itemClick: function (node, item, e) {
this.makeToast('Details', node.model.value, 'info')
},
itemDragStart(node, item, e) {
if (node.model.children.length > 0) {
e.dataTransfer.setData('hasChildren', true)
}
e.dataTransfer.setData('value', node.model.value)
},
htmlDragStart: function (e) {
e.dataTransfer.setData('value', e.target.innerText)
},
imageDragStart: function (e) {
e.dataTransfer.setData('value', e.target.src)
},
replacePreview: function (field, e) {
let v = e.dataTransfer.getData('value')
if (e.dataTransfer.getData('hasChildren')) {
this.makeToast('Error', 'Items with children cannot be dropped here!', 'danger')
return
}
switch (field) {
case 'name':
this.recipe_json.name = [this.recipe_json.name, v].filter(Boolean).join(" ");
break;
case 'description':
this.recipe_json.description = [this.recipe_json.description, v].filter(Boolean).join(" ");
break;
case 'image':
this.recipe_json.image = v
break;
case 'keywords':
this.recipe_json.keywords.push({'text': v, 'id': null})
break;
case 'servings':
this.recipe_json.servings = parseInt(v.match(/\b\d+\b/)[0])
break;
case 'prepTime':
this.recipe_json.prepTime = v
break;
case 'cookTime':
this.recipe_json.cookTime = v
break;
case 'ingredients':
this.$http.post('string_from_ingredients', {text: v}, {emulateJSON: true}
).then((response) => {
let new_ingredient = {
unit: {id: Math.random() * 1000, text: response.body.unit},
amount: String(response.body.amount),
ingredient: {id: Math.random() * 1000, text: response.body.food},
note: response.body.note,
original_text: v
}
this.recipe_json.recipeIngredient.push(new_ingredient)
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
})
break;
case 'instructions':
this.recipe_json.recipeInstructions = [this.recipe_json.recipeInstructions, v].filter(Boolean).join("\n\n");
break;
case 'blank':
this.blank_field = [this.blank_field, v].filter(Boolean).join(" ");
break;
}
},
}
}
</script>
<style scoped>
</style>