moved keyword Vue to generic components
This commit is contained in:
parent
638dd96812
commit
f12558951a
@ -1 +0,0 @@
|
|||||||
.shake[data-v-d394ab04]{-webkit-animation:shake-data-v-d394ab04 .82s cubic-bezier(.36,.07,.19,.97) both;animation:shake-data-v-d394ab04 .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden;perspective:1000px}@-webkit-keyframes shake-data-v-d394ab04{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake-data-v-d394ab04{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Vue App</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="css/keyword_list_view.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/keyword_list_view.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="img/icons/favicon-16x16.png"><link rel="manifest" href="manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Recipes"><link rel="apple-touch-icon" href="img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><div id="app"></div><script src="js/chunk-vendors.js"></script></body></html>
|
|
@ -1,31 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% load render_bundle from webpack_loader %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load l10n %}
|
|
||||||
{% comment %} TODO Can this be combined with Food template? {% endcomment %}
|
|
||||||
{% block title %}{% trans 'Keywords' %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content_fluid %}
|
|
||||||
|
|
||||||
<div id="app" >
|
|
||||||
<keyword-list-view></keyword-list-view>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
{% if debug %}
|
|
||||||
<script src="{% url 'js_reverse' %}"></script>
|
|
||||||
{% else %}
|
|
||||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script type="application/javascript">
|
|
||||||
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% render_bundle 'keyword_list_view' %}
|
|
||||||
{% endblock %}
|
|
File diff suppressed because one or more lines are too long
@ -105,7 +105,17 @@ def invite_link(request):
|
|||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def keyword(request):
|
def keyword(request):
|
||||||
return render(request, 'model/keyword_template.html', {"title": _("Keywords")})
|
return render(
|
||||||
|
request,
|
||||||
|
'generic/model_template.html',
|
||||||
|
{
|
||||||
|
"title": _("Keywords"),
|
||||||
|
"config": {
|
||||||
|
'model': "KEYWORD",
|
||||||
|
'recipe_param': 'keywords'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
|
13
vue/package-lock.json
generated
13
vue/package-lock.json
generated
@ -44,7 +44,7 @@
|
|||||||
"eslint-plugin-vue": "^7.10.0",
|
"eslint-plugin-vue": "^7.10.0",
|
||||||
"typescript": "~4.3.2",
|
"typescript": "~4.3.2",
|
||||||
"vue-cli-plugin-i18n": "^2.1.1",
|
"vue-cli-plugin-i18n": "^2.1.1",
|
||||||
"webpack-bundle-tracker": "1.1.0",
|
"webpack-bundle-tracker": "1.3.0",
|
||||||
"workbox-expiration": "^6.0.2",
|
"workbox-expiration": "^6.0.2",
|
||||||
"workbox-navigation-preload": "^6.0.2",
|
"workbox-navigation-preload": "^6.0.2",
|
||||||
"workbox-precaching": "^6.0.2",
|
"workbox-precaching": "^6.0.2",
|
||||||
@ -14289,15 +14289,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-bundle-tracker": {
|
"node_modules/webpack-bundle-tracker": {
|
||||||
"version": "1.1.0",
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-cs3QMgW5F0mE0e91X/SuEq2MUfu2LqpjFSdfINsOmNt/T4v39EESNhJ+P8d5lmbfFL6Z1z7n6LlpVCERTdDDvQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash.assign": "^4.2.0",
|
"lodash.assign": "^4.2.0",
|
||||||
"lodash.defaults": "^4.2.0",
|
"lodash.defaults": "^4.2.0",
|
||||||
"lodash.foreach": "^4.5.0",
|
"lodash.foreach": "^4.5.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.merge": "^4.6.2",
|
|
||||||
"strip-ansi": "^6.0.0"
|
"strip-ansi": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -24887,14 +24887,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"webpack-bundle-tracker": {
|
"webpack-bundle-tracker": {
|
||||||
"version": "1.1.0",
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webpack-bundle-tracker/-/webpack-bundle-tracker-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-cs3QMgW5F0mE0e91X/SuEq2MUfu2LqpjFSdfINsOmNt/T4v39EESNhJ+P8d5lmbfFL6Z1z7n6LlpVCERTdDDvQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"lodash.assign": "^4.2.0",
|
"lodash.assign": "^4.2.0",
|
||||||
"lodash.defaults": "^4.2.0",
|
"lodash.defaults": "^4.2.0",
|
||||||
"lodash.foreach": "^4.5.0",
|
"lodash.foreach": "^4.5.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.merge": "^4.6.2",
|
|
||||||
"strip-ansi": "^6.0.0"
|
"strip-ansi": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,605 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="app" style="margin-bottom: 4vh">
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-2 d-none d-md-block">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-8 col-12">
|
|
||||||
<!-- TODO only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different component? -->
|
|
||||||
<div class="container-fluid d-flex flex-column flex-grow-1" :class="{'vh-100' : show_split}">
|
|
||||||
<!-- expanded options box -->
|
|
||||||
<div class="row flex-shrink-0">
|
|
||||||
<div class="col col-md-12">
|
|
||||||
<b-collapse id="collapse_advanced" class="mt-2" v-model="advanced_visible">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-3" style="margin-top: 1vh">
|
|
||||||
<div class="btn btn-primary btn-block text-uppercase" @click="startAction({'action':'new'})">
|
|
||||||
{{ this.$t('New_Keyword') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3" style="margin-top: 1vh">
|
|
||||||
<button class="btn btn-primary btn-block text-uppercase" @click="resetSearch">
|
|
||||||
{{ this.$t('Reset_Search') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3" style="position: relative; margin-top: 1vh">
|
|
||||||
<b-form-checkbox v-model="show_split" name="check-button"
|
|
||||||
class="shadow-none"
|
|
||||||
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
|
|
||||||
{{ this.$t('show_split_screen') }}
|
|
||||||
</b-form-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</b-collapse>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row flex-shrink-0">
|
|
||||||
<!-- search box -->
|
|
||||||
<div class="col col-md">
|
|
||||||
<b-input-group class="mt-3">
|
|
||||||
<b-input class="form-control" v-model="search_input"
|
|
||||||
v-bind:placeholder="this.$t('Search')"></b-input>
|
|
||||||
<b-input-group-append>
|
|
||||||
<b-button v-b-toggle.collapse_advanced variant="primary" class="shadow-none">
|
|
||||||
<i class="fas fa-caret-down" v-if="!advanced_visible"></i>
|
|
||||||
<i class="fas fa-caret-up" v-if="advanced_visible"></i>
|
|
||||||
</b-button>
|
|
||||||
</b-input-group-append>
|
|
||||||
</b-input-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- split side search -->
|
|
||||||
<div class="col col-md" v-if="show_split">
|
|
||||||
<b-input-group class="mt-3">
|
|
||||||
<b-input class="form-control" v-model="search_input2"
|
|
||||||
v-bind:placeholder="this.$t('Search')"></b-input>
|
|
||||||
</b-input-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- only show scollbars in split mode, but this doesn't interact well with infinite scroll, maybe a different componenet? -->
|
|
||||||
<div class="row" :class="{'overflow-hidden' : show_split}" style="margin-top: 2vh">
|
|
||||||
<div class="col col-md" :class="{'mh-100 overflow-auto' : show_split}">
|
|
||||||
<generic-horizontal-card v-for="kw in keywords" v-bind:key="kw.id"
|
|
||||||
:item=kw
|
|
||||||
item_type="Keyword"
|
|
||||||
:draggable="true"
|
|
||||||
:merge="true"
|
|
||||||
:move="true"
|
|
||||||
@item-action="startAction($event, 'left')"
|
|
||||||
/>
|
|
||||||
<infinite-loading
|
|
||||||
:identifier='left'
|
|
||||||
@infinite="infiniteHandler($event, 'left')"
|
|
||||||
spinner="waveDots">
|
|
||||||
<template v-slot:no-more><span/></template>
|
|
||||||
</infinite-loading>
|
|
||||||
</div>
|
|
||||||
<!-- right side keyword cards -->
|
|
||||||
<div class="col col-md mh-100 overflow-auto " v-if="show_split">
|
|
||||||
<generic-horizontal-card v-for="kw in keywords2" v-bind:key="kw.id"
|
|
||||||
:item=kw
|
|
||||||
item_type="Keyword"
|
|
||||||
:draggable="true"
|
|
||||||
:merge="true"
|
|
||||||
:move="true"
|
|
||||||
@item-action="startAction($event, 'right')"
|
|
||||||
/>
|
|
||||||
<infinite-loading
|
|
||||||
:identifier='right'
|
|
||||||
@infinite="infiniteHandler($event, 'right')"
|
|
||||||
spinner="waveDots">
|
|
||||||
<template v-slot:no-more><span/></template>
|
|
||||||
</infinite-loading>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 d-none d-md-block">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TODO Modals can probably be made generic and moved to component -->
|
|
||||||
<!-- edit modal -->
|
|
||||||
<b-modal class="modal"
|
|
||||||
:id="'id_modal_keyword_edit'"
|
|
||||||
@shown="prepareEmoji"
|
|
||||||
:title="this.$t('Edit_Keyword')"
|
|
||||||
:ok-title="this.$t('Save')"
|
|
||||||
:cancel-title="this.$t('Cancel')"
|
|
||||||
@ok="saveKeyword">
|
|
||||||
<form>
|
|
||||||
<label for="id_keyword_name_edit">{{ this.$t('Name') }}</label>
|
|
||||||
<input class="form-control" type="text" id="id_keyword_name_edit" v-model="this_item.name">
|
|
||||||
<label for="id_keyword_description_edit">{{ this.$t('Description') }}</label>
|
|
||||||
<input class="form-control" type="text" id="id_keyword_description_edit" v-model="this_item.description">
|
|
||||||
<label for="id_keyword_icon_edit">{{ this.$t('Icon') }}</label>
|
|
||||||
<twemoji-textarea
|
|
||||||
id="id_keyword_icon_edit"
|
|
||||||
ref="_edit"
|
|
||||||
:emojiData="emojiDataAll"
|
|
||||||
:emojiGroups="emojiGroups"
|
|
||||||
triggerType="hover"
|
|
||||||
recentEmojisFeat="true"
|
|
||||||
recentEmojisStorage="local"
|
|
||||||
@contentChanged="setIcon"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</b-modal>
|
|
||||||
<!-- delete modal -->
|
|
||||||
<b-modal class="modal"
|
|
||||||
:id="'id_modal_keyword_delete'"
|
|
||||||
:title="this.$t('Delete_Keyword')"
|
|
||||||
:ok-title="this.$t('Delete')"
|
|
||||||
:cancel-title="this.$t('Cancel')"
|
|
||||||
@ok="delKeyword(this_item.id)">
|
|
||||||
{{this.$t("delete_confimation", {'kw': this_item.name})}} {{this_item.name}}
|
|
||||||
</b-modal>
|
|
||||||
<!-- move modal -->
|
|
||||||
<b-modal class="modal" v-if="models"
|
|
||||||
:id="'id_modal_keyword_move'"
|
|
||||||
:title="this.$t('Move_Keyword')"
|
|
||||||
:ok-title="this.$t('Move')"
|
|
||||||
:cancel-title="this.$t('Cancel')"
|
|
||||||
@ok="moveKeyword(this_item.id, this_item.target.id)">
|
|
||||||
{{ this.$t("move_selection", {'child': this_item.name}) }}
|
|
||||||
<generic-multiselect
|
|
||||||
@change="this_item.target=$event.val"
|
|
||||||
label="name"
|
|
||||||
:model="models.KEYWORD"
|
|
||||||
:multiple="false"
|
|
||||||
:sticky_options="[{'id': 0,'name': $t('Root')}]"
|
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
|
||||||
:placeholder="this.$t('Search')">
|
|
||||||
</generic-multiselect>
|
|
||||||
</b-modal>
|
|
||||||
<!-- merge modal -->
|
|
||||||
<b-modal class="modal" v-if="models"
|
|
||||||
:id="'id_modal_keyword_merge'"
|
|
||||||
:title="this.$t('Merge_Keyword')"
|
|
||||||
:ok-title="this.$t('Merge')"
|
|
||||||
:cancel-title="this.$t('Cancel')"
|
|
||||||
@ok="mergeKeyword(this_item.id, this_item.target.id)">
|
|
||||||
{{ this.$t("merge_selection", {'source': this_item.name, 'type': this.$t('keyword')}) }}
|
|
||||||
<generic-multiselect
|
|
||||||
@change="this_item.target=$event.val"
|
|
||||||
:model="models.KEYWORD"
|
|
||||||
:multiple="false"
|
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
|
||||||
:placeholder="this.$t('Search')">
|
|
||||||
</generic-multiselect>
|
|
||||||
</b-modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
|
||||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
|
||||||
|
|
||||||
import Vue from 'vue'
|
|
||||||
import {BootstrapVue} from 'bootstrap-vue'
|
|
||||||
|
|
||||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
|
||||||
import _debounce from 'lodash/debounce'
|
|
||||||
|
|
||||||
import {ToastMixin} from "@/utils/utils";
|
|
||||||
|
|
||||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
|
||||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard";
|
|
||||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
|
||||||
import InfiniteLoading from 'vue-infinite-loading';
|
|
||||||
|
|
||||||
// would move with modals if made generic
|
|
||||||
import {TwemojiTextarea} from '@kevinfaguiar/vue-twemoji-picker';
|
|
||||||
// TODO add localization
|
|
||||||
import EmojiAllData from '@kevinfaguiar/vue-twemoji-picker/emoji-data/en/emoji-all-groups.json';
|
|
||||||
import EmojiGroups from '@kevinfaguiar/vue-twemoji-picker/emoji-data/emoji-groups.json';
|
|
||||||
// end move with generic modals
|
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
|
||||||
import {Models} from "@/utils/models";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'KeywordListView',
|
|
||||||
mixins: [ToastMixin],
|
|
||||||
components: {TwemojiTextarea, GenericHorizontalCard, GenericMultiselect, InfiniteLoading},
|
|
||||||
computed: {
|
|
||||||
// move with generic modals
|
|
||||||
emojiDataAll() {
|
|
||||||
return EmojiAllData;
|
|
||||||
},
|
|
||||||
emojiGroups() {
|
|
||||||
return EmojiGroups;
|
|
||||||
}
|
|
||||||
// end move with generic modals
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
keywords: [],
|
|
||||||
keywords2: [],
|
|
||||||
models: Models,
|
|
||||||
show_split: false,
|
|
||||||
search_input: '',
|
|
||||||
search_input2: '',
|
|
||||||
advanced_visible: false,
|
|
||||||
right_page: 0,
|
|
||||||
right: +new Date(),
|
|
||||||
isDirtyRight: false,
|
|
||||||
left_page: 0,
|
|
||||||
left: +new Date(),
|
|
||||||
isDirtyLeft: false,
|
|
||||||
this_item: {
|
|
||||||
'id': -1,
|
|
||||||
'name': '',
|
|
||||||
'description': '',
|
|
||||||
'icon': '',
|
|
||||||
'target': {
|
|
||||||
'id': -1,
|
|
||||||
'name': ''
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search_input: _debounce(function() {
|
|
||||||
this.left_page = 0
|
|
||||||
this.keywords = []
|
|
||||||
this.left += 1
|
|
||||||
}, 700),
|
|
||||||
search_input2: _debounce(function() {
|
|
||||||
this.right_page = 0
|
|
||||||
this.keywords2 = []
|
|
||||||
this.right += 1
|
|
||||||
}, 700)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
resetSearch: function () {
|
|
||||||
if (this.search_input !== '') {
|
|
||||||
this.search_input = ''
|
|
||||||
} else {
|
|
||||||
this.left_page = 0
|
|
||||||
this.keywords = []
|
|
||||||
this.left += 1
|
|
||||||
}
|
|
||||||
if (this.search_input2 !== '') {
|
|
||||||
this.search_input2 = ''
|
|
||||||
} else {
|
|
||||||
this.right_page = 0
|
|
||||||
this.keywords2 = []
|
|
||||||
this.right += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
// TODO should model actions be included with the context menu? the card? a seperate mixin avaible to all?
|
|
||||||
startAction: function(e, col) {
|
|
||||||
let target = e.target || null
|
|
||||||
let source = e.source || null
|
|
||||||
|
|
||||||
if (e.action == 'delete') {
|
|
||||||
this.this_item = source
|
|
||||||
this.$bvModal.show('id_modal_keyword_delete')
|
|
||||||
} else if (e.action == 'new') {
|
|
||||||
this.this_item = {}
|
|
||||||
this.$bvModal.show('id_modal_keyword_edit')
|
|
||||||
} else if (e.action == 'edit') {
|
|
||||||
this.this_item = source
|
|
||||||
this.$bvModal.show('id_modal_keyword_edit')
|
|
||||||
} else if (e.action === 'move') {
|
|
||||||
this.this_item = source
|
|
||||||
if (target == null) {
|
|
||||||
this.$bvModal.show('id_modal_keyword_move')
|
|
||||||
} else {
|
|
||||||
this.moveKeyword(source.id, target.id)
|
|
||||||
}
|
|
||||||
} else if (e.action === 'merge') {
|
|
||||||
this.this_item = source
|
|
||||||
if (target == null) {
|
|
||||||
this.$bvModal.show('id_modal_keyword_merge')
|
|
||||||
} else {
|
|
||||||
this.mergeKeyword(e.source.id, e.target.id)
|
|
||||||
}
|
|
||||||
} else if (e.action === 'get-children') {
|
|
||||||
if (source.show_children) {
|
|
||||||
Vue.set(source, 'show_children', false)
|
|
||||||
} else {
|
|
||||||
this.this_item = source
|
|
||||||
this.getChildren(col, source)
|
|
||||||
}
|
|
||||||
} else if (e.action === 'get-recipes') {
|
|
||||||
if (source.show_recipes) {
|
|
||||||
Vue.set(source, 'show_recipes', false)
|
|
||||||
} else {
|
|
||||||
this.this_item = source
|
|
||||||
this.getRecipes(col, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
saveKeyword: function () {
|
|
||||||
let apiClient = new ApiApiFactory()
|
|
||||||
let kw = {
|
|
||||||
name: this.this_item.name,
|
|
||||||
description: this.this_item.description,
|
|
||||||
icon: this.this_item.icon,
|
|
||||||
}
|
|
||||||
if (!this.this_item.id) { // if there is no item id assume its a new item
|
|
||||||
apiClient.createKeyword(kw).then(result => {
|
|
||||||
// place all new keywords at the top of the list - could sort instead
|
|
||||||
this.keywords = [result.data].concat(this.keywords)
|
|
||||||
// this creates a deep copy to make sure that columns stay independent
|
|
||||||
if (this.show_split){
|
|
||||||
this.keywords2 = [JSON.parse(JSON.stringify(result.data))].concat(this.keywords2)
|
|
||||||
} else {
|
|
||||||
this.keywords2 = []
|
|
||||||
}
|
|
||||||
this.this_item={}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.this_item = {}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
apiClient.partialUpdateKeyword(this.this_item.id, kw).then(result => {
|
|
||||||
this.refreshCard(this.this_item.id)
|
|
||||||
this.this_item={}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.this_item = {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
delKeyword: function (id) {
|
|
||||||
let apiClient = new ApiApiFactory()
|
|
||||||
|
|
||||||
apiClient.destroyKeyword(id).then(response => {
|
|
||||||
this.destroyCard(id)
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.this_item = {}
|
|
||||||
})
|
|
||||||
|
|
||||||
},
|
|
||||||
moveKeyword: function (source_id, target_id) {
|
|
||||||
let apiClient = new ApiApiFactory()
|
|
||||||
apiClient.moveKeyword(String(source_id), String(target_id)).then(result => {
|
|
||||||
if (target_id === 0) {
|
|
||||||
let kw = this.findKeyword(this.keywords, source_id) || this.findKeyword(this.keywords2, source_id)
|
|
||||||
kw.parent = null
|
|
||||||
|
|
||||||
if (this.show_split){
|
|
||||||
this.destroyCard(source_id) // order matters, destroy old card before adding it back in at root
|
|
||||||
|
|
||||||
this.keywords = [kw].concat(this.keywords)
|
|
||||||
this.keywords2 = [JSON.parse(JSON.stringify(kw))].concat(this.keywords2)
|
|
||||||
} else {
|
|
||||||
this.destroyCard(source_id)
|
|
||||||
this.keywords = [kw].concat(this.keywords)
|
|
||||||
this.keywords2 = []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.destroyCard(source_id)
|
|
||||||
this.refreshCard(target_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
}).catch((err) => {
|
|
||||||
// TODO none of the error checking works because the openapi generated functions don't throw an error?
|
|
||||||
// or i'm capturing it incorrectly
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
mergeKeyword: function (source_id, target_id) {
|
|
||||||
let apiClient = new ApiApiFactory()
|
|
||||||
apiClient.mergeKeyword(String(source_id), String(target_id)).then(result => {
|
|
||||||
this.destroyCard(source_id)
|
|
||||||
this.refreshCard(target_id)
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log('Error', err)
|
|
||||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// TODO: DRY the listKeyword functions (refresh, get children, infinityHandler ) can probably all be consolidated into a single function
|
|
||||||
getChildren: function(col, kw){
|
|
||||||
let apiClient = new ApiApiFactory()
|
|
||||||
let parent = {}
|
|
||||||
let query = undefined
|
|
||||||
let page = undefined
|
|
||||||
let root = kw.id
|
|
||||||
let tree = undefined
|
|
||||||
let pageSize = 200
|
|
||||||
|
|
||||||
apiClient.listKeywords(query, root, tree, page, pageSize).then(result => {
|
|
||||||
if (col == 'left') {
|
|
||||||
parent = this.findKeyword(this.keywords, kw.id)
|
|
||||||
} else if (col == 'right'){
|
|
||||||
parent = this.findKeyword(this.keywords2, kw.id)
|
|
||||||
}
|
|
||||||
if (parent) {
|
|
||||||
Vue.set(parent, 'children', result.data.results)
|
|
||||||
Vue.set(parent, 'show_children', true)
|
|
||||||
Vue.set(parent, 'show_recipes', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getRecipes: function(col, kw){
|
|
||||||
let apiClient = new ApiApiFactory()
|
|
||||||
let parent = {}
|
|
||||||
let pageSize = 200
|
|
||||||
let keyword = String(kw.id)
|
|
||||||
|
|
||||||
apiClient.listRecipes(
|
|
||||||
undefined, keyword, undefined, undefined, undefined, undefined,
|
|
||||||
undefined, undefined, undefined, undefined, undefined, pageSize, undefined
|
|
||||||
).then(result => {
|
|
||||||
if (col == 'left') {
|
|
||||||
parent = this.findKeyword(this.keywords, kw.id)
|
|
||||||
} else if (col == 'right'){
|
|
||||||
parent = this.findKeyword(this.keywords2, kw.id)
|
|
||||||
}
|
|
||||||
if (parent) {
|
|
||||||
Vue.set(parent, 'recipes', result.data.results)
|
|
||||||
Vue.set(parent, 'show_recipes', true)
|
|
||||||
Vue.set(parent, 'show_children', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
refreshCard: function(id){
|
|
||||||
let target = {}
|
|
||||||
let apiClient = new ApiApiFactory()
|
|
||||||
let idx = undefined
|
|
||||||
let idx2 = undefined
|
|
||||||
apiClient.retrieveKeyword(id).then(result => {
|
|
||||||
target = this.findKeyword(this.keywords, id) || this.findKeyword(this.keywords2, id)
|
|
||||||
|
|
||||||
if (target.parent) {
|
|
||||||
let parent = this.findKeyword(this.keywords, target.parent)
|
|
||||||
let parent2 = this.findKeyword(this.keywords2, target.parent)
|
|
||||||
|
|
||||||
if (parent) {
|
|
||||||
if (parent.show_children){
|
|
||||||
idx = parent.children.indexOf(parent.children.find(kw => kw.id === target.id))
|
|
||||||
Vue.set(parent.children, idx, result.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parent2){
|
|
||||||
if (parent2.show_children){
|
|
||||||
idx2 = parent2.children.indexOf(parent2.children.find(kw => kw.id === target.id))
|
|
||||||
// deep copy to force columns to be indepedent
|
|
||||||
Vue.set(parent2.children, idx2, JSON.parse(JSON.stringify(result.data)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
idx = this.keywords.indexOf(this.keywords.find(kw => kw.id === target.id))
|
|
||||||
idx2 = this.keywords2.indexOf(this.keywords2.find(kw => kw.id === target.id))
|
|
||||||
Vue.set(this.keywords, idx, result.data)
|
|
||||||
Vue.set(this.keywords2, idx2, JSON.parse(JSON.stringify(result.data)))
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
},
|
|
||||||
findKeyword: function(card_list, id){
|
|
||||||
let card_length = card_list?.length ?? 0
|
|
||||||
if (card_length == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let cards = card_list.filter(obj => obj.id == id)
|
|
||||||
if (cards.length == 1) {
|
|
||||||
return cards[0]
|
|
||||||
} else if (cards.length == 0) {
|
|
||||||
for (const c of card_list.filter(x => x.show_children == true)) {
|
|
||||||
cards = this.findKeyword(c.children, id)
|
|
||||||
if (cards) {
|
|
||||||
return cards
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('something terrible happened')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// this would move with modals with mixin?
|
|
||||||
prepareEmoji: function() {
|
|
||||||
this.$refs._edit.addText(this.this_item.icon || '');
|
|
||||||
this.$refs._edit.blur()
|
|
||||||
document.getElementById('btn-emoji-default').disabled = true;
|
|
||||||
},
|
|
||||||
// this would move with modals with mixin?
|
|
||||||
setIcon: function(icon) {
|
|
||||||
this.this_item.icon = icon
|
|
||||||
},
|
|
||||||
infiniteHandler: function($state, col) {
|
|
||||||
let apiClient = new ApiApiFactory()
|
|
||||||
let query = (col==='left') ? this.search_input : this.search_input2
|
|
||||||
let page = (col==='left') ? this.left_page + 1 : this.right_page + 1
|
|
||||||
let root = undefined
|
|
||||||
let tree = undefined
|
|
||||||
let pageSize = undefined
|
|
||||||
|
|
||||||
if (query === '') {
|
|
||||||
query = undefined
|
|
||||||
root = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
apiClient.listKeywords(query, root, tree, page, pageSize).then(result => {
|
|
||||||
if (result.data.results.length){
|
|
||||||
if (col ==='left') {
|
|
||||||
this.left_page+=1
|
|
||||||
this.keywords = this.keywords.concat(result.data.results)
|
|
||||||
$state.loaded();
|
|
||||||
if (this.keywords.length >= result.data.count) {
|
|
||||||
$state.complete();
|
|
||||||
}
|
|
||||||
} else if (col ==='right') {
|
|
||||||
this.right_page+=1
|
|
||||||
this.keywords2 = this.keywords2.concat(result.data.results)
|
|
||||||
$state.loaded();
|
|
||||||
if (this.keywords2.length >= result.data.count) {
|
|
||||||
$state.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('no data returned')
|
|
||||||
$state.complete();
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
|
||||||
$state.complete();
|
|
||||||
})
|
|
||||||
},
|
|
||||||
destroyCard: function(id) {
|
|
||||||
let kw = this.findKeyword(this.keywords, id)
|
|
||||||
let kw2 = this.findKeyword(this.keywords2, id)
|
|
||||||
let p_id = undefined
|
|
||||||
p_id = kw?.parent ?? kw2.parent
|
|
||||||
|
|
||||||
if (p_id) {
|
|
||||||
let parent = this.findKeyword(this.keywords, p_id)
|
|
||||||
let parent2 = this.findKeyword(this.keywords2, p_id)
|
|
||||||
if (parent){
|
|
||||||
Vue.set(parent, 'numchild', parent.numchild - 1)
|
|
||||||
if (parent.show_children) {
|
|
||||||
let idx = parent.children.indexOf(parent.children.find(kw => kw.id === id))
|
|
||||||
Vue.delete(parent.children, idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parent2){
|
|
||||||
Vue.set(parent2, 'numchild', parent2.numchild - 1)
|
|
||||||
if (parent2.show_children) {
|
|
||||||
let idx = parent2.children.indexOf(parent2.children.find(kw => kw.id === id))
|
|
||||||
Vue.delete(parent2.children, idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.keywords = this.keywords.filter(kw => kw.id != id)
|
|
||||||
this.keywords2 = this.keywords2.filter(kw => kw.id != id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,10 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import App from './KeywordListView'
|
|
||||||
import i18n from '@/i18n'
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
i18n,
|
|
||||||
render: h => h(App),
|
|
||||||
}).$mount('#app')
|
|
@ -28,6 +28,11 @@
|
|||||||
<template v-slot:upper-right>
|
<template v-slot:upper-right>
|
||||||
<b-button v-if="i.recipe" v-b-tooltip.hover :title="i.recipe.name"
|
<b-button v-if="i.recipe" v-b-tooltip.hover :title="i.recipe.name"
|
||||||
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="i.recipe.url"/>
|
class=" btn fas fa-book-open p-0 border-0" variant="link" :href="i.recipe.url"/>
|
||||||
|
<!-- keywords can have icons - if it exists, display it -->
|
||||||
|
<b-button v-if="i.icon"
|
||||||
|
class=" btn p-0 border-0" variant="link">
|
||||||
|
{{i.icon}}
|
||||||
|
</b-button>
|
||||||
</template>
|
</template>
|
||||||
</generic-horizontal-card>
|
</generic-horizontal-card>
|
||||||
</template>
|
</template>
|
||||||
@ -98,11 +103,8 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
// this.genericAPI inherited from ApiMixin
|
// this.genericAPI inherited from ApiMixin
|
||||||
resetList: function (e) {
|
resetList: function (e) {
|
||||||
if (e.column === 'left') {
|
this.items_right = []
|
||||||
this.items_left = []
|
this.items_left = []
|
||||||
} else if (e.column === 'right') {
|
|
||||||
this.items_right = []
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
startAction: function (e, param) {
|
startAction: function (e, param) {
|
||||||
let source = e?.source ?? {}
|
let source = e?.source ?? {}
|
||||||
@ -205,10 +207,11 @@ export default {
|
|||||||
saveThis: function (thisItem) {
|
saveThis: function (thisItem) {
|
||||||
if (!thisItem?.id) { // if there is no item id assume it's a new item
|
if (!thisItem?.id) { // if there is no item id assume it's a new item
|
||||||
this.genericAPI(this.this_model, this.Actions.CREATE, thisItem).then((result) => {
|
this.genericAPI(this.this_model, this.Actions.CREATE, thisItem).then((result) => {
|
||||||
// place all new items at the top of the list - could sort instead
|
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
|
||||||
this.items_left = [result.data].concat(this.items_left)
|
// then place all new items at the top of the list - could sort instead
|
||||||
|
this.items_left = [result.data].concat(this.destroyCard(result?.data?.id, this.items_left))
|
||||||
// this creates a deep copy to make sure that columns stay independent
|
// this creates a deep copy to make sure that columns stay independent
|
||||||
this.items_right = [{...result.data}].concat(this.items_right)
|
this.items_right = [{...result.data}].concat(this.destroyCard(result?.data?.id, this.items_right))
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
@ -230,14 +233,15 @@ export default {
|
|||||||
this.clearState()
|
this.clearState()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (source_id === undefined || target_id === undefined) {
|
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
|
||||||
|
if (source_id === undefined || target_id === undefined || item?.parent == target_id) {
|
||||||
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
|
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
|
||||||
this.clearState()
|
this.clearState()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.genericAPI(this.this_model, this.Actions.MOVE, {'source': source_id, 'target': target_id}).then((result) => {
|
this.genericAPI(this.this_model, this.Actions.MOVE, {'source': source_id, 'target': target_id}).then((result) => {
|
||||||
if (target_id === 0) {
|
if (target_id === 0) {
|
||||||
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
|
|
||||||
this.items_left = [item].concat(this.destroyCard(source_id, this.items_left)) // order matters, destroy old card before adding it back in at root
|
this.items_left = [item].concat(this.destroyCard(source_id, this.items_left)) // order matters, destroy old card before adding it back in at root
|
||||||
this.items_right = [...[item]].concat(this.destroyCard(source_id, this.items_right)) // order matters, destroy old card before adding it back in at root
|
this.items_right = [...[item]].concat(this.destroyCard(source_id, this.items_right)) // order matters, destroy old card before adding it back in at root
|
||||||
item.parent = null
|
item.parent = null
|
||||||
|
@ -172,6 +172,11 @@ export default {
|
|||||||
resetSearch: function () {
|
resetSearch: function () {
|
||||||
this.search_right = ''
|
this.search_right = ''
|
||||||
this.search_left = ''
|
this.search_left = ''
|
||||||
|
this.right_page = 0
|
||||||
|
this.left_page = 0
|
||||||
|
this.right += 1
|
||||||
|
this.left += 1
|
||||||
|
this.$emit('reset')
|
||||||
},
|
},
|
||||||
infiniteHandler: function($state, col) {
|
infiniteHandler: function($state, col) {
|
||||||
let params = {
|
let params = {
|
||||||
|
@ -1,212 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div row>
|
|
||||||
<b-card no-body d-flex flex-column :class="{'border border-primary' : over, 'shake': isError}"
|
|
||||||
refs="keywordCard"
|
|
||||||
style="height: 10vh;" :style="{'cursor:grab' : draggable}"
|
|
||||||
@dragover.prevent
|
|
||||||
@dragenter.prevent
|
|
||||||
:draggable="draggable"
|
|
||||||
@dragstart="handleDragStart($event)"
|
|
||||||
@dragenter="handleDragEnter($event)"
|
|
||||||
@dragleave="handleDragLeave($event)"
|
|
||||||
@drop="handleDragDrop($event)">
|
|
||||||
<b-row no-gutters style="height:inherit;">
|
|
||||||
<b-col no-gutters md="3" style="height:inherit;">
|
|
||||||
<b-card-img-lazy style="object-fit: cover; height: 10vh;" :src="keyword_image" v-bind:alt="$t('Recipe_Image')"></b-card-img-lazy>
|
|
||||||
</b-col>
|
|
||||||
<b-col no-gutters md="9" style="height:inherit;">
|
|
||||||
<b-card-body class="m-0 py-0" style="height:inherit;">
|
|
||||||
<b-card-text class=" h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
|
|
||||||
<h5 class="m-0 mt-1 text-truncate">{{ keyword.name }}</h5>
|
|
||||||
<div class= "m-0 text-truncate">{{ keyword.description }}</div>
|
|
||||||
<div class="mt-auto mb-1 d-flex flex-row justify-content-end">
|
|
||||||
<div v-if="keyword.numchild !=0" class="mx-2 btn btn-link btn-sm"
|
|
||||||
style="z-index: 800;" v-on:click="$emit('item-action',{'action':'get-children','source':keyword})">
|
|
||||||
<div v-if="!keyword.expanded">{{keyword.numchild}} {{$t('Keywords')}}</div>
|
|
||||||
<div v-else>{{$t('Hide Keywords')}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="mx-2 btn btn-link btn-sm" style="z-index: 800;"
|
|
||||||
v-on:click="$emit('item-action',{'action':'get-recipes','source':keyword})">
|
|
||||||
<div v-if="!keyword.show_recipes">{{keyword.numrecipe}} {{$t('Recipes')}}</div>
|
|
||||||
<div v-else>{{$t('Hide Recipes')}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</b-card-text>
|
|
||||||
</b-card-body>
|
|
||||||
</b-col>
|
|
||||||
<div class="card-img-overlay justify-content-right h-25 m-0 p-0 text-right">
|
|
||||||
<generic-context-menu class="p-0"
|
|
||||||
:show_merge="true"
|
|
||||||
:show_move="true"
|
|
||||||
@item-action="$emit('item-action', {'action': $event, 'source': keyword})">
|
|
||||||
</generic-context-menu>
|
|
||||||
</div>
|
|
||||||
</b-row>
|
|
||||||
</b-card>
|
|
||||||
<!-- recursively add child keywords -->
|
|
||||||
<div class="row" v-if="keyword.expanded">
|
|
||||||
<div class="col-md-11 offset-md-1">
|
|
||||||
<keyword-card v-for="child in keyword.children"
|
|
||||||
:keyword="child"
|
|
||||||
v-bind:key="child.id"
|
|
||||||
draggable="true"
|
|
||||||
@item-action="$emit('item-action', $event)">
|
|
||||||
</keyword-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- conditionally view recipes -->
|
|
||||||
<div class="row" v-if="keyword.show_recipes">
|
|
||||||
<div class="col-md-11 offset-md-1">
|
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));grid-gap: 1rem;">
|
|
||||||
<recipe-card v-for="r in keyword.recipes"
|
|
||||||
v-bind:key="r.id"
|
|
||||||
:recipe="r">
|
|
||||||
</recipe-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- this should be made a generic component, would also require mixin for functions that generate the popup and put in parent container-->
|
|
||||||
<b-list-group ref="tooltip" variant="light" v-show="show_menu" v-on-clickaway="closeMenu" style="z-index:999; cursor:pointer">
|
|
||||||
<b-list-group-item action v-on:click="$emit('item-action',{'action': 'move', 'target': keyword, 'source': source}); closeMenu()">
|
|
||||||
<i class="fas fa-expand-arrows-alt fa-fw"></i> {{$t('Move')}}: {{$t('move_confirmation', {'child': source.name,'parent':keyword.name})}}
|
|
||||||
</b-list-group-item>
|
|
||||||
<b-list-group-item action v-on:click="$emit('item-action',{'action': 'merge', 'target': keyword, 'source': source}); closeMenu()">
|
|
||||||
<i class="fas fa-compress-arrows-alt fa-fw"></i> {{$t('Merge')}}: {{ $t('merge_confirmation', {'source': source.name,'target':keyword.name}) }}
|
|
||||||
</b-list-group-item>
|
|
||||||
<b-list-group-item action v-on:click="closeMenu()">
|
|
||||||
<i class="fas fa-times fa-fw"></i> {{$t('Cancel')}}
|
|
||||||
</b-list-group-item>
|
|
||||||
</b-list-group>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import GenericContextMenu from "@/components/GenericContextMenu";
|
|
||||||
import RecipeCard from "@/components/RecipeCard";
|
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
|
||||||
import { createPopper } from '@popperjs/core';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "KeywordCard",
|
|
||||||
components: { GenericContextMenu, RecipeCard },
|
|
||||||
mixins: [clickaway],
|
|
||||||
props: {
|
|
||||||
keyword: Object,
|
|
||||||
draggable: {type: Boolean, default: false}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
keyword_image: '',
|
|
||||||
over: false,
|
|
||||||
show_menu: false,
|
|
||||||
dragMenu: undefined,
|
|
||||||
isError: false,
|
|
||||||
source: {},
|
|
||||||
target: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.keyword == null || this.keyword.image == null) {
|
|
||||||
this.keyword_image = window.IMAGE_PLACEHOLDER
|
|
||||||
} else {
|
|
||||||
this.keyword_image = this.keyword.image
|
|
||||||
}
|
|
||||||
this.dragMenu = this.$refs.tooltip
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleDragStart: function(e) {
|
|
||||||
this.isError = false
|
|
||||||
e.dataTransfer.setData('source', JSON.stringify(this.keyword))
|
|
||||||
},
|
|
||||||
handleDragEnter: function(e) {
|
|
||||||
if (!e.currentTarget.contains(e.relatedTarget) && e.relatedTarget != null) {
|
|
||||||
this.over = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleDragLeave: function(e) {
|
|
||||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
||||||
this.over = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleDragDrop: function(e) {
|
|
||||||
let source = JSON.parse(e.dataTransfer.getData('source'))
|
|
||||||
if (source.id != this.keyword.id){
|
|
||||||
this.source = source
|
|
||||||
let menuLocation = {getBoundingClientRect: this.generateLocation(e.clientX, e.clientY),}
|
|
||||||
this.show_menu = true
|
|
||||||
let popper = createPopper(
|
|
||||||
menuLocation,
|
|
||||||
this.dragMenu,
|
|
||||||
{
|
|
||||||
placement: 'bottom-start',
|
|
||||||
modifiers: [
|
|
||||||
{
|
|
||||||
name: 'preventOverflow',
|
|
||||||
options: {
|
|
||||||
rootBoundary: 'document',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'flip',
|
|
||||||
options: {
|
|
||||||
fallbackPlacements: ['bottom-end', 'top-start', 'top-end', 'left-start', 'right-start'],
|
|
||||||
rootBoundary: 'document',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
popper.update()
|
|
||||||
this.over = false
|
|
||||||
this.$emit({'action': 'drop', 'target': this.keyword, 'source': this.source})
|
|
||||||
} else {
|
|
||||||
this.isError = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
generateLocation: function (x = 0, y = 0) {
|
|
||||||
return () => ({
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
top: y,
|
|
||||||
right: x,
|
|
||||||
bottom: y,
|
|
||||||
left: x,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
closeMenu: function(){
|
|
||||||
this.show_menu = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.shake {
|
|
||||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
backface-visibility: hidden;
|
|
||||||
perspective: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shake {
|
|
||||||
10%,
|
|
||||||
90% {
|
|
||||||
transform: translate3d(-1px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
20%,
|
|
||||||
80% {
|
|
||||||
transform: translate3d(2px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
30%,
|
|
||||||
50%,
|
|
||||||
70% {
|
|
||||||
transform: translate3d(-4px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
40%,
|
|
||||||
60% {
|
|
||||||
transform: translate3d(4px, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
71
vue/src/components/Modals/EmojiInput.vue
Normal file
71
vue/src/components/Modals/EmojiInput.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<b-form-group
|
||||||
|
v-bind:label="label"
|
||||||
|
class="mb-3">
|
||||||
|
<twemoji-textarea
|
||||||
|
:ref="'_edit_' + id"
|
||||||
|
:initialContent="value"
|
||||||
|
:emojiData="emojiDataAll"
|
||||||
|
:emojiGroups="emojiGroups"
|
||||||
|
triggerType="hover"
|
||||||
|
recentEmojisFeat="true"
|
||||||
|
recentEmojisStorage="local"
|
||||||
|
@contentChanged="setIcon"
|
||||||
|
/>
|
||||||
|
</b-form-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {TwemojiTextarea} from '@kevinfaguiar/vue-twemoji-picker';
|
||||||
|
// TODO add localization
|
||||||
|
import EmojiAllData from '@kevinfaguiar/vue-twemoji-picker/emoji-data/en/emoji-all-groups.json';
|
||||||
|
import EmojiGroups from '@kevinfaguiar/vue-twemoji-picker/emoji-data/emoji-groups.json';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EmojiInput',
|
||||||
|
components: {TwemojiTextarea},
|
||||||
|
props: {
|
||||||
|
field: {type: String, default: 'You Forgot To Set Field Name'},
|
||||||
|
label: {type: String, default: ''},
|
||||||
|
value: {type: String, default: ''},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
new_value: undefined,
|
||||||
|
id: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// modelName() {
|
||||||
|
// return this?.model?.name ?? this.$t('Search')
|
||||||
|
// },
|
||||||
|
emojiDataAll() {
|
||||||
|
return EmojiAllData;
|
||||||
|
},
|
||||||
|
emojiGroups() {
|
||||||
|
return EmojiGroups;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'new_value': function () {
|
||||||
|
this.$root.$emit('change', this.field, this.new_value ?? null)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.id = this._uid
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
prepareEmoji: function() {
|
||||||
|
this.$refs['_edit_' + this.id].addText(this.this_item.icon || '');
|
||||||
|
this.$refs['_edit_' + this.id].blur()
|
||||||
|
document.getElementById('btn-emoji-default').disabled = true;
|
||||||
|
},
|
||||||
|
setIcon: function(icon) {
|
||||||
|
this.new_value = icon
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -12,7 +12,6 @@
|
|||||||
:model="listModel(f.list)"
|
:model="listModel(f.list)"
|
||||||
:sticky_options="f.sticky_options || undefined"
|
:sticky_options="f.sticky_options || undefined"
|
||||||
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
|
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
|
||||||
<!-- TODO: add emoji field -->
|
|
||||||
<!-- TODO: add multi-selection input list -->
|
<!-- TODO: add multi-selection input list -->
|
||||||
<checkbox-input v-if="f.type=='checkbox'"
|
<checkbox-input v-if="f.type=='checkbox'"
|
||||||
:label="f.label"
|
:label="f.label"
|
||||||
@ -23,6 +22,11 @@
|
|||||||
:value="f.value"
|
:value="f.value"
|
||||||
:field="f.field"
|
:field="f.field"
|
||||||
:placeholder="f.placeholder"/>
|
:placeholder="f.placeholder"/>
|
||||||
|
<emoji-input v-if="f.type=='emoji'"
|
||||||
|
:label="f.label"
|
||||||
|
:value="f.value"
|
||||||
|
:field="f.field"
|
||||||
|
@change="storeValue"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-slot:modal-footer>
|
<template v-slot:modal-footer>
|
||||||
@ -43,10 +47,11 @@ import {Models} from "@/utils/models";
|
|||||||
import CheckboxInput from "@/components/Modals/CheckboxInput";
|
import CheckboxInput from "@/components/Modals/CheckboxInput";
|
||||||
import LookupInput from "@/components/Modals/LookupInput";
|
import LookupInput from "@/components/Modals/LookupInput";
|
||||||
import TextInput from "@/components/Modals/TextInput";
|
import TextInput from "@/components/Modals/TextInput";
|
||||||
|
import EmojiInput from "@/components/Modals/EmojiInput";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GenericModalForm',
|
name: 'GenericModalForm',
|
||||||
components: {CheckboxInput, LookupInput, TextInput},
|
components: {CheckboxInput, LookupInput, TextInput, EmojiInput},
|
||||||
props: {
|
props: {
|
||||||
model: {required: true, type: Object, default: function() {}},
|
model: {required: true, type: Object, default: function() {}},
|
||||||
action: {required: true, type: Object, default: function() {}},
|
action: {required: true, type: Object, default: function() {}},
|
||||||
|
@ -47,10 +47,5 @@ export default {
|
|||||||
this.$root.$emit('change', this.field, this.new_value?.id ?? null)
|
this.$root.$emit('change', this.field, this.new_value?.id ?? null)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
Button: function(e) {
|
|
||||||
this.$bvModal.show('modal')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -121,5 +121,6 @@
|
|||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
"Recipe": "Recipe",
|
"Recipe": "Recipe",
|
||||||
"tree_root": "Root of Tree"
|
"tree_root": "Root of Tree",
|
||||||
|
"Icon": "Icon"
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,33 @@ export class Models {
|
|||||||
static KEYWORD = {
|
static KEYWORD = {
|
||||||
'name': i18n.t('Keyword'), // *OPTIONAL: parameters will be built model -> model_type -> default
|
'name': i18n.t('Keyword'), // *OPTIONAL: parameters will be built model -> model_type -> default
|
||||||
'apiName': 'Keyword',
|
'apiName': 'Keyword',
|
||||||
'model_type': this.TREE
|
'model_type': this.TREE,
|
||||||
|
'create': {
|
||||||
|
// if not defined partialUpdate will use the same parameters, prepending 'id'
|
||||||
|
'params': [['name', 'description', 'icon']],
|
||||||
|
'form': {
|
||||||
|
'name': {
|
||||||
|
'form_field': true,
|
||||||
|
'type': 'text',
|
||||||
|
'field': 'name',
|
||||||
|
'label': i18n.t('Name'),
|
||||||
|
'placeholder': ''
|
||||||
|
},
|
||||||
|
'description': {
|
||||||
|
'form_field': true,
|
||||||
|
'type': 'text',
|
||||||
|
'field': 'description',
|
||||||
|
'label': i18n.t('Description'),
|
||||||
|
'placeholder': ''
|
||||||
|
},
|
||||||
|
'icon': {
|
||||||
|
'form_field': true,
|
||||||
|
'type': 'emoji',
|
||||||
|
'field': 'icon',
|
||||||
|
'label': i18n.t('Icon')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
static UNIT = {}
|
static UNIT = {}
|
||||||
static RECIPE = {}
|
static RECIPE = {}
|
||||||
|
@ -25,10 +25,6 @@ const pages = {
|
|||||||
entry: './src/apps/UserFileView/main.js',
|
entry: './src/apps/UserFileView/main.js',
|
||||||
chunks: ['chunk-vendors']
|
chunks: ['chunk-vendors']
|
||||||
},
|
},
|
||||||
'keyword_list_view': {
|
|
||||||
entry: './src/apps/KeywordListView/main.js',
|
|
||||||
chunks: ['chunk-vendors']
|
|
||||||
},
|
|
||||||
'model_list_view': {
|
'model_list_view': {
|
||||||
entry: './src/apps/ModelListView/main.js',
|
entry: './src/apps/ModelListView/main.js',
|
||||||
chunks: ['chunk-vendors']
|
chunks: ['chunk-vendors']
|
||||||
@ -85,7 +81,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO make this conditional on .env DEBUG = FALSE
|
// TODO make this conditional on .env DEBUG = FALSE
|
||||||
config.optimization.minimize(false)
|
config.optimization.minimize(true)
|
||||||
);
|
);
|
||||||
|
|
||||||
//TODO somehow remov them as they are also added to the manifest config of the service worker
|
//TODO somehow remov them as they are also added to the manifest config of the service worker
|
||||||
|
@ -1 +1,119 @@
|
|||||||
{"status":"done","chunks":{"recipe_search_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_search_view.js"],"recipe_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/recipe_view.js"],"offline_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/offline_view.js"],"import_response_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/import_response_view.js"],"supermarket_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/supermarket_view.js"],"user_file_view":["css/chunk-vendors.css","js/chunk-vendors.js","js/user_file_view.js"],"keyword_list_view":["css/chunk-vendors.css","js/chunk-vendors.js","css/keyword_list_view.css","js/keyword_list_view.js"],"model_list_view":["css/chunk-vendors.css","js/chunk-vendors.js","css/model_list_view.css","js/model_list_view.js"]},"assets":{"../../templates/sw.js":{"name":"../../templates/sw.js","path":"../../templates/sw.js"},"css/chunk-vendors.css":{"name":"css/chunk-vendors.css","path":"css/chunk-vendors.css"},"js/chunk-vendors.js":{"name":"js/chunk-vendors.js","path":"js/chunk-vendors.js"},"js/import_response_view.js":{"name":"js/import_response_view.js","path":"js/import_response_view.js"},"css/keyword_list_view.css":{"name":"css/keyword_list_view.css","path":"css/keyword_list_view.css"},"js/keyword_list_view.js":{"name":"js/keyword_list_view.js","path":"js/keyword_list_view.js"},"css/model_list_view.css":{"name":"css/model_list_view.css","path":"css/model_list_view.css"},"js/model_list_view.js":{"name":"js/model_list_view.js","path":"js/model_list_view.js"},"js/offline_view.js":{"name":"js/offline_view.js","path":"js/offline_view.js"},"js/recipe_search_view.js":{"name":"js/recipe_search_view.js","path":"js/recipe_search_view.js"},"js/recipe_view.js":{"name":"js/recipe_view.js","path":"js/recipe_view.js"},"js/supermarket_view.js":{"name":"js/supermarket_view.js","path":"js/supermarket_view.js"},"js/user_file_view.js":{"name":"js/user_file_view.js","path":"js/user_file_view.js"},"recipe_search_view.html":{"name":"recipe_search_view.html","path":"recipe_search_view.html"},"recipe_view.html":{"name":"recipe_view.html","path":"recipe_view.html"},"offline_view.html":{"name":"offline_view.html","path":"offline_view.html"},"import_response_view.html":{"name":"import_response_view.html","path":"import_response_view.html"},"supermarket_view.html":{"name":"supermarket_view.html","path":"supermarket_view.html"},"user_file_view.html":{"name":"user_file_view.html","path":"user_file_view.html"},"keyword_list_view.html":{"name":"keyword_list_view.html","path":"keyword_list_view.html"},"model_list_view.html":{"name":"model_list_view.html","path":"model_list_view.html"},"manifest.json":{"name":"manifest.json","path":"manifest.json"}}}
|
{
|
||||||
|
"status": "done",
|
||||||
|
"assets": {
|
||||||
|
"../../templates/sw.js": {
|
||||||
|
"name": "../../templates/sw.js",
|
||||||
|
"path": "../../templates/sw.js"
|
||||||
|
},
|
||||||
|
"css/chunk-vendors.css": {
|
||||||
|
"name": "css/chunk-vendors.css",
|
||||||
|
"path": "css/chunk-vendors.css"
|
||||||
|
},
|
||||||
|
"js/chunk-vendors.js": {
|
||||||
|
"name": "js/chunk-vendors.js",
|
||||||
|
"path": "js/chunk-vendors.js"
|
||||||
|
},
|
||||||
|
"js/import_response_view.js": {
|
||||||
|
"name": "js/import_response_view.js",
|
||||||
|
"path": "js/import_response_view.js"
|
||||||
|
},
|
||||||
|
"css/model_list_view.css": {
|
||||||
|
"name": "css/model_list_view.css",
|
||||||
|
"path": "css/model_list_view.css"
|
||||||
|
},
|
||||||
|
"js/model_list_view.js": {
|
||||||
|
"name": "js/model_list_view.js",
|
||||||
|
"path": "js/model_list_view.js"
|
||||||
|
},
|
||||||
|
"js/offline_view.js": {
|
||||||
|
"name": "js/offline_view.js",
|
||||||
|
"path": "js/offline_view.js"
|
||||||
|
},
|
||||||
|
"js/recipe_search_view.js": {
|
||||||
|
"name": "js/recipe_search_view.js",
|
||||||
|
"path": "js/recipe_search_view.js"
|
||||||
|
},
|
||||||
|
"js/recipe_view.js": {
|
||||||
|
"name": "js/recipe_view.js",
|
||||||
|
"path": "js/recipe_view.js"
|
||||||
|
},
|
||||||
|
"js/supermarket_view.js": {
|
||||||
|
"name": "js/supermarket_view.js",
|
||||||
|
"path": "js/supermarket_view.js"
|
||||||
|
},
|
||||||
|
"js/user_file_view.js": {
|
||||||
|
"name": "js/user_file_view.js",
|
||||||
|
"path": "js/user_file_view.js"
|
||||||
|
},
|
||||||
|
"recipe_search_view.html": {
|
||||||
|
"name": "recipe_search_view.html",
|
||||||
|
"path": "recipe_search_view.html"
|
||||||
|
},
|
||||||
|
"recipe_view.html": {
|
||||||
|
"name": "recipe_view.html",
|
||||||
|
"path": "recipe_view.html"
|
||||||
|
},
|
||||||
|
"offline_view.html": {
|
||||||
|
"name": "offline_view.html",
|
||||||
|
"path": "offline_view.html"
|
||||||
|
},
|
||||||
|
"import_response_view.html": {
|
||||||
|
"name": "import_response_view.html",
|
||||||
|
"path": "import_response_view.html"
|
||||||
|
},
|
||||||
|
"supermarket_view.html": {
|
||||||
|
"name": "supermarket_view.html",
|
||||||
|
"path": "supermarket_view.html"
|
||||||
|
},
|
||||||
|
"user_file_view.html": {
|
||||||
|
"name": "user_file_view.html",
|
||||||
|
"path": "user_file_view.html"
|
||||||
|
},
|
||||||
|
"model_list_view.html": {
|
||||||
|
"name": "model_list_view.html",
|
||||||
|
"path": "model_list_view.html"
|
||||||
|
},
|
||||||
|
"manifest.json": {
|
||||||
|
"name": "manifest.json",
|
||||||
|
"path": "manifest.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chunks": {
|
||||||
|
"recipe_search_view": [
|
||||||
|
"css/chunk-vendors.css",
|
||||||
|
"js/chunk-vendors.js",
|
||||||
|
"js/recipe_search_view.js"
|
||||||
|
],
|
||||||
|
"recipe_view": [
|
||||||
|
"css/chunk-vendors.css",
|
||||||
|
"js/chunk-vendors.js",
|
||||||
|
"js/recipe_view.js"
|
||||||
|
],
|
||||||
|
"offline_view": [
|
||||||
|
"css/chunk-vendors.css",
|
||||||
|
"js/chunk-vendors.js",
|
||||||
|
"js/offline_view.js"
|
||||||
|
],
|
||||||
|
"import_response_view": [
|
||||||
|
"css/chunk-vendors.css",
|
||||||
|
"js/chunk-vendors.js",
|
||||||
|
"js/import_response_view.js"
|
||||||
|
],
|
||||||
|
"supermarket_view": [
|
||||||
|
"css/chunk-vendors.css",
|
||||||
|
"js/chunk-vendors.js",
|
||||||
|
"js/supermarket_view.js"
|
||||||
|
],
|
||||||
|
"user_file_view": [
|
||||||
|
"css/chunk-vendors.css",
|
||||||
|
"js/chunk-vendors.js",
|
||||||
|
"js/user_file_view.js"
|
||||||
|
],
|
||||||
|
"model_list_view": [
|
||||||
|
"css/chunk-vendors.css",
|
||||||
|
"js/chunk-vendors.js",
|
||||||
|
"css/model_list_view.css",
|
||||||
|
"js/model_list_view.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user