Skip to content

Commit 5ff4955

Browse files
author
yann.lesage
committedSep 20, 2021
improve :
- import js binding from disk - better memory handle (don't keep visu in memory when delete) - can have multiple websocket source
1 parent a2746b5 commit 5ff4955

File tree

5 files changed

+1971
-1400
lines changed

5 files changed

+1971
-1400
lines changed
 

‎resources/TelescopeBinding.js

+575
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,575 @@
1+
class TLJS {
2+
static eventsWithInteractions = {
3+
"tap": "click",
4+
"cxttap": "rightClick",
5+
"mouseover": "mouseOver",
6+
"mouseout": "mouseOut"
7+
}
8+
9+
static getWebSocket(wsPortOrNull, webSocketMapOrNull) {
10+
const webSocketMap = webSocketMapOrNull || this.webSocketMap
11+
const wsPort = wsPortOrNull || "1701"
12+
if (!this.__webSocketMap.has(wsPort)) {
13+
TLJS.__initWebSocketOnPort(wsPort, webSocketMap)
14+
}
15+
return webSocketMap.get(wsPort)
16+
}
17+
18+
static resetWebSocket(wsPortOrNull, webSocketMapOrNull) {
19+
const webSocketMap = webSocketMapOrNull || this.webSocketMap
20+
const wsPort = wsPortOrNull || "1701"
21+
TLJS.__initWebSocketOnPort(wsPort, webSocketMap)
22+
}
23+
24+
static get webSocketMap() {
25+
if (!this.__webSocketMap) {
26+
this.__webSocketMap = new Map()
27+
}
28+
return this.__webSocketMap
29+
}
30+
31+
static get wsProtocol() {
32+
if (location.protocol === "https:") {
33+
return "wss://"
34+
} else {
35+
return "ws://"
36+
}
37+
}
38+
39+
static __initWebSocketOnPort(wsPort, webSocketMap) {
40+
const websocket = new WebSocket(TLJS.wsProtocol + location.hostname + ":" + wsPort + "/ws-TLCytoscape")
41+
websocket.onclose = function (evt) {
42+
TLJS.singleton.onClose(evt)
43+
}
44+
websocket.onmessage = function (evt) {
45+
TLJS.singleton.onMessage(evt)
46+
}
47+
websocket.onerror = function (evt) {
48+
TLJS.singleton.onError(evt)
49+
}
50+
websocket.onopen = function (evt) {
51+
const instance = TLJS.singleton
52+
instance.sendGenerationCommand(instance.visusWaitingConnectToGenerate)
53+
}
54+
webSocketMap.set(wsPort, websocket)
55+
}
56+
57+
static get singleton() {
58+
if (!this.__singleton) {
59+
this.__singleton = new TLJS()
60+
}
61+
return this.__singleton
62+
}
63+
64+
static initVisus() {
65+
TLJS.singleton.initVisus()
66+
}
67+
68+
constructor() {
69+
this.visus = new WeakMap()
70+
this.commandsAction = new Map()
71+
this.visusWaitingConnectToGenerate = []
72+
this.waitingDivByVisu = {}
73+
this.toNotifyMessage = {}
74+
this.id = 0
75+
this.timeoutId=null
76+
this.setUpCommand()
77+
}
78+
79+
initVisus(htmlElement) {
80+
let idsNeedGeneration = []
81+
for (let visuElement of (htmlElement || document).getElementsByClassName("visualization")) {
82+
const container = visuElement.parentNode
83+
if (!this.visus.has(container)) {
84+
this.waitingDivByVisu[container.getAttribute("id")] = container.getElementsByClassName("tlWaiting")[0]
85+
const visu = cytoscape({
86+
pixelRatio: 1,
87+
container: visuElement,
88+
layout: {name: "preset"},
89+
})
90+
this.visus.set(container, visu)
91+
idsNeedGeneration.push(container.id)
92+
this.parametrizeInteractionsListenerForVisu({visuId: container.id, visu})
93+
}
94+
}
95+
this.sendGenerationCommand(idsNeedGeneration)
96+
}
97+
98+
parametrizeInteractionsListenerForVisu(visuWithId) {
99+
visuWithId.protectDoubleFire = {}
100+
const evtFunction = this.createEventFunction(visuWithId)
101+
const evtFunctionWithDelais = this.delaisEvtFunction(evtFunction)
102+
visuWithId.visu.on("tap mouseout cxttap", evtFunction)
103+
visuWithId.visu.on("mouseover", evtFunctionWithDelais)
104+
visuWithId.visu.on("free", "node", this.createDragEventFunction(visuWithId))
105+
}
106+
107+
createDragEventFunction(visuWithId) {
108+
const that = this
109+
return function (evt) {
110+
//We only send a moveNode command if it is not considered as a drop
111+
if (!that.isDropActionOnANode(visuWithId, evt.cy.elements(), this.renderedPosition(), evt.target))
112+
that.sendCommand([{id: visuWithId.visuId, nodeId: evt.target.id(), command: "moveNode", position: evt.target.position()}], false);
113+
}
114+
}
115+
116+
isDropActionOnANode(visuWithId, candidates, pos, droppedNode) {
117+
var target;
118+
119+
for (let i = 0; i < candidates.length; i++) {
120+
var node = candidates[i];
121+
var bound = node.renderedBoundingBox();
122+
if (node != droppedNode && bound.x1 < pos.x && bound.x2 > pos.x && bound.y1 < pos.y && bound.y2 > pos.y) {
123+
//here we found a node correctly positionned and *WARNING* we keep the last one so with the closest zIndex
124+
target = node;
125+
}
126+
}
127+
128+
// if we found a target and this one has a drop interaction then we request the server
129+
if (target && target.dropInteraction) {
130+
this.sendCommand([{id: visuWithId.visuId, nodeId: droppedNode.id(), command: "dropNode", targetNode: target.id()}]);
131+
return true;
132+
} else
133+
return false;
134+
}
135+
136+
// I manage the events I receive from Cytoscape to send to Telescope.
137+
// I implement some mecanism to make the management of the events better for the user.
138+
// First, I implement a mecanisme to avoid to double fire an event.
139+
// Second, I implement an mecanisme to workaround a bug where cytoscape do not send a mouse out event sometimes.
140+
// To do that, I keep the current mouseover event and when I mouseover another element, I send an emulated mouseout event for this saved event to the websocket if there was no mouseout event received before.
141+
// This allows TelescopeCytoscape to not accumulate mouse over interactions when we do not receive a mouse out event.
142+
createEventFunction(visuWithId) {
143+
const that = this
144+
return function (evt) {
145+
that.clearOverInteraction()
146+
var visu = visuWithId.visu
147+
if (evt.target.id != null && !visuWithId.protectDoubleFire[evt.type] && (!visu.animated() || (evt.type == "mouseout"))) {
148+
// Sometimes cytoscape fail to send a mouseout event. This can cause multiple tooltip to stay visible when they should not.
149+
// In order to improve the usability of Telescope, when we go over of any element, we also hide the other tooltips that are visible and that should show on a mouse over.
150+
if (evt.type == "mouseover") {
151+
visu.elements().each(function (element) {
152+
var qtipAPI = element.qtip("api")
153+
if (element != evt.target && qtipAPI && qtipAPI.tooltip && qtipAPI.tooltip.is(":visible") && qtipAPI.options.show.event == "mouseover") {
154+
qtipAPI.hide()
155+
}
156+
})
157+
}
158+
159+
// Server interaction processing
160+
if (!((!evt.target["mouseOverInteraction"]) && ((evt.type == "mouseover") || (evt.type == "mouseout")))) {
161+
visuWithId.protectDoubleFire[evt.type] = true
162+
setTimeout(function () {
163+
visuWithId.protectDoubleFire[evt.type] = false
164+
}, self.reactTime * 4)
165+
166+
// If we have a mouseout event and a previously saved mouseover event, we remove the saved event.
167+
if ((evt.type == "mouseout") && visu.currentOveredElement != null && visu.currentOveredElement.target.id() == evt.target.id()) {
168+
visu.currentOveredElement = null
169+
}
170+
171+
// If we receive a mouseover event, we discard the potentially saved mouseover event for which we did not received a mouseout event from cytoscape. Then save the new mouseover event.
172+
if (evt.type == "mouseover") {
173+
if (visu.currentOveredElement != null) {
174+
that.sendCommand([{
175+
id: visuWithId.visuId,
176+
drawableId: visu.currentOveredElement.target.id(),
177+
command: "interaction",
178+
kind: (TLJS.eventsWithInteractions["mouseout"])
179+
}])
180+
visu.currentOveredElement = null
181+
}
182+
visu.currentOveredElement = evt
183+
}
184+
185+
that.sendCommand([{
186+
id: visuWithId.visuId,
187+
drawableId: evt.target.id(),
188+
command: "interaction",
189+
kind: (TLJS.eventsWithInteractions[evt.type])
190+
}])
191+
// menu management
192+
if (evt.type == "cxttap" && evt.target["menu"]) {
193+
that.displayMenuForElement(evt.target, visuWithId.visuId, {
194+
x: evt.originalEvent.clientX,
195+
y: evt.originalEvent.clientY
196+
})
197+
visu.container().style.cursor = ""
198+
}
199+
}
200+
}
201+
}
202+
}
203+
204+
delaisEvtFunction(evtFunction) {
205+
const that = this
206+
return function (evt) {
207+
that.clearOverInteraction();
208+
that.timeoutId = setTimeout(function() {
209+
evt.target.onmouseover= null;
210+
evtFunction(evt);
211+
}, self.reactTime);
212+
}
213+
}
214+
215+
clearOverInteraction(){
216+
if(this.timeoutId!=null){
217+
clearTimeout(this.timeoutId);
218+
this.timeoutId=null;
219+
}
220+
}
221+
222+
sendGenerationCommand(visusIds, port) {
223+
const websocket = TLJS.getWebSocket(port)
224+
if (websocket.readyState == 0) {
225+
this.visusWaitingConnectToGenerate = this.visusWaitingConnectToGenerate.concat(visusIds)
226+
} else {
227+
const messages = visusIds.reduce((acc, id) => {
228+
if (document.getElementById(id) != null) {
229+
acc.push({id, command: "generate"})
230+
}
231+
return acc
232+
}, [])
233+
websocket.send(JSON.stringify(messages))
234+
this.visusWaitingConnectToGenerate = []
235+
}
236+
}
237+
238+
sendCommand(jsonArr, progress, port) {
239+
if (progress !== false) // strict equality to accept null as true value for progress
240+
for (var i = 0; i < jsonArr.length; i++)
241+
this.visuWithId(jsonArr[0].id).container().style.cursor = "progress"
242+
TLJS.getWebSocket(port).send(JSON.stringify(jsonArr))
243+
}
244+
245+
246+
// I am called when the websocket has an error event
247+
onError(evt) {
248+
this.tryReconnect = false
249+
}
250+
251+
// I am called when the websocket is closing.
252+
onClose(evt) {
253+
this.tryReconnect = this.tryReconnect && this.pageHaveVisu()
254+
if (this.tryReconnect) {
255+
TLJS.resetWebSocket(visus)
256+
}
257+
}
258+
259+
onMessage(evt) {
260+
const commands = JSON.parse(evt.data)
261+
const toUpdate = {}
262+
const needNotify = {}
263+
commands.forEach(command => {
264+
if (command.visuId) {
265+
needNotify[command.visuId] = true
266+
}
267+
const visu = this.visuWithId(command.visuId)
268+
if (this.commandsAction.has(command.command)) {
269+
this.commandsAction.get(command.command)(command, visu, toUpdate)
270+
} else {
271+
console.log("unsupported command: " + command.command)
272+
}
273+
})
274+
//customize in one visu rendering all element toUpdate
275+
this.customizeAll(toUpdate)
276+
Object.keys(needNotify).forEach(visuId => {
277+
this.notifyMessageEnd(needNotify[visuId])
278+
this.visuWithId(visuId).container().style.cursor = ""
279+
})
280+
this.notifyMessageEnd("onAll")
281+
}
282+
283+
notifyMessageEnd(visuId) {
284+
if (this.toNotifyMessage[visuId] != null)
285+
for (let i = 0; i < this.toNotifyMessage[visuId].length; i++) {
286+
this.toNotifyMessage[visuId][i]()
287+
}
288+
}
289+
290+
//Return true if the page has at least one visualization
291+
pageHaveVisu() {
292+
return this.visus.size > 0
293+
}
294+
295+
static visuWithId(aVisuId) {
296+
return this.singleton.visuWithId(aVisuId)
297+
}
298+
299+
visuWithId(aVisuId) {
300+
return this.visus.get(document.getElementById(aVisuId))
301+
}
302+
303+
toBatchCommand(command, visu, toUpdate) {
304+
toUpdate[command.visuId] = toUpdate[command.visuId] || []
305+
toUpdate[command.visuId].push(command)
306+
}
307+
308+
customizeAll(toUpdate) {
309+
const visuIDs = Object.keys(toUpdate)
310+
for (var i = 0; i < visuIDs.length; i++) {
311+
const cmds = toUpdate[visuIDs[i]]
312+
const visu = this.visuWithId(visuIDs[i])
313+
visu.startBatch()
314+
for (var j = 0; j < cmds.length; j++) {
315+
this.commandsActionBatch[cmds[j].command](cmds[j], visu)
316+
}
317+
visu.endBatch()
318+
}
319+
}
320+
321+
removeWaitingForVisuId(aVisuId) {
322+
try {
323+
this.waitingDivByVisu[aVisuId].parentNode.removeChild(this.waitingDivByVisu[aVisuId])
324+
this.waitingDivByVisu[aVisuId] = null
325+
} catch (err) {
326+
//Here the waiting has been removed a previous time
327+
console.log(err)
328+
}
329+
}
330+
331+
customizeElement(element, commandParametersForElement) {
332+
// here we define the attribute for mouse over to avoid sending request to the server if unnecessary
333+
if ((commandParametersForElement.mouseOverInteraction !== null))
334+
element["mouseOverInteraction"] = commandParametersForElement.mouseOverInteraction
335+
// here we define the attribute for mouse over to avoid sending request to the server if unnecessary
336+
if ((commandParametersForElement.dropInteraction !== null))
337+
element["dropInteraction"] = commandParametersForElement.dropInteraction
338+
// here we define a popup if the element has one
339+
if (commandParametersForElement.popUp) {
340+
element.popUp = commandParametersForElement.popUp
341+
element.qtip(commandParametersForElement.popUp)
342+
}
343+
// here we define a menu if element has one
344+
if (commandParametersForElement.menu) {
345+
element["menu"] = commandParametersForElement.menu
346+
}
347+
}
348+
349+
addStaticLegendEntry(visuId, html) {
350+
var div = document.getElementById(visuId + "legend")
351+
if (!div) {
352+
var legendInfoId = "legend" + this.id
353+
this.id++
354+
div = $("<div>", {id: visuId + "legend", class: "tlLegend"})
355+
.html("<span style=\"font-weight:bold;\">Legend</span> <div style=\"float: right; margin-left: 15px;\"><span id=\"" + legendInfoId + "\" style=\"cursor: help; margin-right: 5px;\">�</span><div style=\"display: none\"><strong>Box selection</strong><hr>It is possible to move multiple nodes at a time in the visualization by maintaining the SHIFT key (or three finger swipe on tablet) while doing a box selection. You can then move the selected node in group. You can cancel the selection by clicking somewhere in the visualization.</div><img data-fold style=\"cursor: pointer;\" src=\"/files/CYSFileLibrary/arrowUp.png\" onclick=\"TLJS.toggleLegend(this);\"></div><table></table>")[0]
356+
document.getElementById(visuId).appendChild(div)
357+
$("#" + legendInfoId).qtip({
358+
content: {
359+
text: $("#" + legendInfoId).next()
360+
}
361+
})
362+
}
363+
364+
div.getElementsByTagName("table")[0].insertRow(-1).innerHTML = html
365+
}
366+
367+
setUpCommand() {
368+
this.commandsAction.set("add", (command, visu) => {
369+
// Since v3.3.0 of cytoscape, it is not possible anymore to create a node with a style in the parameters.
370+
// Here we create one stylesheet for each element and my add the related class to objet to create.
371+
// Performance wise, it is betten than creating the nodes then applying a style bypass to each of them.
372+
// Maybe in the future we can do better but it would mean a good refactoring of the connector.
373+
// We would need to group the nodes to add and update by stylesheet. This mean that the multiple add command would no longer be a simple composite and actions should keep state to keep the list of stylesheets.
374+
let elementClass
375+
command.parameters.forEach(parameter => {
376+
elementClass = "element" + this.id
377+
this.id++
378+
visu.style().selector("." + elementClass).css(parameter.style)
379+
parameter.style = null
380+
parameter.classes = [elementClass]
381+
})
382+
383+
const elements = visu.add(command.parameters)
384+
for (let elementId = 0; elementId < elements.length; elementId++) {
385+
this.customizeElement(elements[elementId], command.parameters[elementId])
386+
}
387+
})
388+
389+
//just register visu to remove cursor progress
390+
let toBatchCommand = this.toBatchCommand.bind(this)
391+
this.commandsAction.set("acknoledgeReceipt", toBatchCommand)
392+
this.commandsActionBatch = {}
393+
this.commandsActionBatch.acknoledgeReceipt = function () {
394+
}
395+
396+
this.commandsAction.set("remove", toBatchCommand)
397+
this.commandsActionBatch.remove = function (command, visu) {
398+
visu.remove(visu.getElementById(command.nodeId))
399+
}
400+
401+
this.commandsAction.set("positioning", (command, visu) => {
402+
visu.layout(command.layout).run()
403+
})
404+
405+
this.commandsAction.set("customize", toBatchCommand)
406+
this.commandsActionBatch.customize = function (command, visu) {
407+
let element = visu.getElementById(command.elementId)
408+
element.style(command.style)
409+
element.mouseOverInteraction = command.mouseOverInteraction
410+
}
411+
412+
this.commandsAction.set("addStaticLegendEntry", (command) => {
413+
this.addStaticLegendEntry(command.visuId, command.html)
414+
})
415+
416+
this.commandsAction.set("removeLegend", (command) => {
417+
$("#" + command.visuId + "legend").find("table").children().remove()
418+
})
419+
420+
this.commandsAction.set("refreshNode", (command, visu) => {
421+
visu.$("#" + command.data.id).changeData(command)
422+
})
423+
424+
this.commandsAction.set("callbackUrl", (command, visu) => {
425+
sendCallBack(command.callbackUrl, command.openInNewTab)
426+
})
427+
428+
// Called when we execute a Seaside ajax callback interaction
429+
this.commandsAction.set("ajax", (command, visu) => {
430+
$(command.cssQuery).load(command.callbackUrl)
431+
})
432+
433+
this.commandsAction.set("generated", (command) => {
434+
this.removeWaitingForVisuId(command.visuId)
435+
})
436+
437+
this.commandsAction.set("error", (command) => {
438+
if (handleServerError[command.detail]) {
439+
handleServerError[command.detail](command)//usefull to display messages
440+
} else if (this.waitingDivByVisu[command.visuId] != null) {
441+
this.waitingDivByVisu[command.visuId].innerHTML = "An error has occured."
442+
notify("An error has occured")
443+
console.log("message error have no display")
444+
console.log(command)
445+
}
446+
onError()//generic handle Error
447+
})
448+
}
449+
450+
static toggleLegend(button) {
451+
var table = $(button.parentNode.parentNode.getElementsByTagName("table")[0])
452+
if (button.dataset.fold) {
453+
table.fadeIn()
454+
button.setAttribute("src", "/files/CYSFileLibrary/arrowUp.png")
455+
button.dataset.fold = ""
456+
} else {
457+
table.fadeOut()
458+
button.setAttribute("src", "/files/CYSFileLibrary/arrowDown.png")
459+
button.dataset.fold = "true"
460+
}
461+
}
462+
463+
static disableUserNotification() {
464+
notify = function (message) {
465+
console.log(message)
466+
}
467+
}
468+
469+
static notifyMessageEnd(visuId) {
470+
this.singleton.notifyMessageEnd()
471+
}
472+
473+
notifyMessageEnd(visuId) {
474+
if (this.toNotifyMessage[visuId] != null)
475+
for (let i = 0; i < this.toNotifyMessage[visuId].length; i++) {
476+
this.toNotifyMessage[visuId][i]()
477+
}
478+
}
479+
480+
static enableUserNotification() {
481+
this.singleton.enableUserNotification()
482+
}
483+
484+
enableUserNotification() {
485+
function showNotification(message) {
486+
new Notification(message)
487+
}
488+
489+
if (!("Notification" in window)) {
490+
TLJS.disableUserNotification()
491+
} else if (Notification.permission === "granted") {
492+
notify = showNotification
493+
} else if (Notification.permission !== "denied") {
494+
Notification.requestPermission(function (permission) {
495+
if (permission === "granted") {
496+
notify = showNotification
497+
} else {
498+
TLJS.disableUserNotification()
499+
}
500+
})
501+
} else {
502+
TLJS.disableUserNotification()
503+
}
504+
}
505+
506+
static onMessageEnd(callback, id) {
507+
this.singleton.onMessageEnd(callback, id)
508+
}
509+
510+
onMessageEnd(callback, id) {
511+
if (id != null) {
512+
if (this.toNotifyMessage[id] == null)
513+
this.toNotifyMessage[id] = []
514+
this.toNotifyMessage[id].push(callback)
515+
} else {
516+
this.onMessageEnd(callback, "onAll")
517+
}
518+
}
519+
520+
static useExternalTrigger(evt, visuId, triggerId) {
521+
return this.singleton.useExternalTrigger(evt, visuId, triggerId)
522+
}
523+
524+
useExternalTrigger(evt, visuId, triggerId) {
525+
this.sendCommand([{
526+
id: visuId,
527+
command: "externalTrigger",
528+
triggerId: triggerId,
529+
kind: (TLJS.eventsWithInteractions[evt.type] || evt.type)
530+
}])
531+
}
532+
533+
}
534+
535+
window.addEventListener("load", () => {
536+
console.log("loaded")
537+
TLJS.initVisus()
538+
}, false)
539+
540+
541+
var notify
542+
TLJS.enableUserNotification()
543+
544+
545+
function changeData(command) {
546+
let self = this
547+
if (command.style) {
548+
self.style(command.style)
549+
}
550+
visu = self.cy()
551+
newNode = {
552+
group: self.group(),
553+
position: self.position(),
554+
data: command.data,
555+
style: self.style()
556+
}
557+
edges = self.connectedEdges()
558+
children = self.children()
559+
}
560+
561+
562+
cytoscape("collection", "changeData", changeData)
563+
564+
console.warn("telescope is deprecated. Use TLJS instead.")
565+
let telescope = TLJS
566+
567+
568+
visuWithId = function (id) {
569+
console.log("visuWithId is deprecated. Use telescope.visuWithId instead.")
570+
return telescope.visuWithId(id)
571+
}
572+
573+
telescope.loadVisuIn = (htmlElement) => {
574+
TLJS.singleton.initVisus(htmlElement)
575+
}

‎src/BaselineOfTelescopeCytoscape/BaselineOfTelescopeCytoscape.class.st

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ BaselineOfTelescopeCytoscape >> baseline: spec [
1919
seaside3: spec;
2020
webSocket: spec;
2121
telescope: spec;
22+
gitBridge: spec;
2223
pragmaCompatibility: spec.
2324

2425
"Packages"
@@ -37,6 +38,13 @@ BaselineOfTelescopeCytoscape >> baseline: spec [
3738
group: 'tests' with: #('Telescope-Cytoscape-Tests') ]
3839
]
3940

41+
{ #category : #baselines }
42+
BaselineOfTelescopeCytoscape >> gitBridge: spec [
43+
spec
44+
baseline: 'GitBridge'
45+
with: [ spec repository: 'github://jecisc/GitBridge:v1.x.x/src' ]
46+
]
47+
4048
{ #category : #dependencies }
4149
BaselineOfTelescopeCytoscape >> neoJSON: spec [
4250
spec baseline: 'NeoJSON' with: [ spec repository: 'github://svenvc/NeoJSON/repository' ]

‎src/Telescope-Cytoscape-Libraries/CYSFileLibrary.class.st

+1,372-1,399
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Class {
2+
#name : #GitTelescopeWeb,
3+
#superclass : #GitBridge,
4+
#category : #'Telescope-Cytoscape-Libraries'
5+
}
6+
7+
{ #category : #'class initialization' }
8+
GitTelescopeWeb class >> initialize [
9+
SessionManager default registerSystemClassNamed: self name
10+
]
11+
12+
{ #category : #'class initialization' }
13+
GitTelescopeWeb class >> resources [
14+
^ self root / 'resources'
15+
]
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Package { #name : #'Telescope-Cytoscape-Libraries' }
1+
Package { #name : #'Telescope-Cytoscape-Libraries' }

0 commit comments

Comments
 (0)
Please sign in to comment.