SpeciesFileGroup/taxonworks

View on GitHub
app/javascript/vue/tasks/biological_associations/biological_associations_graph/composition/useGraph.js

Summary

Maintainability
F
3 days
Test Coverage
import { reactive, computed, toRefs } from 'vue'
import {
  BiologicalAssociation,
  BiologicalAssociationGraph,
  Citation
} from '@/routes/endpoints'
import {
  makeBiologicalAssociation,
  makeCitation,
  makeCitationPayload,
  makeGraph
} from '../adapters'
import {
  unsavedEdge,
  nodeCollectionObjectStyle,
  nodeOtuStyle,
  unsavedNodeStyle
} from '../constants/graphStyle.js'
import {
  parseNodeId,
  makeNodeId,
  isEqualNodeObject,
  isNetwork,
  getHexColorFromString
} from '../utils'
import { randomUUID } from '@/helpers'
import { addToArray } from '@/helpers/arrays'
import { COLLECTION_OBJECT, BIOLOGICAL_ASSOCIATION } from '@/constants/index.js'

const EXTEND_GRAPH = [
  'biological_associations_biological_associations_graphs',
  'biological_associations',
  'citations'
]

const EXTEND_BA = ['subject', 'object', 'biological_relationship', 'citations']

function initState() {
  return {
    biologicalAssociations: [],
    nodeObjects: [],
    selectedNodes: [],
    selectedEdges: [],
    citations: [],
    layouts: {
      nodes: {}
    },
    isSaving: false,
    isLoading: false,
    graph: makeGraph({})
  }
}

export function useGraph() {
  const state = reactive(initState())

  const nodes = computed(() =>
    Object.fromEntries(
      state.nodeObjects.map((obj) => {
        const nodeId = makeNodeId(obj)
        const isSaved = getBiologicalRelationshipsByNodeId(nodeId).some(
          (ba) => !!ba.id
        )
        const node = {
          name: obj.name
        }

        Object.assign(
          node,
          obj.objectType === COLLECTION_OBJECT
            ? nodeCollectionObjectStyle
            : nodeOtuStyle
        )

        if (!isSaved) {
          Object.assign(node, unsavedNodeStyle)
        }

        return [nodeId, node]
      })
    )
  )

  const edges = computed(() => {
    const baEdges = state.biologicalAssociations.map((ba) => {
      const edgeObject = {
        id: ba.id,
        source: makeNodeId(ba.subject),
        target: makeNodeId(ba.object),
        label: ba.biologicalRelationship.name,
        color: ba.color
      }

      if (ba.isUnsaved || ba.citations.some((c) => !c.id)) {
        Object.assign(edgeObject, unsavedEdge)
      }

      return [ba.uuid, edgeObject]
    })

    return Object.fromEntries(baEdges)
  })

  const currentGraph = computed(() => state.graph)
  const currentNodes = computed(() => state.selectedNodes)

  function resetStore() {
    Object.assign(state, initState())
  }

  function setGraphName(name) {
    state.graph.name = name
    state.graph.isUnsaved = true
  }

  function addCitationFor(obj, citationData) {
    const citation = makeCitation({
      ...citationData,
      objectUuid: obj.uuid,
      objectType: obj.objectType
    })

    obj.citations.push(citation)

    return citation
  }

  const getBiologicalRelationshipsByNodeId = (nodeId) => {
    const obj = parseNodeId(nodeId)

    return state.biologicalAssociations.filter((ba) => {
      return (
        isEqualNodeObject(ba.object, obj) || isEqualNodeObject(ba.subject, obj)
      )
    })
  }

  const isGraphUnsaved = computed(() =>
    [...state.biologicalAssociations, state.graph].some(
      (obj) => obj.isUnsaved || obj.citations.some((c) => !c.id)
    )
  )

  async function addBiologicalRelationship({
    subjectNodeId,
    objectNodeId,
    relationship
  }) {
    const nObj = parseNodeId(subjectNodeId)
    const nSub = parseNodeId(objectNodeId)

    const subject = state.nodeObjects.find((o) => isEqualNodeObject(o, nSub))
    const object = state.nodeObjects.find((o) => isEqualNodeObject(o, nObj))
    const alreadyExist = state.biologicalAssociations.find(
      (ba) =>
        ba.object.id === object.id &&
        ba.subject.id === subject.id &&
        ba.biologicalRelationship.id === relationship.id
    )

    if (alreadyExist) return

    const biologicalAssociation = {
      id: undefined,
      uuid: randomUUID(),
      subject,
      object,
      citations: [],
      objectType: BIOLOGICAL_ASSOCIATION,
      biologicalRelationship: relationship,
      color: getHexColorFromString(relationship.name),
      isUnsaved: true
    }

    state.biologicalAssociations.push(biologicalAssociation)
  }

  function getObjectByUuid(uuid) {
    return [state.graph, ...state.biologicalAssociations].find(
      (item) => item.uuid === uuid
    )
  }

  function reverseRelation(uuid) {
    const ba = state.biologicalAssociations.find((ba) => ba.uuid === uuid)

    Object.assign(ba, {
      subject: ba.object,
      object: ba.subject,
      isUnsaved: true
    })
  }

  function setNodePosition(nodeId, position) {
    state.layouts.nodes[nodeId] = position
  }

  function addObject(obj) {
    if (!state.nodeObjects.some((item) => isEqualNodeObject(item, obj))) {
      state.nodeObjects.push(obj)
    }
  }

  async function loadGraph(graphId) {
    state.isLoading = true

    const params = { extend: EXTEND_GRAPH }
    const graph = makeGraph(
      (await BiologicalAssociationGraph.find(graphId, params)).body
    )
    const baIds = graph.biologicalAssociationIds.map(
      (ba) => ba.biological_association_id
    )

    resetStore()
    state.graph = graph
    state.layouts = JSON.parse(graph.layout)

    if (baIds.length) {
      await loadBiologicalAssociations(baIds)
    }

    state.isLoading = false

    return graph
  }

  async function loadBiologicalAssociations(ids) {
    const { body } = await BiologicalAssociation.where({
      biological_association_id: ids,
      extend: EXTEND_BA
    })

    for (const item of body) {
      const ba = await makeBiologicalAssociation(item)

      addObject(ba.subject)
      addObject(ba.object)
      addToArray(state.biologicalAssociations, ba)
    }

    return state.biologicalAssociations.filter((ba) =>
      body.find((item) => item.id === ba.id)
    )
  }

  function updateObjectByUuid(uuid, objProps) {
    const obj = getObjectByUuid(uuid)

    Object.assign(obj, objProps)
  }

  function getSourceIds() {
    const citations = [].concat(
      ...state.biologicalAssociations.map((ba) => ba.citations),
      state.graph.citations
    )
    const sourceIds = citations.map((c) => c.sourceId)

    return [...new Set(sourceIds)]
  }

  function removeCitationFor({ obj, citation }) {
    const index = obj.citations.findIndex((item) => item.uuid === citation.uuid)

    if (citation.id) {
      Citation.destroy(citation.id)
    }

    obj.citations.splice(index, 1)
  }

  function removeEdge(edgeId, destroy) {
    const index = state.biologicalAssociations.findIndex(
      (ba) => ba.uuid === edgeId
    )
    const ba = state.biologicalAssociations[index]

    if (ba.id && destroy) {
      BiologicalAssociation.destroy(ba.id).then((_) => {
        TW.workbench.alert.create(
          'Biological association was successfully deleted.',
          'notice'
        )
      })
    }

    state.biologicalAssociations.splice(index, 1)
  }

  function removeNode(nodeId, destroy) {
    const biologicalAssociations = getBiologicalRelationshipsByNodeId(nodeId)
    const created = biologicalAssociations.filter(({ id }) => id)
    const nodeObject = parseNodeId(nodeId)

    biologicalAssociations.forEach((ba) => {
      removeEdge(ba.uuid, destroy)
    })

    if (destroy) {
      const message =
        created.length > 1
          ? 'Biological association(s) were successfully deleted.'
          : 'Biological association was successfully deleted.'

      TW.workbench.alert.create(message, 'notice')
    }

    removeNodeObject(nodeObject)

    state.biologicalAssociations = state.biologicalAssociations.filter(
      ({ uuid }) => biologicalAssociations.some((ba) => ba.uuid !== uuid)
    )
  }

  function removeNodeObject(obj) {
    const index = state.nodeObjects.findIndex((o) => isEqualNodeObject(o, obj))

    state.nodeObjects.splice(index, 1)
  }

  function saveBiologicalAssociations() {
    const biologicalAssociations = state.biologicalAssociations.filter(
      (ba) => ba.isUnsaved
    )

    const requests = biologicalAssociations.map((ba) => {
      const { biologicalRelationship, object, subject, id } = ba
      const payload = {
        biological_association: {
          biological_relationship_id: biologicalRelationship.id,
          biological_association_object_id: object.id,
          biological_association_object_type: object.objectType,
          biological_association_subject_id: subject.id,
          biological_association_subject_type: subject.objectType
        },
        extend: EXTEND_BA
      }

      const request = id
        ? BiologicalAssociation.update(id, payload)
        : BiologicalAssociation.create(payload)

      request.then(({ body }) => {
        ba.id = body.id
        ba.isUnsaved = false
      })

      return request
    })

    return Promise.all(requests)
  }

  async function save() {
    state.isSaving = true
    let createdBiologicalAssociations
    let biologicalAssociationGraph
    let citations

    try {
      createdBiologicalAssociations = await saveBiologicalAssociations()

      const savedCitations = [
        state.biologicalAssociations.map((ba) => saveCitationsFor(ba))
      ]

      if (isNetwork(state.biologicalAssociations) || state.graph.id) {
        biologicalAssociationGraph = await saveGraph()
        savedCitations.push(saveCitationsFor(state.graph))
      }

      citations = await Promise.all(savedCitations)

      state.isSaving = false
    } catch (e) {
      state.isSaving = false
    }

    return {
      biologicalAssociations: createdBiologicalAssociations,
      biologicalAssociationGraph,
      citations
    }
  }

  function saveCitationsFor(obj) {
    const unsaved = obj.citations.filter((c) => !c.id)

    const requests = unsaved.map((c) => {
      const payload = makeCitationPayload({ ...c, objectId: obj.id })

      return Citation.create({ citation: payload }).then(({ body }) => {
        const index = obj.citations.findIndex((item) => item.uuid === c.uuid)

        obj.citations[index] = makeCitation(body)
      })
    })

    return requests
  }

  function saveGraph() {
    const biologicalAssociationsSaved = state.biologicalAssociations.filter(
      (r) => r.id
    )
    const biologicalAssociationsInGraph =
      state.graph.biologicalAssociationIds.map(
        (obj) => obj.biological_association_id
      )

    const payload = {
      biological_associations_graph: {
        name: state.graph.name,
        layout: JSON.stringify(state.layouts),
        biological_associations_biological_associations_graphs_attributes:
          biologicalAssociationsSaved
            .filter((ba) => !biologicalAssociationsInGraph.includes(ba.id))
            .map((r) => ({
              biological_association_id: r.id
            }))
      },
      extend: EXTEND_GRAPH
    }

    const request = state.graph.id
      ? BiologicalAssociationGraph.update(state.graph.id, payload)
      : BiologicalAssociationGraph.create(payload)

    request.then(({ body }) => {
      Object.assign(state.graph, {
        id: body.id,
        globalId: body.global_id,
        label: body.object_tag,
        biologicalAssociationIds:
          body.biological_associations_biological_associations_graphs,
        isUnsaved: false
      })
    })

    return request
  }

  return {
    addBiologicalRelationship,
    addCitationFor,
    addObject,
    currentGraph,
    currentNodes,
    edges,
    getBiologicalRelationshipsByNodeId,
    getObjectByUuid,
    getSourceIds,
    isGraphUnsaved,
    loadBiologicalAssociations,
    loadGraph,
    nodes,
    removeCitationFor,
    removeEdge,
    removeNode,
    resetStore,
    reverseRelation,
    save,
    saveBiologicalAssociations,
    saveGraph,
    setGraphName,
    setNodePosition,
    updateObjectByUuid,
    ...toRefs(state)
  }
}