use new components in history, remove automatic toggling, adapt scrolling behavior

This commit is contained in:
axolotle 2021-02-15 15:33:41 +01:00
parent 56a3b29f65
commit 957ca17b84

View file

@ -1,66 +1,82 @@
<template> <template>
<div id="console"> <b-card no-body id="console">
<b-list-group>
<!-- HISTORY BAR --> <!-- HISTORY BAR -->
<b-list-group-item <b-card-header
class="d-flex align-items-center"
:class="{ 'bg-best text-white': open }"
ref="history-button"
role="button" tabindex="0" role="button" tabindex="0"
:aria-expanded="open ? 'true' : 'false'" aria-controls="console-collapse" :aria-expanded="open ? 'true' : 'false'" aria-controls="console-collapse"
header-tag="header" :header-bg-variant="open ? 'best' : 'white'"
:class="{ 'text-white': open }"
class="d-flex align-items-center"
@mousedown.left.prevent="onHistoryBarClick" @mousedown.left.prevent="onHistoryBarClick"
@keyup.enter.space.prevent="open = !open" @keyup.space.enter.prevent="onHistoryBarKey"
> >
<h6 class="m-0"> <h6 class="m-0">
<icon iname="history" /> {{ $t('history.title') }} <icon iname="history" /> <span class="d-none d-sm-inline">{{ $t('history.title') }}</span>
</h6> </h6>
<div class="ml-auto"> <!-- CURRENT/LAST ACTION -->
<!-- LAST ACTION --> <b-button
<small v-if="lastAction"> v-if="lastAction"
<u v-t="'history.last_action'" /> size="sm" pill
{{ lastAction.uri | readableUri }} ({{ $t('history.methods.' + lastAction.method) }}) class="ml-auto py-0"
</small> :variant="open ? 'light' : 'best'"
</div> @click.prevent="onLastActionClick"
</b-list-group-item> @keyup.enter.space.prevent="onLastActionClick"
>
<small>{{ $t('history.' + (lastAction.status === 'pending' ? 'current_action' : 'last_action')) }}</small>
</b-button>
<query-header v-if="lastAction" :action="lastAction" class="w-auto ml-2 xs-hide" />
</b-card-header>
<!-- ACTION LIST -->
<b-collapse id="console-collapse" v-model="open"> <b-collapse id="console-collapse" v-model="open">
<b-list-group-item <div
class="accordion" role="tablist"
id="history" ref="history" id="history" ref="history"
class="p-0" :class="{ 'show-last': openedByWaiting }" >
<!-- ACTION LIST -->
<b-card
v-for="(action, i) in history" :key="i"
no-body class="rounded-0 rounded-top border-left-0 border-right-0"
> >
<!-- ACTION --> <!-- ACTION -->
<b-list-group v-for="(action, i) in history" :key="i" flush> <b-card-header header-tag="header" header-bg-variant="white" class="sticky-top d-flex">
<!-- ACTION DESC --> <!-- ACTION DESC -->
<b-list-group-item class="sticky-top d-flex align-items-center"> <query-header
<div> role="tab" v-b-toggle="action.messages.length ? 'messages-collapse-' + i : false"
<strong>{{ $t('action') }}:</strong> :action="action" show-time show-error
{{ action.uri | readableUri }}
<small>({{ $t('history.methods.' + action.method) }})</small>
</div>
<time :datetime="action.date | hour" class="ml-auto">{{ action.date | hour }}</time>
</b-list-group-item>
<!-- ACTION MESSAGE -->
<b-list-group-item
v-for="({ type, text }, j) in action.messages" :key="j"
:variant="type" v-html="text"
/> />
</b-list-group> </b-card-header>
</b-list-group-item>
<!-- ACTION MESSAGES -->
<b-collapse
v-if="action.messages.length"
:id="'messages-collapse-' + i" accordion="my-accordion"
role="tabpanel"
@shown="scrollToAction(i)"
@hide="scrollToAction(i)"
>
<message-list-group :messages="action.messages" flush />
</b-collapse> </b-collapse>
</b-list-group> </b-card>
</div> </div>
</b-collapse>
</b-card>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import QueryHeader from '@/components/QueryHeader'
import MessageListGroup from '@/components/MessageListGroup'
export default { export default {
name: 'HistoryConsole', name: 'HistoryConsole',
components: {
QueryHeader,
MessageListGroup
},
props: { props: {
value: { type: Boolean, default: false }, value: { type: Boolean, default: false },
height: { type: [Number, String], default: 30 } height: { type: [Number, String], default: 30 }
@ -68,83 +84,71 @@ export default {
data () { data () {
return { return {
open: false, open: false
openedByWaiting: false
} }
}, },
watch: { computed: {
open (value) { ...mapGetters(['history', 'lastAction', 'waiting', 'error'])
// In case it is needed.
this.$emit('toggle', value)
if (value) {
// Wait for DOM update and scroll if needed.
this.$nextTick().then(this.scrollToLastAction)
}
}, },
'lastAction.messages' () {
if (!this.open) return
this.$nextTick(this.scrollToLastAction)
},
waiting (waiting) {
if (waiting && !this.open) {
// Open the history while waiting for the server's response to display WebSocket messages.
this.open = true
this.openedByWaiting = true
const history = this.$refs.history
this.$nextTick().then(() => {
history.style.height = ''
history.classList.add('with-max')
})
} else if (!waiting && this.openedByWaiting) {
// Automaticly close the history if it was not opened before the request
setTimeout(() => {
// Do not close it if the history was enlarged during the action
if (!history.style || history.style.height === '') {
this.open = false
}
this.openedByWaiting = false
}, 500)
}
}
},
computed: mapGetters(['history', 'lastAction', 'waiting']),
methods: { methods: {
scrollToLastAction () { scrollToAction (actionIndex) {
const historyElem = this.$refs.history const actionCard = this.$el.querySelector('#messages-collapse-' + actionIndex).parentElement
const lastActionGroup = historyElem.lastElementChild const headerOffset = actionCard.firstElementChild.offsetHeight
if (lastActionGroup) { // Can't use `scrollIntoView()` here since it will also scroll in the main content.
const lastItem = lastActionGroup.lastElementChild || lastActionGroup this.$refs.history.scrollTop = actionCard.offsetTop - headerOffset
historyElem.scrollTop = lastItem.offsetTop },
async onLastActionClick () {
if (!this.open) {
this.open = true
await this.$nextTick()
} }
const historyElem = this.$refs.history
const lastActionCard = historyElem.lastElementChild
const lastCollapsable = lastActionCard.querySelector('.collapse')
if (lastCollapsable && !lastCollapsable.classList.contains('show')) {
this.$root.$emit('bv::toggle::collapse', lastCollapsable.id)
// `scrollToAction` will be triggered and will handle the scrolling.
} else {
const headerOffset = lastActionCard.firstElementChild.offsetHeight
historyElem.scrollTop = lastActionCard.offsetTop - headerOffset
}
},
onHistoryBarKey (e) {
// FIXME interactive element in another is not valid, need to find another way.
if (e.target.nodeName === 'BUTTON' || e.target.parentElement.nodeName === 'BUTTON') return
this.open = !this.open
}, },
onHistoryBarClick (e) { onHistoryBarClick (e) {
const history = this.$refs.history // FIXME interactive element in another is not valid, need to find another way.
if (e.target.nodeName === 'BUTTON' || e.target.parentElement.nodeName === 'BUTTON') return
const historyElem = this.$refs.history
let mousePos = e.clientY let mousePos = e.clientY
const onMouseMove = ({ clientY }) => { const onMouseMove = ({ clientY }) => {
if (!this.open) { if (!this.open) {
history.style.height = '0px' historyElem.style.height = '0px'
this.open = true this.open = true
} }
const currentHeight = history.offsetHeight const currentHeight = historyElem.offsetHeight
const move = mousePos - clientY const move = mousePos - clientY
const nextSize = currentHeight + move const nextSize = currentHeight + move
if (nextSize < 10 && nextSize < currentHeight) { if (nextSize < 10 && nextSize < currentHeight) {
// Close the console and reset its size if the user reduce it to less than 10px. // Close the console and reset its size if the user reduce it to less than 10px.
mousePos = e.clientY mousePos = e.clientY
history.style.height = '' historyElem.style.height = ''
onMouseUp() onMouseUp()
} else { } else {
history.style.height = nextSize + 'px' historyElem.style.height = nextSize + 'px'
// Simulate scroll when reducing the box so the content doesn't move // Simulate scroll when reducing the box so the content doesn't move.
if (nextSize < currentHeight) { if (nextSize < currentHeight) {
history.scrollBy(0, -move) historyElem.scrollBy(0, -move)
} }
mousePos = clientY mousePos = clientY
} }
@ -152,17 +156,17 @@ export default {
// Delay the mouse move listener to distinguish a click from a drag. // Delay the mouse move listener to distinguish a click from a drag.
const listenToMouseMove = setTimeout(() => { const listenToMouseMove = setTimeout(() => {
history.style.height = history.offsetHeight + 'px' historyElem.style.height = historyElem.offsetHeight + 'px'
history.classList.remove('with-max') historyElem.classList.add('no-max')
window.addEventListener('mousemove', onMouseMove) window.addEventListener('mousemove', onMouseMove)
}, 200) }, 200)
const onMouseUp = () => { const onMouseUp = () => {
// Toggle opening if no mouse movement // Toggle opening if no mouse movement.
if (mousePos === e.clientY) { if (mousePos === e.clientY) {
// add a max-height class if the box's height is not custom // remove the free height class if the box's height is not custom
if (!history.style.height) { if (!historyElem.style.height) {
history.classList.add('with-max') historyElem.classList.remove('no-max')
} }
this.open = !this.open this.open = !this.open
} }
@ -173,65 +177,76 @@ export default {
window.addEventListener('mouseup', onMouseUp) window.addEventListener('mouseup', onMouseUp)
} }
},
filters: {
readableUri (uri) {
return uri.split('?')[0].replace('/', ' > ')
},
hour (date) {
return new Date(date).toLocaleTimeString()
}
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// reset default style
.card + .card {
margin-top: 0;
}
.card-header {
padding: $tooltip-padding-y $tooltip-padding-x;
}
#console { #console {
position: sticky; position: sticky;
z-index: 15; z-index: 15;
bottom: 0; bottom: 0;
margin-left: -1.5rem;
width: calc(100% + 3rem); width: calc(100% + 3rem);
margin-left: -1.5rem;
border-bottom: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
font-size: $font-size-sm;
.btn {
height: 1.25rem;
display: flex;
align-items: center;
}
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
margin-left: -15px; margin-left: -15px;
width: calc(100% + 30px); width: calc(100% + 30px);
& > .list-group { & > .card-header {
border-radius: 0; border-radius: 0;
} }
} }
} }
#console-collapse { // Hacky disable of collapse animation
// disable collapse animation .collapsing {
transition: none !important; transition: none !important;
height: auto !important;
display: block !important;
position: static !important;
} }
#history { #history {
overflow-y: auto; overflow-y: auto;
max-height: 20vh;
&.with-max { &.no-max {
max-height: 30vh; max-height: none;
} }
// Used to display only the last message of the last action while an action is triggered > .card {
// and console was not opened. // reset bootstrap's `overflow: hidden` that prevent sticky headers to work properly.
&.with-max.show-last { overflow: visible;
& > :not(:last-child) {
display: none; &:first-of-type {
} // hide first top border that conflicts with the console header's bottom border.
& > :last-child > :not(:last-child) { margin-top: -1px;
display: none;
}
} }
} }
.list-group-item { [aria-controls] {
font-size: $font-size-sm; cursor: pointer;
padding: $tooltip-padding-y $tooltip-padding-x; }
} }
</style> </style>