Compare commits

..

5 commits

6 changed files with 119 additions and 17 deletions

View file

@ -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 }

View file

@ -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

View 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

View file

@ -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

View file

@ -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) {

View file

@ -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