From 617db3a654c8679391033221c7374afdff89d620 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 6 Oct 2016 17:11:27 +0800 Subject: [PATCH 1/5] first attempt at copy to export JS --- app/controllers/maps_controller.rb | 8 ++++++-- app/services/map_export_service.rb | 8 ++++++-- frontend/src/Metamaps/Export.js | 32 ++++++++++++++++++++++++++++++ frontend/src/Metamaps/Listeners.js | 6 ++++++ frontend/src/Metamaps/index.js | 2 ++ 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 frontend/src/Metamaps/Export.js diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index f253efd5..3d785e91 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -116,8 +116,12 @@ class MapsController < ApplicationController # GET maps/:id/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| format.json { render json: exporter.json } format.csv { send_data exporter.csv } diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index 4e180bf2..5bd23b65 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -6,6 +6,8 @@ class MapExportService def initialize(user, map, opts = {}) @user = user @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' end @@ -61,6 +63,7 @@ class MapExportService topic_mappings.map do |mapping| topic = mapping.mappable next nil if topic.nil? + next nil if @topic_ids && !@topic_ids.include?(topic.id) OpenStruct.new( id: topic.id, name: topic.name, @@ -72,13 +75,14 @@ class MapExportService user: topic.user.name, permission: topic.permission ) - end.compact + end.compact.uniq(&:id) end 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| next nil if synapse.nil? + next nil if @synapse_ids && !@synapse_ids.include?(synapse.id) OpenStruct.new( topic1: synapse.topic1_id, topic2: synapse.topic2_id, diff --git a/frontend/src/Metamaps/Export.js b/frontend/src/Metamaps/Export.js new file mode 100644 index 00000000..b0cf429c --- /dev/null +++ b/frontend/src/Metamaps/Export.js @@ -0,0 +1,32 @@ +/* global $ */ +import Active from './Active' +import GlobalUI from './GlobalUI' +import Selected from './Selected' + +const Export = { + // simple hack to use the existing ruby export code + // someday we can build a real export function here + copySelection: function() { + if (!Active.Map) return // someday we can expand this + const topic_ids = Selected.Nodes.map(node => node.id).join(',') + const synapse_ids = Selected.Edges.map(edge => { + return edge.getData('synapses')[edge.getData('displayIndex')].id + }).join(',') + const url = `/maps/${Active.Map.id}/export.json` + const query = `topic_ids=${topic_ids}&synapse_ids=${synapse_ids}` + $.ajax(`${url}?${query}`, { + success: data => { + $('body').append($('').select()) + const copied = document.execCommand('copy') + $('#clipboard-text').remove() + if (copied) { + GlobalUI.notifyUser(`${Selected.Nodes.length} topics and ${Selected.Edges.length} synapses were copied to the clipboard`) + } else { + GlobalUI.notifyUser(`Copy-paste failed, try manually exporting the map at ${url}.`) + } + } + }) + } +} + +export default Export diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index a5dc630f..b5670e1e 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -4,6 +4,7 @@ import Active from './Active' import Create from './Create' import Control from './Control' import DataModel from './DataModel' +import Export from './Export' import JIT from './JIT' import Realtime from './Realtime' import Selected from './Selected' @@ -72,6 +73,11 @@ const Listeners = { Visualize.mGraph.plot() } + break + case 67: // if c or C is pressed + if (e.ctrlKey && e.target.tagName === 'BODY') { + Export.copySelection() + } break case 68: // if d or D is pressed if (e.ctrlKey || e.metaKey) { diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 747cb643..2f28ee55 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -7,6 +7,7 @@ import Control from './Control' import Create from './Create' import DataModel from './DataModel' import Debug from './Debug' +import Export from './Export' import Filter from './Filter' import GlobalUI, { Notifications, ReactApp, Search, CreateMap, ImportDialog @@ -40,6 +41,7 @@ Metamaps.Control = Control Metamaps.Create = Create Metamaps.DataModel = DataModel Metamaps.Debug = Debug +Metamaps.Export = Export Metamaps.Filter = Filter Metamaps.GlobalUI = GlobalUI Metamaps.GlobalUI.Notifications = Notifications From d3c3f928d4ef47cd9f0599659a9eec37fdd6ce28 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 7 Oct 2016 13:22:31 +0800 Subject: [PATCH 2/5] Two Ctrl+Cs in a row and copy works! --- frontend/src/Metamaps/Export.js | 56 ++++++++++++++++++++++-------- frontend/src/Metamaps/Listeners.js | 21 ++++++++++- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/frontend/src/Metamaps/Export.js b/frontend/src/Metamaps/Export.js index b0cf429c..dcabd2d9 100644 --- a/frontend/src/Metamaps/Export.js +++ b/frontend/src/Metamaps/Export.js @@ -1,29 +1,57 @@ /* 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 = { - // simple hack to use the existing ruby export code - // someday we can build a real export function here + data: null, copySelection: function() { + 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 topic_ids = Selected.Nodes.map(node => node.id).join(',') - const synapse_ids = Selected.Edges.map(edge => { + const topics = Selected.Nodes.map(node => node.id) + + // deselect synapses not joined to a selected topic + Selected.Edges.slice(0).forEach(edge => { + const synapse = edge.getData('synapses')[edge.getData('displayIndex')] + const topic1_id = synapse.get('topic1_id') + const topic2_id = synapse.get('topic2_id') + if (topics.indexOf(topic1_id) === -1 || topics.indexOf(topic2_id) === -1) { + Control.deselectEdge(edge) + } + }) + + const synapses = Selected.Edges.map(edge => { return edge.getData('synapses')[edge.getData('displayIndex')].id - }).join(',') + }) + const url = `/maps/${Active.Map.id}/export.json` - const query = `topic_ids=${topic_ids}&synapse_ids=${synapse_ids}` + const query = `topic_ids=${topics.join(',')}&synapse_ids=${synapses.join(',')}` $.ajax(`${url}?${query}`, { + dataType: 'text', success: data => { - $('body').append($('').select()) - const copied = document.execCommand('copy') - $('#clipboard-text').remove() - if (copied) { - GlobalUI.notifyUser(`${Selected.Nodes.length} topics and ${Selected.Edges.length} synapses were copied to the clipboard`) - } else { - GlobalUI.notifyUser(`Copy-paste failed, try manually exporting the map at ${url}.`) - } + Export.data = data + GlobalUI.notifyUser(`Press Ctrl+C again to copy ${topics.length} topics and ${synapses.length} synapses`) } }) } diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index b5670e1e..c60ba2dc 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -1,5 +1,7 @@ /* global $ */ +import clipboard from 'clipboard-js' + import Active from './Active' import Create from './Create' import Control from './Control' @@ -31,9 +33,26 @@ const Listeners = { case 27: // if esc key is pressed JIT.escKeyHandler() 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) { 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 case 46: // if DEL is pressed From 26189edb749ce93a864f67c8dd78cc426052db06 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 7 Oct 2016 13:50:06 +0800 Subject: [PATCH 3/5] ok copy-paste mostly works. pasting on top of other topics is still awkward --- frontend/src/Metamaps/Export.js | 2 +- frontend/src/Metamaps/Import.js | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/frontend/src/Metamaps/Export.js b/frontend/src/Metamaps/Export.js index dcabd2d9..4f8ab8b9 100644 --- a/frontend/src/Metamaps/Export.js +++ b/frontend/src/Metamaps/Export.js @@ -29,7 +29,7 @@ const Export = { loadCopyData: function() { if (!Active.Map) return // someday we can expand this - const topics = Selected.Nodes.map(node => node.id) + const topics = Selected.Nodes.map(node => node.getData('topic').id) // deselect synapses not joined to a selected topic Selected.Edges.slice(0).forEach(edge => { diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index d4051ddb..a7727c5d 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -6,6 +6,7 @@ import _ from 'lodash' import Active from './Active' import AutoLayout from './AutoLayout' import DataModel from './DataModel' +import Control from './Control' import Map from './Map' import Synapse from './Synapse' import Topic from './Topic' @@ -78,6 +79,8 @@ const Import = { if (topics.length > 0 || synapses.length > 0) { if (window.confirm('Are you sure you want to create ' + topics.length + ' new topics and ' + synapses.length + ' new synapses?')) { + Control.deselectAllNodes() + Control.deselectAllEdges() self.importTopics(topics) // window.setTimeout(() => self.importSynapses(synapses), 5000) self.importSynapses(synapses) @@ -122,10 +125,10 @@ const Import = { if (noblanks.length === 0) { state = STATES.UNKNOWN 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 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 break } @@ -233,7 +236,11 @@ const Import = { self.createTopicWithParameters( 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(() => { self.createSynapseWithParameters( synapse.desc, synapse.category, synapse.permission, - topic1, topic2 + topic1, topic2, { + success: synapse => { + Control.selectEdge(synapse.get('edge')) + } + } ) }) }) @@ -369,6 +380,7 @@ const Import = { importId, { success: function(topic) { + Control.selectNode(topic.get('node')) if (topic.get('name') !== 'Link') return $.get('/hacks/load_url_title', { url @@ -421,13 +433,6 @@ const Import = { console.error(message) }, - // TODO investigate replacing with es6 (?) trim() - simplify: function(string) { - return string - .replace(/(^\s*|\s*$)/g, '') - .toLowerCase() - }, - normalizeKey: function(key) { let newKey = key.toLowerCase() newKey = newKey.replace(/\s/g, '') // remove whitespace From 99be53225cd2795c1a6f7565fd89f490a234b588 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 7 Oct 2016 13:56:30 +0800 Subject: [PATCH 4/5] code climate and enable Cmd+C copy --- frontend/src/Metamaps/Export.js | 14 ++++++++------ frontend/src/Metamaps/Import.js | 4 ++-- frontend/src/Metamaps/Listeners.js | 5 ++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/frontend/src/Metamaps/Export.js b/frontend/src/Metamaps/Export.js index 4f8ab8b9..115de12c 100644 --- a/frontend/src/Metamaps/Export.js +++ b/frontend/src/Metamaps/Export.js @@ -11,7 +11,9 @@ import Selected from './Selected' const Export = { data: null, - copySelection: function() { + 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 { @@ -27,16 +29,16 @@ const Export = { } }, - loadCopyData: function() { + 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 topic1_id = synapse.get('topic1_id') - const topic2_id = synapse.get('topic2_id') - if (topics.indexOf(topic1_id) === -1 || topics.indexOf(topic2_id) === -1) { + const topic1Id = synapse.get('topic1_id') + const topic2Id = synapse.get('topic2_id') + if (topics.indexOf(topic1Id) === -1 || topics.indexOf(topic2Id) === -1) { Control.deselectEdge(edge) } }) @@ -44,7 +46,7 @@ const Export = { 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}`, { diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index a7727c5d..535058dc 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -280,8 +280,8 @@ const Import = { topic1, topic2, { success: synapse => { Control.selectEdge(synapse.get('edge')) - } - } + } + } ) }) }) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index c60ba2dc..edcb0161 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -1,7 +1,5 @@ /* global $ */ -import clipboard from 'clipboard-js' - import Active from './Active' import Create from './Create' import Control from './Control' @@ -94,7 +92,8 @@ const Listeners = { break case 67: // if c or C is pressed - if (e.ctrlKey && e.target.tagName === 'BODY') { + // metaKey is OSX command key for Cmd+C + if ((e.ctrlKey || e.metaKey) && e.target.tagName === 'BODY') { Export.copySelection() } break From cfca0ba481a72aefbbde8277dafa3e52ec84b9fd Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 8 Oct 2016 13:56:10 +0800 Subject: [PATCH 5/5] map export service can rely on rubocop AbcSize instead of MethodLength because of long literal assignments --- app/services/map_export_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index 5bd23b65..8f0ab662 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true - +# rubocop:disable Metrics/MethodLength class MapExportService attr_reader :user, :map, :base_url @@ -114,3 +114,4 @@ class MapExportService spreadsheet end end +# rubocop:disable Metrics/MethodLength