mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
adding an updateHack in store, and a watcher in component. * updateHack increment a value in the lpop, * the watcher detect when value changes * and $forceUpdate improve layer checkbox legend refresh and rebuild
474 lines
17 KiB
Vue
474 lines
17 KiB
Vue
<template>
|
|
|
|
<div id="visgraph"></div>
|
|
|
|
<teleport to="#visgraph-legend">
|
|
<div class="post-menu">
|
|
<div class="list-group mt-4">
|
|
<button type="button" class="list-group-item list-group-item-action btn btn-create" @click="createRelationship">
|
|
{{ $t('visgraph.add_link') }}
|
|
</button>
|
|
<button type="button" class="list-group-item list-group-item-action btn btn-misc">
|
|
<i class="fa fa-camera fa-fw"></i> {{ $t('visgraph.screenshot') }}
|
|
</button>
|
|
<button type="button" class="list-group-item list-group-item-action btn btn-light" @click="refreshNetwork">
|
|
<i class="fa fa-refresh fa-fw"></i> {{ $t('visgraph.refresh') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="displayHelpMessage" class="alert alert-info mt-3">
|
|
{{ $t('visgraph.create_link_help') }}
|
|
</div>
|
|
|
|
<div class="my-4 legend">
|
|
<h3>{{ $t('visgraph.Legend') }}</h3>
|
|
<div class="list-group">
|
|
<label class="list-group-item" v-for="layer in legendLayers">
|
|
<input
|
|
class="form-check-input me-1"
|
|
type="checkbox"
|
|
:value="layer.id"
|
|
v-model="checkedLayers"
|
|
@change="toggleLayer"
|
|
/>
|
|
{{ layer.label }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</teleport>
|
|
|
|
<teleport to="body">
|
|
<modal v-if="modal.showModal" :modalDialogClass="modal.modalDialogClass" @close="modal.showModal = false">
|
|
<template v-slot:header>
|
|
<h2 class="modal-title">{{ $t(modal.title) }}</h2>
|
|
<!-- {{ modal.data.id }} -->
|
|
</template>
|
|
<template v-slot:body>
|
|
<div v-if="modal.action === 'delete'">
|
|
<p>{{ $t('visgraph.delete_confirmation_text') }}</p>
|
|
</div>
|
|
<div v-else>
|
|
<form>
|
|
<div class="row">
|
|
<div class="col-12 text-center">{{ $t('visgraph.between') }}<br>{{ $t('visgraph.and') }}</div>
|
|
<div class="col">
|
|
<h4>{{ getPerson(modal.data.from).text }}</h4>
|
|
<p class="text-start" v-if="relation && relation.title">
|
|
<span v-if="reverse">
|
|
{{ $t('visgraph.relation_from_to_like', [ getPerson(modal.data.from).text, getPerson(modal.data.to).text, relation.reverseTitle.fr.toLowerCase() ])}}
|
|
</span>
|
|
<span v-else>
|
|
{{ $t('visgraph.relation_from_to_like', [ getPerson(modal.data.from).text, getPerson(modal.data.to).text, relation.title.fr.toLowerCase() ])}}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<div class="col text-end">
|
|
<h4>{{ getPerson(modal.data.to).text }}</h4>
|
|
<p class="text-end" v-if="relation && relation.title">
|
|
<span v-if="reverse">
|
|
{{ $t('visgraph.relation_from_to_like', [ getPerson(modal.data.to).text, getPerson(modal.data.from).text, relation.title.fr.toLowerCase() ])}}
|
|
</span>
|
|
<span v-else>
|
|
{{ $t('visgraph.relation_from_to_like', [ getPerson(modal.data.to).text, getPerson(modal.data.from).text, relation.reverseTitle.fr.toLowerCase() ])}}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="my-3">
|
|
<VueMultiselect
|
|
id="relation"
|
|
label="title"
|
|
track-by="id"
|
|
:custom-label="customLabel"
|
|
:placeholder="$t('visgraph.choose_relation')"
|
|
:close-on-select="true"
|
|
:multiple="false"
|
|
:searchable="true"
|
|
:options="relations"
|
|
v-model="relation"
|
|
:value="relation"
|
|
>
|
|
</VueMultiselect>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input
|
|
class="form-check-input"
|
|
type="checkbox"
|
|
id="reverse"
|
|
v-model="reverse"
|
|
>
|
|
<label class="form-check-label" for="reverse">{{ $t('visgraph.reverse_relation') }}</label>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</template>
|
|
<template v-slot:footer>
|
|
<button class="btn" :class="modal.button.class" @click="submitRelationship">
|
|
{{ $t(modal.button.text)}}
|
|
</button>
|
|
<button class="btn btn-delete" v-if="modal.action === 'edit'" @click="dropRelationship"></button>
|
|
</template>
|
|
</modal>
|
|
</teleport>
|
|
|
|
</template>
|
|
|
|
<script>
|
|
import vis from 'vis-network/dist/vis-network'
|
|
import { mapState, mapGetters } from "vuex"
|
|
import Modal from 'ChillMainAssets/vuejs/_components/Modal'
|
|
import VueMultiselect from 'vue-multiselect'
|
|
import { getRelationsList, postRelationship, patchRelationship, deleteRelationship } from "./api";
|
|
import { splitId } from "./vis-network";
|
|
|
|
export default {
|
|
name: "App",
|
|
components: {
|
|
Modal,
|
|
VueMultiselect
|
|
},
|
|
data() {
|
|
return {
|
|
container: '',
|
|
checkedLayers: [],
|
|
relations: [],
|
|
displayHelpMessage: false,
|
|
listenPersonFlag: 'normal',
|
|
newEdgeData: {},
|
|
modal: {
|
|
showModal: false,
|
|
modalDialogClass: "modal-md",
|
|
title: null,
|
|
action: null,
|
|
data: {
|
|
type: 'relationship',
|
|
from: null,
|
|
to: null,
|
|
relation: null,
|
|
reverse: false
|
|
},
|
|
button: {
|
|
class: null,
|
|
text: null
|
|
},
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
...mapGetters(['nodes', 'edges',
|
|
// not used 'isInWhitelist', 'isHouseholdLoading', 'isCourseLoaded', 'isRelationshipLoaded', 'isPersonLoaded', 'isExcludedNode', 'countLinksByNode', 'getParticipationsByCourse', 'getMembersByHousehold', 'getPersonsGroup',
|
|
]),
|
|
...mapState(['persons', 'households', 'courses', 'excludedNodesIds', 'updateHack',
|
|
// not used 'links', 'relationships', 'whitelistIds', 'personLoadedIds', 'householdLoadingIds', 'courseLoadedIds', 'relationshipLoadedIds',
|
|
]),
|
|
|
|
visgraph_data() {
|
|
console.log('::: visgraph_data :::', this.nodes.length, 'nodes,', this.edges.length, 'edges')
|
|
return {
|
|
nodes: this.nodes,
|
|
edges: this.edges
|
|
}
|
|
},
|
|
|
|
refreshNetwork() {
|
|
console.log('--- refresh network')
|
|
window.network.setData(this.visgraph_data)
|
|
},
|
|
|
|
legendLayers() {
|
|
console.log('--- refresh legend and rebuild checked Layers')
|
|
this.checkedLayers = []
|
|
let layersDisplayed = [
|
|
...this.nodes.filter(n => n.id.startsWith('household')),
|
|
...this.nodes.filter(n => n.id.startsWith('accompanying'))
|
|
]
|
|
layersDisplayed.forEach(layer => {
|
|
this.checkedLayers.push(layer.id)
|
|
})
|
|
return [
|
|
...this.households,
|
|
...this.courses
|
|
]
|
|
},
|
|
|
|
checkedLayers() { // required to refresh data checkedLayers
|
|
console.log('--- checkedLayers')
|
|
return this.checkedLayers
|
|
},
|
|
|
|
relation: {
|
|
get() {
|
|
return this.modal.data.relation
|
|
},
|
|
set(value) {
|
|
this.modal.data.relation = value
|
|
}
|
|
},
|
|
|
|
reverse: {
|
|
get() {
|
|
return this.modal.data.reverse
|
|
},
|
|
set(value) {
|
|
this.modal.data.reverse = value
|
|
}
|
|
},
|
|
|
|
},
|
|
watch: {
|
|
updateHack(newValue, oldValue) {
|
|
console.log(`--- updateHack ${oldValue} <> ${newValue}`)
|
|
if (oldValue !== newValue) {
|
|
this.forceUpdateComponent()
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
//console.log('=== mounted: init graph')
|
|
this.initGraph()
|
|
this.listenOnGraph()
|
|
this.getRelationsList()
|
|
},
|
|
methods: {
|
|
forceUpdateComponent() {
|
|
//console.log('!! forceUpdateComponent !!')
|
|
this.refreshNetwork
|
|
this.$forceUpdate()
|
|
},
|
|
|
|
initGraph() {
|
|
this.container = document.getElementById('visgraph')
|
|
// Instanciate vis objects in separate window variables, see vis-network.js
|
|
window.network = new vis.Network(this.container, this.visgraph_data, window.options)
|
|
},
|
|
|
|
// events
|
|
listenOnGraph() {
|
|
window.network.on('selectNode', (data) => {
|
|
if (data.nodes.length > 1) {
|
|
throw 'Multi selection is not allowed. Disable it in options.interaction !'
|
|
}
|
|
let node = data.nodes[0]
|
|
let nodeType = splitId(node, 'type')
|
|
switch (nodeType) {
|
|
|
|
case 'person':
|
|
let person = this.nodes.filter(n => n.id === node)[0]
|
|
console.log('@@@@@@ event on selected Node', person.id)
|
|
if (this.listenPersonFlag === 'normal') {
|
|
if (person.folded === true) {
|
|
console.log(' @@> expand mode event')
|
|
this.$store.commit('unfoldPerson', person)
|
|
}
|
|
} else {
|
|
console.log(' @@> create link mode event')
|
|
this.listenStepsToAddRelationship(person)
|
|
}
|
|
break
|
|
|
|
case 'household':
|
|
let household = this.nodes.filter(n => n.id === node)[0]
|
|
console.log('@@@@@@ event on selected Node', household.id)
|
|
this.$store.dispatch('unfoldPersonsByHousehold', household)
|
|
break
|
|
|
|
case 'accompanying_period':
|
|
let course = this.nodes.filter(n => n.id === node)[0]
|
|
console.log('@@@@@@ event on selected Node', course.id)
|
|
this.$store.dispatch('unfoldPersonsByCourse', course)
|
|
break
|
|
|
|
default:
|
|
throw 'event is undefined for this type of node'
|
|
}
|
|
this.forceUpdateComponent()
|
|
})
|
|
window.network.on('selectEdge', (data) => {
|
|
if (data.nodes.length !== 0 || data.edges.length !== 1) {
|
|
return false //we don't want to trigger nodeEdge or multiselect !
|
|
}
|
|
let link = data.edges[0]
|
|
let linkType = splitId(link, 'link')
|
|
console.log('@@@@@ event on selected Edge', data.edges.length, linkType, data)
|
|
|
|
if (linkType.startsWith('relationship')) {
|
|
//console.log('linkType relationship')
|
|
|
|
let relationships = this.edges.filter(l => l.id === link)
|
|
if (relationships.length > 1) {
|
|
throw 'error: only one link is allowed between two person!'
|
|
}
|
|
|
|
let relationship = relationships[0]
|
|
//console.log(relationship)
|
|
|
|
this.editRelationshipModal({
|
|
from: relationship.from,
|
|
to: relationship.to,
|
|
id: relationship.id,
|
|
relation: relationship.relation,
|
|
reverse: relationship.reverse
|
|
})
|
|
}
|
|
})
|
|
},
|
|
listenStepsToAddRelationship(person) {
|
|
console.log(' @@> listenStep', this.listenPersonFlag)
|
|
if (this.listenPersonFlag === 'step2') {
|
|
//console.log(' @@> person 2', person)
|
|
this.newEdgeData.to = person.id
|
|
this.addRelationshipModal(this.newEdgeData)
|
|
this.displayHelpMessage = false
|
|
this.listenPersonFlag = 'normal'
|
|
this.newEdgeData = {}
|
|
}
|
|
if (this.listenPersonFlag === 'step1') {
|
|
//console.log(' @@> person 1', person)
|
|
this.newEdgeData.from = person.id
|
|
this.listenPersonFlag = 'step2'
|
|
}
|
|
},
|
|
|
|
/// control Layers
|
|
toggleLayer(value) {
|
|
let id = value.target.value
|
|
console.log('@@@@@@ toggle Layer', id)
|
|
this.forceUpdateComponent()
|
|
if (this.checkedLayers.includes(id)) {
|
|
this.removeLayer(id)
|
|
} else {
|
|
this.addLayer(id)
|
|
}
|
|
},
|
|
addLayer(id) {
|
|
//console.log('+ addLayer', id)
|
|
this.checkedLayers.push(id)
|
|
this.$store.dispatch('excludedNode', ['remove', id])
|
|
},
|
|
removeLayer(id) {
|
|
//console.log('- removeLayer', id)
|
|
this.checkedLayers = this.checkedLayers.filter(i => i !== id)
|
|
this.$store.dispatch('excludedNode', ['add', id])
|
|
},
|
|
|
|
/// control Modal
|
|
addRelationshipModal(edgeData) {
|
|
//console.log('==- addRelationshipModal', edgeData)
|
|
this.modal = {
|
|
data: { from: edgeData.from, to: edgeData.to },
|
|
action: 'create',
|
|
showModal: true,
|
|
title: 'visgraph.add_relationship_link',
|
|
button: { class: 'btn-create', text: 'action.create' }
|
|
}
|
|
},
|
|
editRelationshipModal(edgeData) {
|
|
//console.log('==- editRelationshipModal', edgeData)
|
|
this.modal = {
|
|
data: edgeData,
|
|
action: 'edit',
|
|
showModal: true,
|
|
title: 'visgraph.edit_relationship_link',
|
|
button: { class: 'btn-edit', text: 'action.edit' }
|
|
}
|
|
},
|
|
|
|
// form
|
|
resetForm() {
|
|
this.modal = {
|
|
data: { type: 'relationship', from: null, to: null, relation: null, reverse: false },
|
|
action: null,
|
|
title: null,
|
|
button: { class: null, text: null, }
|
|
}
|
|
console.log('==- reset Form', this.modal.data)
|
|
},
|
|
getRelationsList() {
|
|
//console.log('fetch relationsList')
|
|
return getRelationsList().then(relations => new Promise(resolve => {
|
|
//console.log('+ relations list', relations.results.length)
|
|
this.relations = relations.results.filter(r => r.isActive === true)
|
|
resolve()
|
|
})).catch()
|
|
},
|
|
customLabel(value) {
|
|
//console.log('customLabel', value)
|
|
return (value.title && value.reverseTitle) ? `${value.title.fr} ↔ ${value.reverseTitle.fr}` : ''
|
|
},
|
|
getPerson(id) {
|
|
let person = this.persons.filter(p => p.id === id)
|
|
return person[0]
|
|
},
|
|
|
|
// actions
|
|
createRelationship() {
|
|
this.displayHelpMessage = true
|
|
this.listenPersonFlag = 'step1' // toggle listener in create link mode
|
|
console.log(' @@> switch listener to create link mode:', this.listenPersonFlag)
|
|
},
|
|
dropRelationship() {
|
|
//console.log('delete', this.modal.data)
|
|
deleteRelationship(this.modal.data)
|
|
.catch()
|
|
this.$store.commit('removeLink', this.modal.data.id)
|
|
this.modal.showModal = false
|
|
this.resetForm()
|
|
},
|
|
submitRelationship() {
|
|
console.log('submitRelationship', this.modal.action)
|
|
switch (this.modal.action) {
|
|
|
|
case 'create':
|
|
return postRelationship(this.modal.data)
|
|
.then(relationship => new Promise(resolve => {
|
|
console.log('post relationship response', relationship)
|
|
this.$store.dispatch('addLinkFromRelationship', relationship)
|
|
this.modal.showModal = false
|
|
this.resetForm()
|
|
resolve()
|
|
}))
|
|
.catch()
|
|
|
|
case 'edit':
|
|
return patchRelationship(this.modal.data)
|
|
.then(relationship => new Promise(resolve => {
|
|
console.log('patch relationship response', relationship)
|
|
this.$store.commit('updateLink', relationship)
|
|
this.modal.showModal = false
|
|
this.resetForm()
|
|
resolve()
|
|
}))
|
|
.catch()
|
|
|
|
default:
|
|
throw "uncaught action"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style src="vis-network/dist/dist/vis-network.min.css"></style>
|
|
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
|
|
|
<style lang="scss" scoped>
|
|
div#visgraph {
|
|
height: 700px;
|
|
margin: auto;
|
|
}
|
|
div#visgraph-legend {
|
|
div.post-menu.legend {
|
|
}
|
|
}
|
|
.modal-mask {
|
|
background-color: rgba(0, 0, 0, 0.25);
|
|
}
|
|
.debug {
|
|
margin: 1em; padding: 1em;
|
|
color: dimgray;
|
|
font-style: italic;
|
|
font-size: 80%;
|
|
}
|
|
</style>
|