Compare commits
5 commits
develop
...
feature/co
Author | SHA1 | Date | |
---|---|---|---|
|
cfca0ba481 | ||
|
99be53225c | ||
|
26189edb74 | ||
|
d3c3f928d4 | ||
|
617db3a654 |
6 changed files with 119 additions and 17 deletions
|
@ -116,8 +116,12 @@ class MapsController < ApplicationController
|
||||||
|
|
||||||
# GET maps/:id/export
|
# GET maps/:id/export
|
||||||
def export
|
def export
|
||||||
exporter = MapExportService.new(current_user, @map, base_url: request.base_url)
|
topic_ids = params[:topic_ids].split(',').map(&:to_i)
|
||||||
|
synapse_ids = params[:synapse_ids].split(',').map(&:to_i)
|
||||||
|
exporter = MapExportService.new(current_user, @map,
|
||||||
|
topic_ids: topic_ids,
|
||||||
|
synapse_ids: synapse_ids,
|
||||||
|
base_url: request.base_url)
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.json { render json: exporter.json }
|
format.json { render json: exporter.json }
|
||||||
format.csv { send_data exporter.csv }
|
format.csv { send_data exporter.csv }
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
class MapExportService
|
class MapExportService
|
||||||
attr_reader :user, :map, :base_url
|
attr_reader :user, :map, :base_url
|
||||||
|
|
||||||
def initialize(user, map, opts = {})
|
def initialize(user, map, opts = {})
|
||||||
@user = user
|
@user = user
|
||||||
@map = map
|
@map = map
|
||||||
|
@topic_ids = opts[:topic_ids] if opts[:topic_ids]
|
||||||
|
@synapse_ids = opts[:synapse_ids] if opts[:synapse_ids]
|
||||||
@base_url = opts[:base_url] || 'https://metamaps.cc'
|
@base_url = opts[:base_url] || 'https://metamaps.cc'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -61,6 +63,7 @@ class MapExportService
|
||||||
topic_mappings.map do |mapping|
|
topic_mappings.map do |mapping|
|
||||||
topic = mapping.mappable
|
topic = mapping.mappable
|
||||||
next nil if topic.nil?
|
next nil if topic.nil?
|
||||||
|
next nil if @topic_ids && !@topic_ids.include?(topic.id)
|
||||||
OpenStruct.new(
|
OpenStruct.new(
|
||||||
id: topic.id,
|
id: topic.id,
|
||||||
name: topic.name,
|
name: topic.name,
|
||||||
|
@ -72,13 +75,14 @@ class MapExportService
|
||||||
user: topic.user.name,
|
user: topic.user.name,
|
||||||
permission: topic.permission
|
permission: topic.permission
|
||||||
)
|
)
|
||||||
end.compact
|
end.compact.uniq(&:id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def exportable_synapses
|
def exportable_synapses
|
||||||
visible_synapses = Pundit.policy_scope!(user, map.synapses)
|
visible_synapses = Pundit.policy_scope!(user, map.synapses).uniq
|
||||||
visible_synapses.map do |synapse|
|
visible_synapses.map do |synapse|
|
||||||
next nil if synapse.nil?
|
next nil if synapse.nil?
|
||||||
|
next nil if @synapse_ids && !@synapse_ids.include?(synapse.id)
|
||||||
OpenStruct.new(
|
OpenStruct.new(
|
||||||
topic1: synapse.topic1_id,
|
topic1: synapse.topic1_id,
|
||||||
topic2: synapse.topic2_id,
|
topic2: synapse.topic2_id,
|
||||||
|
@ -110,3 +114,4 @@ class MapExportService
|
||||||
spreadsheet
|
spreadsheet
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# rubocop:disable Metrics/MethodLength
|
||||||
|
|
62
frontend/src/Metamaps/Export.js
Normal file
62
frontend/src/Metamaps/Export.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/* global $ */
|
||||||
|
import clipboard from 'clipboard-js'
|
||||||
|
|
||||||
|
import Active from './Active'
|
||||||
|
import Control from './Control'
|
||||||
|
import GlobalUI from './GlobalUI'
|
||||||
|
import Selected from './Selected'
|
||||||
|
|
||||||
|
// simple hack to use the existing ruby export code
|
||||||
|
// someday we can build a real export function here
|
||||||
|
|
||||||
|
const Export = {
|
||||||
|
data: null,
|
||||||
|
copySelection: function () {
|
||||||
|
// clipboard.copy can't be called in a different callback - it has to be directly
|
||||||
|
// inside an event listener. So to work around this, we require two Ctrl+C presses
|
||||||
|
if (Export.data === null) {
|
||||||
|
Export.loadCopyData()
|
||||||
|
} else {
|
||||||
|
clipboard.copy({
|
||||||
|
'text/plain': Export.data,
|
||||||
|
'application/json': Export.data
|
||||||
|
}).then(() => {
|
||||||
|
GlobalUI.notifyUser(`${Selected.Nodes.length} topics and ${Selected.Edges.length} synapses were copied to the clipboard`)
|
||||||
|
}, error => {
|
||||||
|
GlobalUI.notifyUser(error)
|
||||||
|
})
|
||||||
|
Export.data = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadCopyData: function () {
|
||||||
|
if (!Active.Map) return // someday we can expand this
|
||||||
|
const topics = Selected.Nodes.map(node => node.getData('topic').id)
|
||||||
|
|
||||||
|
// deselect synapses not joined to a selected topic
|
||||||
|
Selected.Edges.slice(0).forEach(edge => {
|
||||||
|
const synapse = edge.getData('synapses')[edge.getData('displayIndex')]
|
||||||
|
const topic1Id = synapse.get('topic1_id')
|
||||||
|
const topic2Id = synapse.get('topic2_id')
|
||||||
|
if (topics.indexOf(topic1Id) === -1 || topics.indexOf(topic2Id) === -1) {
|
||||||
|
Control.deselectEdge(edge)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const synapses = Selected.Edges.map(edge => {
|
||||||
|
return edge.getData('synapses')[edge.getData('displayIndex')].id
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = `/maps/${Active.Map.id}/export.json`
|
||||||
|
const query = `topic_ids=${topics.join(',')}&synapse_ids=${synapses.join(',')}`
|
||||||
|
$.ajax(`${url}?${query}`, {
|
||||||
|
dataType: 'text',
|
||||||
|
success: data => {
|
||||||
|
Export.data = data
|
||||||
|
GlobalUI.notifyUser(`Press Ctrl+C again to copy ${topics.length} topics and ${synapses.length} synapses`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Export
|
|
@ -6,6 +6,7 @@ import _ from 'lodash'
|
||||||
import Active from './Active'
|
import Active from './Active'
|
||||||
import AutoLayout from './AutoLayout'
|
import AutoLayout from './AutoLayout'
|
||||||
import DataModel from './DataModel'
|
import DataModel from './DataModel'
|
||||||
|
import Control from './Control'
|
||||||
import Map from './Map'
|
import Map from './Map'
|
||||||
import Synapse from './Synapse'
|
import Synapse from './Synapse'
|
||||||
import Topic from './Topic'
|
import Topic from './Topic'
|
||||||
|
@ -78,6 +79,8 @@ const Import = {
|
||||||
if (topics.length > 0 || synapses.length > 0) {
|
if (topics.length > 0 || synapses.length > 0) {
|
||||||
if (window.confirm('Are you sure you want to create ' + topics.length +
|
if (window.confirm('Are you sure you want to create ' + topics.length +
|
||||||
' new topics and ' + synapses.length + ' new synapses?')) {
|
' new topics and ' + synapses.length + ' new synapses?')) {
|
||||||
|
Control.deselectAllNodes()
|
||||||
|
Control.deselectAllEdges()
|
||||||
self.importTopics(topics)
|
self.importTopics(topics)
|
||||||
// window.setTimeout(() => self.importSynapses(synapses), 5000)
|
// window.setTimeout(() => self.importSynapses(synapses), 5000)
|
||||||
self.importSynapses(synapses)
|
self.importSynapses(synapses)
|
||||||
|
@ -122,10 +125,10 @@ const Import = {
|
||||||
if (noblanks.length === 0) {
|
if (noblanks.length === 0) {
|
||||||
state = STATES.UNKNOWN
|
state = STATES.UNKNOWN
|
||||||
break
|
break
|
||||||
} else if (noblanks.length === 1 && self.simplify(line[0]) === 'topics') {
|
} else if (noblanks.length === 1 && self.normalizeKey(line[0]) === 'topics') {
|
||||||
state = STATES.TOPICS_NEED_HEADERS
|
state = STATES.TOPICS_NEED_HEADERS
|
||||||
break
|
break
|
||||||
} else if (noblanks.length === 1 && self.simplify(line[0]) === 'synapses') {
|
} else if (noblanks.length === 1 && self.normalizeKey(line[0]) === 'synapses') {
|
||||||
state = STATES.SYNAPSES_NEED_HEADERS
|
state = STATES.SYNAPSES_NEED_HEADERS
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -233,7 +236,11 @@ const Import = {
|
||||||
|
|
||||||
self.createTopicWithParameters(
|
self.createTopicWithParameters(
|
||||||
topic.name, topic.metacode, topic.permission,
|
topic.name, topic.metacode, topic.permission,
|
||||||
topic.desc, topic.link, coords.x, coords.y, topic.id
|
topic.desc, topic.link, coords.x, coords.y, topic.id, {
|
||||||
|
success: topic => {
|
||||||
|
Control.selectNode(topic.get('node'))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -270,7 +277,11 @@ const Import = {
|
||||||
$.when(topic1Promise, topic2Promise).done(() => {
|
$.when(topic1Promise, topic2Promise).done(() => {
|
||||||
self.createSynapseWithParameters(
|
self.createSynapseWithParameters(
|
||||||
synapse.desc, synapse.category, synapse.permission,
|
synapse.desc, synapse.category, synapse.permission,
|
||||||
topic1, topic2
|
topic1, topic2, {
|
||||||
|
success: synapse => {
|
||||||
|
Control.selectEdge(synapse.get('edge'))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -369,6 +380,7 @@ const Import = {
|
||||||
importId,
|
importId,
|
||||||
{
|
{
|
||||||
success: function(topic) {
|
success: function(topic) {
|
||||||
|
Control.selectNode(topic.get('node'))
|
||||||
if (topic.get('name') !== 'Link') return
|
if (topic.get('name') !== 'Link') return
|
||||||
$.get('/hacks/load_url_title', {
|
$.get('/hacks/load_url_title', {
|
||||||
url
|
url
|
||||||
|
@ -421,13 +433,6 @@ const Import = {
|
||||||
console.error(message)
|
console.error(message)
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO investigate replacing with es6 (?) trim()
|
|
||||||
simplify: function(string) {
|
|
||||||
return string
|
|
||||||
.replace(/(^\s*|\s*$)/g, '')
|
|
||||||
.toLowerCase()
|
|
||||||
},
|
|
||||||
|
|
||||||
normalizeKey: function(key) {
|
normalizeKey: function(key) {
|
||||||
let newKey = key.toLowerCase()
|
let newKey = key.toLowerCase()
|
||||||
newKey = newKey.replace(/\s/g, '') // remove whitespace
|
newKey = newKey.replace(/\s/g, '') // remove whitespace
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Active from './Active'
|
||||||
import Create from './Create'
|
import Create from './Create'
|
||||||
import Control from './Control'
|
import Control from './Control'
|
||||||
import DataModel from './DataModel'
|
import DataModel from './DataModel'
|
||||||
|
import Export from './Export'
|
||||||
import JIT from './JIT'
|
import JIT from './JIT'
|
||||||
import Realtime from './Realtime'
|
import Realtime from './Realtime'
|
||||||
import Selected from './Selected'
|
import Selected from './Selected'
|
||||||
|
@ -30,9 +31,26 @@ const Listeners = {
|
||||||
case 27: // if esc key is pressed
|
case 27: // if esc key is pressed
|
||||||
JIT.escKeyHandler()
|
JIT.escKeyHandler()
|
||||||
break
|
break
|
||||||
case 38: // if UP key is pressed
|
case 37: // if Left arrow key is pressed
|
||||||
|
if (e.target.tagName === 'BODY') {
|
||||||
|
Visualize.mGraph.canvas.translate(-20, 0)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 38: // if Up arrow key is pressed
|
||||||
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
|
||||||
Control.selectNeighbors()
|
Control.selectNeighbors()
|
||||||
|
} else if (e.target.tagName === 'BODY') {
|
||||||
|
Visualize.mGraph.canvas.translate(0, -20)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 39: // if Right arrow key is pressed
|
||||||
|
if (e.target.tagName === 'BODY') {
|
||||||
|
Visualize.mGraph.canvas.translate(20, 0)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 40: // if Down arrow key is pressed
|
||||||
|
if (e.target.tagName === 'BODY') {
|
||||||
|
Visualize.mGraph.canvas.translate(0, 20)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 46: // if DEL is pressed
|
case 46: // if DEL is pressed
|
||||||
|
@ -72,6 +90,12 @@ const Listeners = {
|
||||||
Visualize.mGraph.plot()
|
Visualize.mGraph.plot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 67: // if c or C is pressed
|
||||||
|
// metaKey is OSX command key for Cmd+C
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.target.tagName === 'BODY') {
|
||||||
|
Export.copySelection()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 68: // if d or D is pressed
|
case 68: // if d or D is pressed
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import Control from './Control'
|
||||||
import Create from './Create'
|
import Create from './Create'
|
||||||
import DataModel from './DataModel'
|
import DataModel from './DataModel'
|
||||||
import Debug from './Debug'
|
import Debug from './Debug'
|
||||||
|
import Export from './Export'
|
||||||
import Filter from './Filter'
|
import Filter from './Filter'
|
||||||
import GlobalUI, {
|
import GlobalUI, {
|
||||||
Notifications, ReactApp, Search, CreateMap, ImportDialog
|
Notifications, ReactApp, Search, CreateMap, ImportDialog
|
||||||
|
@ -40,6 +41,7 @@ Metamaps.Control = Control
|
||||||
Metamaps.Create = Create
|
Metamaps.Create = Create
|
||||||
Metamaps.DataModel = DataModel
|
Metamaps.DataModel = DataModel
|
||||||
Metamaps.Debug = Debug
|
Metamaps.Debug = Debug
|
||||||
|
Metamaps.Export = Export
|
||||||
Metamaps.Filter = Filter
|
Metamaps.Filter = Filter
|
||||||
Metamaps.GlobalUI = GlobalUI
|
Metamaps.GlobalUI = GlobalUI
|
||||||
Metamaps.GlobalUI.Notifications = Notifications
|
Metamaps.GlobalUI.Notifications = Notifications
|
||||||
|
|
Loading…
Reference in a new issue