Merge branch 'typed-fields' into dev
This commit is contained in:
commit
a63da41435
8 changed files with 290 additions and 94 deletions
|
@ -1,100 +1,47 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<img
|
<h6>Editing Item <span class="badge badge-secondary">#{{ item.item_uid }}</span></h6>
|
||||||
v-if="!capturing"
|
<InputPhoto
|
||||||
class="img-fluid rounded mx-auto d-block mb-3"
|
:model="item"
|
||||||
:src="item.dataImage || `https://c3lf.de/api/1/thumbs/${item.file}`"
|
field="file"
|
||||||
alt="Image not available."
|
:on-capture="storeImage"
|
||||||
/>
|
/>
|
||||||
<video
|
<InputString
|
||||||
v-if="capturing"
|
label="description"
|
||||||
ref="video"
|
:model="item"
|
||||||
class="img-fluid rounded mx-auto d-block mb-3"
|
field="description"
|
||||||
>
|
:validation-fn="str => str && str.length > 0"
|
||||||
Video stream not available.
|
/>
|
||||||
</video>
|
<InputCombo
|
||||||
<canvas ref="canvas" class="img-fluid d-none"/>
|
label="box"
|
||||||
<div class="row" v-if="capturing && !streaming">
|
:model="item"
|
||||||
<div class="spinner-grow text-danger mx-auto" role="status">
|
nameKey="box"
|
||||||
<span class="sr-only">Loading...</span>
|
uniqueKey="cid"
|
||||||
</div>
|
:options="boxes"
|
||||||
</div>
|
/>
|
||||||
<div class="row m-auto">
|
</div>
|
||||||
<button v-if="!capturing" class="btn my-2 ml-auto btn-secondary" @click="openStream()">
|
|
||||||
<font-awesome-icon icon="camera"/>
|
|
||||||
</button>
|
|
||||||
<div v-if="capturing" class="btn-group my-2 ml-auto">
|
|
||||||
<button class="btn btn-success" @click="captureVideoImage()">
|
|
||||||
<font-awesome-icon icon="camera"/> Capture
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
:class="(item.file || item.dataImage) ? 'btn-danger' : 'btn-secondary disabled'"
|
|
||||||
@click="(item.file || item.dataImage) && closeStream()"
|
|
||||||
>
|
|
||||||
<font-awesome-icon icon="stop"/> Abort
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h6>Editing Item <span class="badge badge-secondary">#{{ item.item_uid }}</span></h6>
|
|
||||||
<form>
|
|
||||||
<div class="form-group" v-for="field in ['description', 'box']" :key="field">
|
|
||||||
<label>{{ field }}</label>
|
|
||||||
<input type="text" class="form-control" v-model="item[field]">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import InputString from './inputs/InputString';
|
||||||
|
import InputCombo from './inputs/InputCombo';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import InputPhoto from './inputs/InputPhoto';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Add',
|
name: 'EditItem',
|
||||||
|
components: {InputPhoto, InputCombo, InputString },
|
||||||
props: ['item'],
|
props: ['item'],
|
||||||
data: () => ({
|
computed: {
|
||||||
capturing: false,
|
...mapGetters(['getBoxes']),
|
||||||
streaming: false,
|
boxes({ getBoxes }) {
|
||||||
stream: undefined
|
return getBoxes.map(obj => ({cid: obj.cid, box: obj.name}));
|
||||||
}),
|
|
||||||
methods: {
|
|
||||||
openStream() {
|
|
||||||
if (!this.capturing) {
|
|
||||||
this.capturing = true;
|
|
||||||
this.streaming = false;
|
|
||||||
navigator.mediaDevices.getUserMedia({video: true, audio: false}).then(stream => {
|
|
||||||
this.stream = stream;
|
|
||||||
const { video } = this.$refs;
|
|
||||||
video.srcObject = stream;
|
|
||||||
video.play();
|
|
||||||
video.addEventListener('canplay', () => {
|
|
||||||
this.streaming = true;
|
|
||||||
}, false);
|
|
||||||
}).catch(err => console.log(err)); // todo: toast error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
captureVideoImage() {
|
|
||||||
const { video, canvas } = this.$refs;
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
const { videoWidth, videoHeight } = video;
|
|
||||||
canvas.width = videoWidth;
|
|
||||||
canvas.height = videoHeight;
|
|
||||||
context.drawImage(video, 0, 0, videoWidth, videoHeight);
|
|
||||||
this.item.dataImage = canvas.toDataURL('image/png');
|
|
||||||
this.closeStream();
|
|
||||||
},
|
|
||||||
closeStream() {
|
|
||||||
if (this.capturing) {
|
|
||||||
this.stream.getTracks().forEach(s => s.stop());
|
|
||||||
this.capturing = false;
|
|
||||||
this.streaming = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
methods: {
|
||||||
if (!this.item.file)
|
storeImage(image) {
|
||||||
this.openStream();
|
this.item.dataImage = image;
|
||||||
},
|
}
|
||||||
beforeDestroy() {
|
|
||||||
this.closeStream();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -65,8 +65,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hacky Fix, for this Issue: https://hannover.ccc.de/gitlab/c3lf/lffrontend/issues/26 */
|
/* Hacky Fix, for this Issue: https://hannover.ccc.de/gitlab/c3lf/lffrontend/issues/26 */
|
||||||
.modal-body {
|
/*.modal-body {
|
||||||
max-height: calc(100vh - 200px);
|
max-height: calc(100vh - 200px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}*/
|
||||||
</style>
|
</style>
|
50
src/components/inputs/Addon.vue
Normal file
50
src/components/inputs/Addon.vue
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span
|
||||||
|
class="input-group-text"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-placement="top"
|
||||||
|
:title="type"
|
||||||
|
>{{ type[0] }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
:class="'btn-' + color"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-placement="top"
|
||||||
|
:title="tooltip"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="icon"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import $ from 'jquery';
|
||||||
|
import 'bootstrap/js/dist/tooltip';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Addon',
|
||||||
|
props: [ 'type', 'isValid' ],
|
||||||
|
mounted() {
|
||||||
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
icon() {
|
||||||
|
if (this.isValid == undefined) return 'pen';
|
||||||
|
if (this.isValid == false) return 'times';
|
||||||
|
return 'check';
|
||||||
|
},
|
||||||
|
color() {
|
||||||
|
if (this.isValid == undefined) return 'info';
|
||||||
|
if (this.isValid == false) return 'danger';
|
||||||
|
return 'success';
|
||||||
|
},
|
||||||
|
tooltip() {
|
||||||
|
if (this.isValid == undefined) return 'no data validation';
|
||||||
|
if (this.isValid == false) return 'data invalid';
|
||||||
|
return 'data valid';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
75
src/components/inputs/InputCombo.vue
Normal file
75
src/components/inputs/InputCombo.vue
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div class="form-group">
|
||||||
|
<label :for="label">{{ label }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
>Search</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<a
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option[uniqueKey]"
|
||||||
|
class="dropdown-item"
|
||||||
|
@click="setInternalValue(option)"
|
||||||
|
>
|
||||||
|
{{ option[nameKey] }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" :id="label" v-model="internalName">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
:class="{ 'btn-info disabled': isValid, 'btn-success': !isValid }"
|
||||||
|
v-if="!isValid"
|
||||||
|
@click="addOption()"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="plus"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Addon type="Combo Box" :is-valid="isValid"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Addon from './Addon';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'InputCombo',
|
||||||
|
components: {Addon},
|
||||||
|
props: [ 'label', 'model', 'nameKey', 'uniqueKey', 'options', 'onOptionAdd' ],
|
||||||
|
data: ({ options, model, nameKey, uniqueKey }) => ({
|
||||||
|
internalName: model[nameKey],
|
||||||
|
selectedOption: options.filter(e => e[uniqueKey] === model[uniqueKey])[0],
|
||||||
|
addingOption: false
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
isValid: ({options, nameKey, internalName}) => options.some(e => e[nameKey] === internalName)
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
internalName(newValue, oldValue) {
|
||||||
|
if (this.isValid) {
|
||||||
|
if(newValue!=this.selectedOption[this.nameKey]){
|
||||||
|
this.selectedOption = this.options.filter(e => e[this.nameKey] === this.internalName)[0];
|
||||||
|
}
|
||||||
|
this.model[this.nameKey] = this.selectedOption[this.nameKey];
|
||||||
|
this.model[this.uniqueKey] = this.selectedOption[this.uniqueKey];
|
||||||
|
}
|
||||||
|
console.log(oldValue, newValue, this.isValid);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setInternalValue(option) {
|
||||||
|
this.selectedOption = option;
|
||||||
|
this.internalName = option[this.nameKey];
|
||||||
|
},
|
||||||
|
addOption() {
|
||||||
|
this.onOptionAdd({[this.nameKey]: this.internalName});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
95
src/components/inputs/InputPhoto.vue
Normal file
95
src/components/inputs/InputPhoto.vue
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
v-if="!capturing"
|
||||||
|
class="img-fluid rounded mx-auto d-block mb-3"
|
||||||
|
:src="dataImage || `https://c3lf.de/api/1/thumbs/${model[field]}`"
|
||||||
|
alt="Image not available."
|
||||||
|
/>
|
||||||
|
<video
|
||||||
|
v-if="capturing"
|
||||||
|
ref="video"
|
||||||
|
class="img-fluid rounded mx-auto d-block mb-3"
|
||||||
|
>
|
||||||
|
Video stream not available.
|
||||||
|
</video>
|
||||||
|
<canvas ref="canvas" class="img-fluid d-none"/>
|
||||||
|
<div class="row" v-if="capturing && !streaming">
|
||||||
|
<div class="spinner-grow text-danger mx-auto" role="status">
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row m-auto">
|
||||||
|
<button v-if="!capturing" class="btn my-2 ml-auto btn-secondary" @click="openStream()">
|
||||||
|
<font-awesome-icon icon="camera"/>
|
||||||
|
</button>
|
||||||
|
<div v-if="capturing" class="btn-group my-2 ml-auto">
|
||||||
|
<button class="btn btn-success" @click="captureVideoImage()">
|
||||||
|
<font-awesome-icon icon="camera"/> Capture
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
:class="(model[field] || dataImage) ? 'btn-danger' : 'btn-secondary disabled'"
|
||||||
|
@click="(model[field] || dataImage) && closeStream()"
|
||||||
|
>
|
||||||
|
<font-awesome-icon icon="stop"/> Abort
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'InputPhoto',
|
||||||
|
props: [ 'model', 'field', 'onCapture' ],
|
||||||
|
data: () => ({
|
||||||
|
capturing: false,
|
||||||
|
streaming: false,
|
||||||
|
stream: undefined,
|
||||||
|
dataImage: undefined
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
openStream() {
|
||||||
|
if (!this.capturing) {
|
||||||
|
this.capturing = true;
|
||||||
|
this.streaming = false;
|
||||||
|
navigator.mediaDevices.getUserMedia({video: true, audio: false}).then(stream => {
|
||||||
|
this.stream = stream;
|
||||||
|
const { video } = this.$refs;
|
||||||
|
video.srcObject = stream;
|
||||||
|
video.play();
|
||||||
|
video.addEventListener('canplay', () => {
|
||||||
|
this.streaming = true;
|
||||||
|
}, false);
|
||||||
|
}).catch(err => console.log(err)); // todo: toast error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
captureVideoImage() {
|
||||||
|
const { video, canvas } = this.$refs;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
const { videoWidth, videoHeight } = video;
|
||||||
|
canvas.width = videoWidth;
|
||||||
|
canvas.height = videoHeight;
|
||||||
|
context.drawImage(video, 0, 0, videoWidth, videoHeight);
|
||||||
|
this.dataImage = canvas.toDataURL('image/png');
|
||||||
|
this.onCapture(this.dataImage);
|
||||||
|
this.closeStream();
|
||||||
|
},
|
||||||
|
closeStream() {
|
||||||
|
if (this.capturing) {
|
||||||
|
this.stream.getTracks().forEach(s => s.stop());
|
||||||
|
this.capturing = false;
|
||||||
|
this.streaming = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (!this.model[this.field])
|
||||||
|
this.openStream();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.closeStream();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
29
src/components/inputs/InputString.vue
Normal file
29
src/components/inputs/InputString.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="form-group">
|
||||||
|
<label :for="label">{{ label }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" :id="label" v-model="model[field]">
|
||||||
|
<Addon v-if="!hasValidationFn" type="String"/>
|
||||||
|
<Addon v-if="hasValidationFn" type="String" :is-valid="isValid"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Addon from './Addon';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'InputString',
|
||||||
|
components: { Addon },
|
||||||
|
props: [ 'label', 'model', 'field', 'validationFn' ],
|
||||||
|
computed: {
|
||||||
|
hasValidationFn({ validationFn }) {
|
||||||
|
return validationFn != undefined;
|
||||||
|
},
|
||||||
|
isValid({ model, field, validationFn, hasValidationFn }) {
|
||||||
|
if (!hasValidationFn) return true;
|
||||||
|
return validationFn(model[field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -11,10 +11,10 @@ import 'bootstrap/dist/js/bootstrap.min.js';
|
||||||
|
|
||||||
// fontawesome
|
// fontawesome
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, faWindowClose, faCamera, faStop } from '@fortawesome/free-solid-svg-icons';
|
import { faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, faWindowClose, faCamera, faStop, faPen, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, faWindowClose, faCamera, faStop);
|
library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, faWindowClose, faCamera, faStop, faPen, faCheck, faTimes);
|
||||||
|
|
||||||
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ const store = new Vuex.Store({
|
||||||
getEventSlug: state => state.route && state.route.params.event? state.route.params.event : state.events.length ? state.events[0].slug : '36C3',
|
getEventSlug: state => state.route && state.route.params.event? state.route.params.event : state.events.length ? state.events[0].slug : '36C3',
|
||||||
getActiveView: state => state.route.name || 'items',
|
getActiveView: state => state.route.name || 'items',
|
||||||
getFilters: state => state.route.query,
|
getFilters: state => state.route.query,
|
||||||
|
getBoxes: state => state.loadedBoxes
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
replaceEvents(state, events) {
|
replaceEvents(state, events) {
|
||||||
|
@ -91,7 +92,6 @@ const store = new Vuex.Store({
|
||||||
let file = new File([blob], 'dot.png', blob);
|
let file = new File([blob], 'dot.png', blob);
|
||||||
delete item.dataImage;
|
delete item.dataImage;
|
||||||
item.image = file;
|
item.image = file;
|
||||||
item.cid = item.cid || 1;
|
|
||||||
var formData = new FormData();
|
var formData = new FormData();
|
||||||
for ( var key in item ) formData.append(key, item[key]);
|
for ( var key in item ) formData.append(key, item[key]);
|
||||||
const { data } = await axios.post(`/1/${getters.getEventSlug}/item`, formData, {
|
const { data } = await axios.post(`/1/${getters.getEventSlug}/item`, formData, {
|
||||||
|
|
Loading…
Reference in a new issue