Commit 65430005 authored by Christian Hansen's avatar Christian Hansen Committed by Anton Koch

Add SwimLane component

parent 5da7f55f
......@@ -74,7 +74,7 @@ docker images.
### Versions
* Stable: `motionbank/systems-frontend:release_1_3`
* Stable: `motionbank/systems-frontend:release_1_2`
* Staging (beta): `motionbank/systems-frontend:staging`
* Experimental: `motionbank/systems-frontend:experimental`
......
......@@ -27,7 +27,7 @@ module.exports = function (ctx) {
ctx.theme.mat ? 'roboto-font' : null,
'material-icons'
],
supportIE: false,
supportIE: true,
build: {
scopeHoisting: true,
vueRouterMode: 'history',
......@@ -104,6 +104,7 @@ module.exports = function (ctx) {
// Buttons
//
'QBtn',
'QBtnToggle',
'QBtnDropdown',
//
// Navigation
......@@ -178,6 +179,7 @@ module.exports = function (ctx) {
'Ripple',
'Scroll',
'TouchHold',
'TouchPan',
'CloseOverlay'
],
// Quasar plugins
......
<template lang="pug">
// TOP CENTER
//
//
.row.q-mt-md(v-shortkey="shortcuts.focusInput", @shortkey="setFocusOnInput()")
.row.q-mt-md.q-pl-sm.round-borders#hover(v-shortkey="shortcuts.focusInput", @shortkey="setFocusOnInput()")
// q-btn.fixed-bottom-left.q-ma-md(color="primary", outline) alt
// button toggles vocabulary
// BUTTON - SWITCH BETWEEN TEXT INPUT AND VOCABULARY
q-btn.text-primary.q-mr-sm.q-mt-sm(v-if="!vocabularyVisible && staging", round, icon="local_offer",
v-shortkey="shortcuts.showVocabulary", @shortkey.native="toggleVocabulary()", @click="toggleVocabulary()")
.col-xs-2.offset-xs-1.col-md-1.offset-md-1.col-lg-1.offset-lg-2.text-right.q-pa-sm.q-pr-md
q-btn.text-primary.bg-grey-10(v-if="!vocabularyVisible && staging", round, icon="local_offer",
v-shortkey="shortcuts.showVocabulary", @shortkey.native="toggleVocabulary()", @click="toggleVocabulary()")
// input area
.col-xs-8.col-md-8.col-lg-6.bg-grey-10.relative-position(:class="{ 'shadow-4': vocabularyVisible }")
// TEXT INPUT
div.round-borders(:class="{ 'shadow-4': vocabularyVisible }", style="width: 40vw;")
q-input.q-pa-md(v-on:keydown="onKeyDown", @focus="onInputFocus", @blur="onInputBlur",
v-model="annotationText", ref="textInput", type="textarea", autofocus, dark,
:class="[vocabularyVisible ? 'q-pl-xl text-primary' : 'text-white']")
v-model="annotationText", ref="textInput", type="textarea", autofocus, dark
:class="[vocabularyVisible ? 'q-pl-xl text-primary' : 'text-white']"
)
// CLOSE BUTTON
// CLOSE BUTTON (unused?)
.absolute-top.q-mt-sm(v-if="staging", style="width: 3rem;")
q-btn.q-ml-sm.q-mt-xs.q-mr-none.text-primary(round, flat, icon="clear", size="sm",
v-if="vocabularyVisible", v-shortkey="shortcuts.showVocabulary",
@shortkey.native="toggleVocabulary()", @click="toggleVocabulary()")
// VOCABULARY (staging)
// vocabularies
div(v-if="staging")
vocabulary(
......@@ -199,3 +194,9 @@
}
}
</script>
<style lang="stylus">
@import '~variables'
#hover:hover
background-color $dark
</style>
......@@ -4,14 +4,9 @@
width="100%", height="100%"
)
// Graph
svg.sl-graph(
@mousewheel="onGraphMouseWheel ($event)",
// :width="width", height="100%"
// :x="x", y="0"
)
svg.sl-graph(
@mousewheel="onGraphMouseWheel ($event)",
width="100%", height="100%",
:width="width", height="100%",
:x="x", y="0"
)
// Graph Background
......
......@@ -11,7 +11,7 @@
</template>
<script>
import { EventHub } from '../SwimLane/EventHub'
import { EventHub } from './EventHub'
// import { DateTime } from 'luxon'
export default {
......
......@@ -11,7 +11,7 @@
</template>
<script>
import { EventHub } from '../SwimLane/EventHub'
import { EventHub } from './EventHub'
import { DateTime } from 'luxon'
export default {
......
<template lang="pug">
// q-list.sl-marker-details-selected(color="dark")
q-list(color="dark")
//
div
// previous or next entry
.q-mb-sm
q-icon(name="keyboard_arrow_left")
q-icon(name="keyboard_arrow_right")
q-list.q-pa-none(color="dark", no-border)
template(v-if="annotationData")
q-item( v-for="(value, key) in annotationData" )
q-item-side {{ key }}
q-item-main {{ value }}
q-item.q-pa-none.items-start( v-for="(value, key) in annotationData" )
q-item-side.q-pa-none(:class="{'q-captionXXXXXXXXXX': resizable}") {{ key }}
q-item-main.q-pa-none(:class="{'q-captionXXXXXXXXXX': resizable}") {{ value }}
template(v-else)
q-item.q-pa-none.items-start
q-item-side.q-pa-none(:class="{'q-captionXXXXXXXXXX': resizable}") no selection
</template>
<script>
import { EventHub } from '../SwimLane/EventHub'
import { EventHub } from './EventHub'
import { DateTime } from 'luxon'
export default {
props: ['root'],
props: ['root', 'resizable'],
data () {
return {
annotationData: null
......@@ -24,7 +33,7 @@
}
},
async mounted () {
EventHub.$on('markerUnselect', this.onMarkerUnselect)
// EventHub.$on('markerUnselect', this.onMarkerUnselect)
EventHub.$on('markerDown', this.onMarkerDown)
},
beforeDestroy () {
......@@ -59,4 +68,7 @@
.q-item-side
width: 100px
.q-list-header
min-height none!important
</style>
......@@ -65,7 +65,6 @@
return this.root.toAbsComp(this.scrollPosition.x)
},
navHandleWidth () {
// return this.root.toAbsComp(this.scaleFactor)
return this.root.toAbsComp(this.scaleFactor)
},
timecodeCurrentX () {
......@@ -112,6 +111,7 @@
sp = this.navHandleX
// scaleFactor
min = this.root.toAbsComp(this.root.scaleFactorMin)
// max = this.root.el.width - this.navHandleX
max = this.root.el.width - this.navHandleX
raw = this.root.inputPosition.x - this.navHandleX
w = this.root.restrict(raw, min, max)
......
<template lang="pug">
//.settings
.row
div
<!--| Group annotations by:&nbsp;-->
<!--q-tooltip.bg-grey-9 Group annotations by-->
<!--q-popover(@mouseover.native="toggleSettingsPopover()")-->
// q-list
// q-item(v-for="(g, i) in groupAnnotationsBy") {{ groupAnnotationsBy[i] }}
//
q-btn-toggle.bg-grey-8.q-ma-xs(
v-model="groupAnnotationsBy",
toggle-color="primary",
// :options="options",
size="sm"
)
q-btn-dropdown.q-mt-xs.bg-grey-9(:label="groupAnnotationsBy", size="sm", flat)
q-list.q-py-none
q-item.cursor-pointer(v-for="o in options", :key="o.value",
@click.native="groupAnnotationsBy = o.value", v-close-overlay) {{ o.label }}
<!--q-btn.q-ma-none(v-for="o in options", @click="groupAnnotationsBy = o.value", flat, size="sm") {{ o.label }}-->
div.q-pl-sm
| Group annotations by:&nbsp;
q-btn-toggle.bg-grey-8.q-ma-xs(
v-model="groupAnnotationsBy",
toggle-color="primary",
:options="options",
size="sm", rounded
)
div.q-pl-sm.q-ml-md
| Lane mode:&nbsp;
q-btn-toggle.bg-grey-8.q-ma-xs(
v-model="laneMode",
toggle-color="primary",
:options="optionsLaneMode",
size="sm", rounded
)
<!--| Lane mode:&nbsp;-->
//
q-tooltip.bg-grey-9 Lane mode
q-btn-toggle.bg-grey-8.q-ma-xs(
v-model="laneMode",
toggle-color="primary",
// :options="optionsLaneMode",
size="sm"
)
q-btn-dropdown.q-mt-xs.bg-grey-9(:label="laneMode", size="sm", flat)
q-list.q-py-none
q-item.cursor-pointer(v-for="o in optionsLaneMode", :key="o.value",
@click.native="laneMode = o.value", v-close-overlay) {{ o.label }}
</template>
<script>
......
<template lang="pug">
.swim-lane-component(:class="[cursorGlobalResize, cursorGlobalGrabbing]", style="position: relative;")
div
q-btn.q-mr-sm(slot="", @click="createMarker", label="Add annotation", color="primary")
q-btn.q-mr-sm(slot="", @click="", label="Jump to selected annotation", color="primary")
q-btn.q-mr-sm(slot="", @click="", label="Jump to current timecode", color="primary")
.row.q-my-md
//
q-btn.q-mr-sm(slot="", @click="createMarker", label="Add annotation", color="primary")
q-btn.q-mr-sm(slot="", @click="", label="Jump to selected annotation", color="primary")
q-btn.q-mr-sm(slot="", @click="", label="Jump to current timecode", color="primary")
.float-right(v-if="resizable")
q-btn.q-ml-lg.q-px-sm(@click="", v-touch-pan="handlerResize", color="dark")
q-icon.rotate-90(name="code")
q-btn.q-ml-sm.q-px-sm(@click="handlerToggle('swimlanes')", color="dark", icon="clear")
<!--q-btn.float-right(@click="handlerToggle('markerDetails')", :label="[markerDetails ? 'hide details' : 'show details']", color="primary")-->
q-btn.float-right(@click="handlerToggle('markerDetails')", label="details", color="primary")
// TODO: use input field here to set the timecode to an exact value
<!--q-input.timecode-display-current.no-select(v-model="timecode.currentText")-->
<!--.timecode-display-current.no-select Selected timecode: {{timecode.currentText}}-->
marker-context-menu(:root="self")
marker-details-selected.q-mt-md(v-if="markerDetails", :root="self")
marker-details-hover(:root="self")
.row.justify-between.q-my-md
div.q-pt-xs
| Selected timecode: {{ timecode.currentText }}
div
//.row.justify-between.q-my-md
.col-8.row.gutter-sm
.q-pt-xs(:class="{'col-6' : showDetails}")
q-btn.q-px-sm(@click="handlerToggle('markerDetails')", icon="expand_more",
:class="[showDetails ? 'rotate-90' : 'rotate-270 text-white']", size="sm", round)
// TODO: use input field here to set the timecode to an exact value
<!--| Selected timecode: {{ timecode.currentText }}-->
.col-6
settings(ref="settings")
.swim-lane-wrapper.wrapper
.timecode-display-hover.no-select.no-event.p-abs(
ref="timecodeDisplayHover",
:class="(isFocused('timecodeBar') && !isDragged()) || isDragged('timecodeBar') ? '' : 'is-hidden'",
:style="{left: timecodeBar.displayHover.x + 'px'}"
) {{timecode.hoverText}}
// ----------------------------------------------------------------------------------------------------- Outer SVG
svg.swim-lane(
@mousedown.left.prevent,
width="100%", height="50vh",
ref="root"
)
graph(
ref="graph",
:annotationsGrouped="annotationsGrouped",
:root="self"
)
timecode-bar(
ref="timecodeBar",
:root="self"
)
// TODO: own component
line.sl-graph-timecode-current.stroke-neutral.no-event(
:x1="timecodeMarkerCurrentX", y1="0",
:x2="timecodeMarkerCurrentX", y2="100%"
)
navigation-bar(
ref="nav",
:root="self"
)
// resize and hide swimlanes
.col-4.text-right(v-if="resizable")
q-btn.q-ml-lg.q-px-sm(@click="", v-touch-pan="handlerResize", color="dark", round, size="sm")
q-icon.rotate-90(name="code")
q-btn.q-ml-sm.q-px-sm(@click="handlerToggle('swimlanes')", color="dark", icon="clear", round, size="sm")
.row(:class="[showDetails ? 'gutter-sm' : '']")
div(:class="[showDetails ? 'col-4' : '']")
marker-details-selected(v-if="showDetails", :root="self", :resizable="resizable")
<!--div(ref="swimlanewrap", :class="[showMarkerDetails ? 'col-8' : 'col-12']")-->
div(ref="swimlanewrap", :class="[showDetails ? 'col-8' : 'col-12']")
.swim-lane-wrapper.wrapper
.timecode-display-hover.no-select.no-event.p-abs(
ref="timecodeDisplayHover",
:class="(isFocused('timecodeBar') && !isDragged()) || isDragged('timecodeBar') ? '' : 'is-hidden'",
:style="{left: timecodeBar.displayHover.x + 'px'}"
) {{ timecode.hoverText }}
// ----------------------------------------------------------------------------------------------------- Outer SVG
svg.swim-lane(
@mousedown.left.prevent,
width="100%", height="50vh",
ref="root"
)
// swimlanes
graph(
ref="graph",
:annotationsGrouped="annotationsGrouped",
:root="self",
:offset="offset"
)
// sections bar
timecode-bar(
ref="timecodeBar",
:root="self",
:offset="offset"
)
// TODO: own component
line.sl-graph-timecode-current.stroke-neutral.no-event(
:x1="timecodeMarkerCurrentX", y1="0",
:x2="timecodeMarkerCurrentX", y2="100%"
)
// scroll and zoom bar
navigation-bar(
ref="nav",
:root="self",
:offset="offset"
)
</template>
<script>
import { mapGetters } from 'vuex'
import { DateTime } from 'luxon'
import { EventHub } from '../SwimLane/EventHub'
import { EventHub } from './EventHub'
import SwimLaneMarker from './Graph/GraphMarker'
import NavigationBar from './NavigationBar/NavigationBar'
import TimecodeBar from './TimecodeBar/TimecodeBar'
......@@ -80,7 +94,7 @@
MarkerDetailsSelected,
MarkerContextMenu
},
props: ['timelineUuid', 'markerDetails', 'resizable'],
props: ['timelineUuid', 'markerDetails', 'resizable', 'visibilityDetails'],
data () {
return {
self: this,
......@@ -115,7 +129,13 @@
currentKeyDown: null,
annotations: [],
// store all marker for collision detection later on
markerList: []
markerList: [],
showMarkerDetails: undefined,
offset: {
gutter: undefined,
swimlanewrap: undefined
},
showDetails: this.visibilityDetails
}
},
async mounted () {
......@@ -204,14 +224,19 @@
}
},
methods: {
// toggleDetails () {
// this.$emit('toggleDetails', 'bla')
// },
handlerResize (obj) {
console.log(obj.position.top)
// console.log(obj.position.top)
this.$emit('emitResize', obj.position.top)
},
handlerToggle (val) {
switch (val) {
case 'markerDetails':
this.markerDetails = !this.markerDetails
this.showMarkerDetails = !this.showMarkerDetails
if (!this.resizable) this.showDetails = this.showMarkerDetails
this.$emit('emitToggleDetails', this.showMarkerDetails)
break
case 'swimlanes':
this.$emit('emitHandler')
......@@ -285,8 +310,18 @@
// TODO: make own component
let el = this.$refs.timecodeDisplayHover
// if (this.showDetails) {
// this.offset.gutter = 16
// }
// else this.offset.gutter = 0
// this.offset.swimlanewrap = this.$refs.swimlanewrap.offsetLeft
if (el) {
this.timecodeBar.displayHover.x = this.restrict(this.inputPosition.x, 0, this.el.width - el.clientWidth)
this.timecodeBar.displayHover.x = this.restrict(
this.inputPosition.x,
0,
this.el.width - el.clientWidth)
}
},
// ---------------------------------------------------------------------------------------------------- E Input Up
......@@ -362,6 +397,7 @@
},
// ----------------------------------------------------------------------------------------------------------- Set
setScrollPosition (sp) { // 0 - 1
// let offset = this.offset.swimlanewrap + this.offset.gutter
let x = this.restrict(sp, 0, this.toRelComp(this.el.width - this.$refs.nav.navHandleWidth))
let y = 0
this.$store.commit('swimLaneSettings/setScrollPosition', x, y)
......@@ -470,7 +506,9 @@
},
// ---------------------------------------------------------------------------------------------------- Conversion
toAbsComp (rel) {
// let offset = this.offset.swimlanewrap + this.offset.gutter
return rel * this.el.width
// return rel * this.$refs.swimlanewrap.clientWidth
},
toAbsGraph (rel) {
if (this.$refs.graph) return rel * this.$refs.graph.width
......
......@@ -8,6 +8,8 @@
)
rect.fill-medium(width="100%", height="100%")
timecode-bar-section(v-for="(n, index) in numSections", :index="index", :numSections="numSections", :root="root")
// popping up rectangle
svg.timecode-pointer.no-event(
v-if="(root.isFocused('timecodeBar') && !root.isDragged()) || root.isDragged('timecodeBar')",
:x="timecodeMarkerHover.x", y="0"
......@@ -21,7 +23,7 @@
import { mapGetters } from 'vuex'
export default {
props: ['root'],
props: ['root', 'offset'],
components: {
TimecodeBarSection
},
......
Subproject commit 125782eae270c5db4a3d249be1f7de727d62747d
Subproject commit 3fefc736a2bb2e6e905034414acdfdea8e24d4a3
import { ObjectUtil } from 'mbjs-utils'
class ACLHelpers {
static async setACL (context, action, payload, recursive = false) {
await context.$store.dispatch(action, payload)
if (recursive) {
const results = await context.$store.dispatch('annotations/find', { 'target.id': payload.id })
for (let item of results.items) {
if (item.author.id === context.$store.state.auth.user.uuid) {
const itemPayload = ObjectUtil.merge({}, payload)
itemPayload.uuid = item.uuid
await context.$store.dispatch(action, itemPayload)
}
}
}
}
static async updateACL (context) {
console.debug('setting acl...', context.acl)
if (this.acl.public) {
await this.setACL('acl/set', { role: 'public', uuid: context.timeline.uuid, permissions: ['get'] }, context.acl.recursive)
}
else {
await this.setACL('acl/remove', { role: 'public', uuid: context.timeline.uuid, permission: 'get' }, context.acl.recursive)
}
if (this.acl.group) {
await this.setACL('acl/set', { role: context.acl.group, uuid: context.timeline.uuid, permissions: ['get'] }, context.acl.recursive)
}
if (this.acl.group_remove) {
await this.setACL('acl/remove', { role: context.acl.group_remove, uuid: context.timeline.uuid, permission: 'get' }, context.acl.recursive)
}
this.$store.commit('notifications/addMessage', {
body: 'messages.acl_updated',
type: 'success'
})
}
}
export default ACLHelpers
......@@ -16,7 +16,8 @@
// TOP CENTER: INPUT AREA
//
//
.fixed-top.bg-dark.q-pb-md(style="top: 50px; width: 100%; z-index: 1000;")
<!--.fixed-top.bg-dark.q-pb-md(style="top: 50px; width: 100%; z-index: 1000;")-->
q-page-sticky(position="top")
annotation-field(@annotation="onAnnotation", ref="annotationField", :submit-on-num-enters="2")
// CENTER: SHOW ANNOTATIONS
......
import create from './create'
import edit from './edit'
import list from './list'
import show from './show'
import show from './sessions'
import annotate from './annotate'
import user from './user'
......
<template lang="pug">
full-screen
// headline
//
.row.q-mb-xl(v-if="map")
.col-10.offset-1(slot="form-title")
h5.no-margin.text-center
div {{ map.title }}
.text-grey-8 {{ map.author.name }}
// btn: back
q-btn.absolute-top-left(slot="backButton", @click="$router.push({ name: 'piecemaker.timelines.list' })", icon="keyboard_backspace", round, small, style="top: 66px; left: 16px;")
swim-lane(v-if="map", :timelineUuid="map.uuid", :markerDetails="true",
@emitToggleDetails="onToggleDetails", :visibilityDetails="visibilityDetails", :key="visibilityDetails")
</template>
<script>
import SwimLane from '../../../components/piecemaker/partials/SwimLane/SwimLane'
export default {
components: {
SwimLane
},
async mounted () {
this.map = await this.$store.dispatch('maps/get', this.$route.params.id)
},
data () {
return {
map: undefined,
visibilityDetails: undefined
}
},
methods: {
onToggleDetails (val) {
console.log(val)
this.visibilityDetails = val
}
}
}
</script>
......@@ -4,58 +4,58 @@
//
//
.bg-dark(style="height: calc(100vh - 52px); overflow: hidden;")
q-window-resize-observable(@resize="onResize")
confirm-modal(ref="confirmModal", @confirm="handleConfirmModal")
.bg-dark.relative-position(style="height: calc(100vh - 52px);")
// VIDEO
//
//
video-player(v-if="video && annotations", :annotation="video", :fine-controls="true",
@ready="playerReady($event)", @time="onPlayerTime($event)", @canplay.once="gotoHashvalue")
// TOP LEFT
//
//
q-page-sticky.q-pa-md(position="top-left", style="z-index: 2100;")
// BUTTON: GO BACK
back-button
// TOP RIGHT
//
//
q-page-sticky.q-pa-md(position="top-right", style="z-index: 2100;")
// BUTTONS: SWITCH TO FULLSCREEN
// q-btn(v-if="!fullscreen", @click="toggleFullscreen()", icon="fullscreen", round)
// q-btn(v-if="fullscreen", @click="toggleFullscreen()", icon="fullscreen_exit", round)
// BUTTONS: SHOW/HIDE ANNOTATIONS
q-btn.q-ml-xs.bg-dark(v-if="metadata", color="white", round, flat, icon="info")
q-popover.bg-dark.q-py-sm
q-item(multiline)
q-item-side Title:
q-item-main {{ metadata.title }}
q-btn.q-ml-xs.bg-dark(v-if="!drawer", @click="drawer = true", color="white", round, flat)
q-icon(name="fullscreen_exit")
q-btn.q-ml-xs.bg-dark(v-else, @click="drawer = false", color="white", round, flat)
q-icon.flip-horizontal(name="fullscreen")
// TOP CENTER
//
//
.absolute-top(style="width: 100%;")
// video player
div.bg-red(:style="{width: videoWidth}")
video-player(v-if="video", :annotation="video", :fine-controls="true",
@ready="playerReady($event)", @time="onPlayerTime($event)")
// back button
q-page-sticky(position="top-left", style="z-index: 2100;")
back-button.q-ma-md
// button toggles annotations
q-page-sticky(position="top-right", style="z-index: 2100;")
q-btn.q-ma-md(@click="handlerToggle('annotations')", color="dark", round,
:class="[drawer ? 'rotate-180' : '']", icon="keyboard_backspace", size="sm")
// swimlane content
.absolute-bottom-right.bg-dark.full-width.shadow-up-4.q-px-md.q-pb-sm.scroll(v-if="swimlanes",
:style="{height: swimlanesHeight + 'px', borderTop: '1px solid #333'}")
swim-lane(v-if="timeline", :timelineUuid="timeline.uuid", :markerDetails="false", :resizable="true",
@emitHandler="handlerToggle('swimlanes')", @emitResize="onEmitResize",
:key="visibilityDetails", @emitToggleDetails="onToggleDetails", :visibilityDetails="visibilityDetails"
)
q-page-sticky.q-pa-md(position="bottom-right")
// button toggles swimlanes visibility
q-btn(v-if="!swimlanes && userHasSwimlane", @click="handlerToggle('swimlanes')", color="dark", round,
:class="[swimlanes ? 'rotate-270' : 'rotate-90']", icon="keyboard_backspace", size="sm")
// input field for new annotations
<!--.absolute-top(style="width: 100%;")-->
q-page-sticky(position="top")
annotation-field(
@annotation="onAnnotation",
ref="annotationField",
:submit-on-num-enters="1",