This commit is contained in:
j3d1 2024-07-13 17:28:38 +02:00
parent 1cfeb34a4c
commit 4d8406bd7c
10 changed files with 438 additions and 182 deletions

View file

@ -0,0 +1,133 @@
<template>
<div class="async-wrapper" :class="{ 'loaded': loaded }">
<div class="deferred">
<slot></slot>
</div>
<div class="loader-wrapper">
<div class="loader-ellipsis">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AsyncLoader',
props: {
loaded: {
type: Boolean,
default: false
}
}
};
</script>
<style>
.async-wrapper {
position: relative;
}
.async-wrapper > .deferred {
width: 100%;
height: 100%;
display: none;
}
.async-wrapper.loaded > .deferred {
display: block;
}
.async-wrapper > .loader-wrapper {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis {
color: #17a2b8;
}
.async-wrapper.loaded > .loader-wrapper > .loader-ellipsis {
display: none;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis,
.async-wrapper > .loader-wrapper > .loader-ellipsis div {
box-sizing: border-box;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div {
position: absolute;
top: 33.33333px;
width: 13.33333px;
height: 13.33333px;
border-radius: 50%;
background: currentColor;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div:nth-child(1) {
left: 8px;
animation: loader-ellipsis1 0.6s infinite;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div:nth-child(2) {
left: 8px;
animation: loader-ellipsis2 0.6s infinite;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div:nth-child(3) {
left: 32px;
animation: loader-ellipsis2 0.6s infinite;
}
.async-wrapper > .loader-wrapper > .loader-ellipsis div:nth-child(4) {
left: 56px;
animation: loader-ellipsis3 0.6s infinite;
}
@keyframes loader-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes loader-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes loader-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
</style>

View file

@ -29,16 +29,7 @@
</router-link>
</li>
</ul>
<form class="form-inline mt-1 my-lg-auto my-xl-auto w-100 d-inline mr-1" v-if="hasPermissions">
<input
class="form-control w-100"
type="search"
placeholder="Search"
aria-label="Search"
@input="searchEventItems($event.target.value)"
disabled
>
</form>
<SearchBox v-if="hasPermissions" class="mt-1 my-lg-auto my-xl-auto w-100 d-inline mr-1"/>
<div class="custom-control-inline mr-1" v-if="hasPermissions">
<div class="btn-group btn-group-toggle mr-1" v-if="isItemView()">
<button :class="['btn', 'btn-info', { active: layout === 'cards' }]" @click="setLayout('cards')">
@ -103,9 +94,13 @@
<script>
import {mapState, mapActions, mapMutations, mapGetters} from 'vuex';
import SearchBox from "@/components/inputs/SearchBox.vue";
export default {
name: 'Navbar',
components: {
SearchBox
},
data: () => ({
views: [
{'title': 'items', 'path': 'items'},
@ -122,7 +117,7 @@ export default {
...mapGetters(['getEventSlug', 'getActiveView', "checkPermission", "hasPermissions", "layout", "route"]),
},
methods: {
...mapActions(['changeEvent', 'changeView', 'searchEventItems']),
...mapActions(['changeEvent', 'changeView']),
...mapMutations(['logout']),
navigateTo(link) {
if (this.route.path !== link)

View file

@ -40,10 +40,10 @@
<div class="">
<textarea placeholder="add comment..." v-model="newComment" class="form-control">
</textarea>
<button class="btn btn-primary float-right" @click="addCommentAndClear">
<AsyncButton class="btn btn-primary float-right" :task="addCommentAndClear">
<font-awesome-icon icon="comment"/>
Save Comment
</button>
</AsyncButton>
</div>
</div>
</li>
@ -58,10 +58,10 @@
<div>
<textarea placeholder="reply mail..." v-model="newMail" class="form-control">
</textarea>
<button class="btn btn-primary float-right" @click="sendMailAndClear">
<AsyncButton class="btn btn-primary float-right" :task="sendMailAndClear">
<font-awesome-icon icon="envelope"/>
Send Mail
</button>
</AsyncButton>
</div>
</div>
</li>
@ -73,15 +73,16 @@
import TimelineMail from "@/components/TimelineMail.vue";
import TimelineComment from "@/components/TimelineComment.vue";
import TimelineStateChange from "@/components/TimelineStateChange.vue";
import {mapGetters} from "vuex";
import {mapActions, mapGetters} from "vuex";
import TimelineAssignment from "@/components/TimelineAssignment.vue";
import TimelineRelatedItem from "@/components/TimelineRelatedItem.vue";
import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
import AsyncButton from "@/components/inputs/AsyncButton.vue";
export default {
name: 'Timeline',
components: {
TimelineShippingVoucher,
TimelineShippingVoucher, AsyncButton,
TimelineRelatedItem, TimelineAssignment, TimelineStateChange, TimelineComment, TimelineMail
},
props: {
@ -103,12 +104,15 @@ export default {
},
},
methods: {
sendMailAndClear: function () {
this.$emit('sendMail', this.newMail);
...mapActions(['sendMail', 'postComment']),
sendMailAndClear: async function () {
//this.$emit('sendMail', this.newMail);
await this.sendMail(this.newMail);
this.newMail = "";
},
addCommentAndClear: function () {
this.$emit('addComment', this.newComment);
addCommentAndClear: async function () {
//this.$emit('addComment', this.newComment);
await this.postComment(this.newComment);
this.newComment = "";
}
}

View file

@ -0,0 +1,47 @@
<template>
<button @click.stop="handleClick" :disabled="disabled">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"
:class="{'d-none': !disabled}"></span>
<span class="ml-2" :class="{'d-none': !disabled}">In Progress...</span>
<span :class="{'d-none': disabled}"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'AsyncButton',
data() {
return {
disabled: false,
};
},
props: {
task: {
type: Function,
required: true,
},
},
methods: {
async handleClick() {
console.log("AsyncButton.handleClick() called");
if (this.task && typeof this.task === 'function') {
this.disabled = true;
try {
await this.task();
} catch (e) {
console.error(e);
} finally {
this.disabled = false;
}
}
},
}
};
</script>
<style scoped>
.spinner-border {
vertical-align: -0.125em;
}
</style>

View file

@ -0,0 +1,29 @@
<template>
<form class="form-inline">
<input
class="form-control w-100"
type="search"
placeholder="Search"
aria-label="Search"
v-model="search_query"
@input="searchEventItems(search_query)"
>
</form>
</template>
<script>
import {mapActions} from "vuex";
export default {
name: 'SearchBox',
data() {
return {
search_query: ''
}
},
methods: {
...mapActions(['searchEventItems']),
}
};
</script>