pict-section-flow 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +44 -13
  2. package/docs/Architecture.md +8 -148
  3. package/docs/Data_Model.md +2 -11
  4. package/docs/README.md +8 -38
  5. package/docs/Theme_Integration.md +11 -11
  6. package/docs/_cover.md +7 -1
  7. package/docs/_playground.json +24 -0
  8. package/docs/_sidebar.md +4 -0
  9. package/docs/_topbar.md +1 -1
  10. package/docs/_version.json +3 -3
  11. package/docs/card-help/FREAD.md +1 -1
  12. package/docs/diagrams/architecture-at-a-glance.excalidraw +4270 -0
  13. package/docs/diagrams/architecture-at-a-glance.mmd +30 -0
  14. package/docs/diagrams/architecture-at-a-glance.svg +2 -0
  15. package/docs/diagrams/data-flow.excalidraw +1451 -0
  16. package/docs/diagrams/data-flow.mmd +17 -0
  17. package/docs/diagrams/data-flow.svg +2 -0
  18. package/docs/diagrams/high-level-design.excalidraw +5767 -0
  19. package/docs/diagrams/high-level-design.mmd +86 -0
  20. package/docs/diagrams/high-level-design.svg +2 -0
  21. package/docs/diagrams/relationships.excalidraw +3852 -0
  22. package/docs/diagrams/relationships.mmd +9 -0
  23. package/docs/diagrams/relationships.svg +2 -0
  24. package/docs/diagrams/service-initialization-sequence.excalidraw +1466 -0
  25. package/docs/diagrams/service-initialization-sequence.mmd +19 -0
  26. package/docs/diagrams/service-initialization-sequence.svg +2 -0
  27. package/docs/diagrams/svg-layer-structure.excalidraw +1060 -0
  28. package/docs/diagrams/svg-layer-structure.mmd +18 -0
  29. package/docs/diagrams/svg-layer-structure.svg +2 -0
  30. package/docs/examples/README.md +9 -0
  31. package/docs/examples/simple_cards/README.md +677 -0
  32. package/docs/examples/simple_cards/css/flowexample.css +65 -0
  33. package/docs/examples/simple_cards/index.html +32 -0
  34. package/docs/examples/simple_cards/js/pict.min.js +12 -0
  35. package/docs/examples/simple_cards/pict-section-flow-example-simple-cards.compatible.min.js +1 -0
  36. package/docs/index.html +6 -5
  37. package/docs/playground/app.json +6 -0
  38. package/docs/playground/appdata.json +85 -0
  39. package/docs/playground/application.js +23 -0
  40. package/docs/playground/pict.json +17 -0
  41. package/docs/playground/runtime/pict-application.min.js +2 -0
  42. package/docs/playground/runtime/pict-section-flow.min.js +2 -0
  43. package/docs/playground/runtime/pict-section-modal.min.js +2 -0
  44. package/docs/playground/runtime/pict.min.js +12 -0
  45. package/docs/retold-catalog.json +241 -166
  46. package/docs/retold-keyword-index.json +19312 -7226
  47. package/example_applications/simple_cards/package.json +9 -1
  48. package/example_applications/simple_cards/source/views/PictView-FlowExample-BottomBar.js +2 -2
  49. package/package.json +5 -5
  50. package/source/providers/PictProvider-Flow-PanelChrome.js +2 -1
  51. package/source/services/PictService-Flow-DataManager.js +6 -0
  52. package/source/services/PictService-Flow-InteractionManager.js +10 -1
  53. package/source/services/PictService-Flow-PanelManager.js +106 -2
  54. package/source/views/PictView-Flow-PropertiesPanel.js +70 -3
  55. package/source/views/PictView-Flow.js +53 -0
  56. package/test/PanelManager_tests.js +172 -0
@@ -3,6 +3,14 @@
3
3
  "version": "0.0.1",
4
4
  "description": "Pict Section Flow - Simple Cards Example Application",
5
5
  "main": "source/Pict-Application-FlowExample.js",
6
+ "retold": {
7
+ "ExampleApplication": {
8
+ "Stage": true,
9
+ "Title": "Simple Cards",
10
+ "Summary": "Twelve custom PictFlowCard subclasses across six categories — every panel type (Markdown / Template / Form / View), every BodyContent renderer (SVG / HTML / Canvas), wrapped in a multi-page Pict shell with router-driven navigation and a curated sample-graph catalog.",
11
+ "Complexity": "Basic"
12
+ }
13
+ },
6
14
  "scripts": {
7
15
  "start": "node source/Pict-Application-FlowExample.js",
8
16
  "prebuild": "node ../../scripts/generate-card-help.js",
@@ -19,7 +27,7 @@
19
27
  "pict-provider": "^1.0.3",
20
28
  "pict-section-content": "^0.0.6",
21
29
  "pict-section-form": "^1.0.192",
22
- "pict-section-flow": "file:../../"
30
+ "pict-section-flow": "^1.0.1"
23
31
  },
24
32
  "devDependencies": {
25
33
  "quackage": "^1.0.58"
@@ -44,8 +44,8 @@ const _ViewConfiguration =
44
44
  <div class="flowexample-bottombar">
45
45
  <span>Pict Section Flow Example &copy; 2025</span>
46
46
  <div class="flowexample-bottombar-links">
47
- <a href="https://github.com/stevenvelozo/pict" target="_blank">Pict</a>
48
- <a href="https://github.com/stevenvelozo/fable" target="_blank">Fable</a>
47
+ <a href="https://github.com/fable-retold/pict" target="_blank">Pict</a>
48
+ <a href="https://github.com/fable-retold/fable" target="_blank">Fable</a>
49
49
  <a onclick="{~P~}.PictApplication.navigateTo('/About')">About</a>
50
50
  </div>
51
51
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-flow",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Pict Section Flow Diagram",
5
5
  "main": "source/Pict-Section-Flow.js",
6
6
  "scripts": {
@@ -15,15 +15,15 @@
15
15
  "dependencies": {
16
16
  "fable-serviceproviderbase": "^3.0.19",
17
17
  "pict-provider": "^1.0.13",
18
- "pict-section-form": "^1.0.196",
18
+ "pict-section-form": "^1.0.199",
19
19
  "pict-view": "^1.0.68"
20
20
  },
21
21
  "devDependencies": {
22
22
  "chai": "^6.2.2",
23
23
  "mocha": "^11.7.5",
24
- "pict": "^1.0.369",
25
- "pict-docuserve": "^1.3.2",
24
+ "pict": "^1.0.372",
25
+ "pict-docuserve": "^1.4.4",
26
26
  "pict-router": "^1.0.10",
27
- "quackage": "^1.2.3"
27
+ "quackage": "^1.3.0"
28
28
  }
29
29
  }
@@ -40,7 +40,8 @@ class PictProviderFlowPanelChrome extends libFableServiceProviderBase
40
40
  let tmpFO = tmpSVGHelper.createSVGElement('foreignObject');
41
41
  tmpFO.setAttribute('class', 'pict-flow-panel-foreign-object');
42
42
  tmpFO.setAttribute('data-panel-hash', pPanelData.Hash);
43
- tmpFO.setAttribute('data-node-hash', pPanelData.NodeHash);
43
+ tmpFO.setAttribute('data-node-hash', pPanelData.NodeHash || '');
44
+ if (pPanelData.ConnectionHash) { tmpFO.setAttribute('data-connection-hash', pPanelData.ConnectionHash); }
44
45
  tmpFO.setAttribute('x', String(pPanelData.X));
45
46
  tmpFO.setAttribute('y', String(pPanelData.Y));
46
47
  tmpFO.setAttribute('width', String(pPanelData.Width));
@@ -328,6 +328,12 @@ class PictServiceFlowDataManager extends libFableServiceProviderBase
328
328
 
329
329
  let tmpRemovedConnection = this._FlowView._FlowData.Connections.splice(tmpConnectionIndex, 1)[0];
330
330
 
331
+ // Close any properties panel open for this connection.
332
+ if (typeof this._FlowView.closePanelForConnection === 'function')
333
+ {
334
+ this._FlowView.closePanelForConnection(pConnectionHash);
335
+ }
336
+
331
337
  if (this._FlowView._FlowData.ViewState.SelectedConnectionHash === pConnectionHash)
332
338
  {
333
339
  this._FlowView._FlowData.ViewState.SelectedConnectionHash = null;
@@ -322,7 +322,16 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
322
322
  {
323
323
  this._LastConnectionClickTime = 0;
324
324
  this._LastConnectionClickHash = null;
325
- this._addBezierHandle(tmpTarget, pEvent);
325
+ // When the host configured a connection properties panel, double-click opens it;
326
+ // otherwise keep the default behavior of adding a bezier handle.
327
+ if (this._FlowView.options.ConnectionPropertiesPanel && tmpConnectionHash)
328
+ {
329
+ this._FlowView.toggleConnectionPanel(tmpConnectionHash);
330
+ }
331
+ else
332
+ {
333
+ this._addBezierHandle(tmpTarget, pEvent);
334
+ }
326
335
  }
327
336
  else
328
337
  {
@@ -152,6 +152,98 @@ class PictServiceFlowPanelManager extends libFableServiceProviderBase
152
152
  return this.openPanel(pNodeHash);
153
153
  }
154
154
 
155
+ /**
156
+ * Open a properties panel for a connection (edge). The panel config comes from the FlowView's
157
+ * ConnectionPropertiesPanel option (connections are not typed, so one config serves them all);
158
+ * the panel is placed near the connection's midpoint and tethers to it. Returns false when no
159
+ * ConnectionPropertiesPanel is configured, so a host that has not opted in keeps the default
160
+ * edge behavior (double-click adds a bezier handle).
161
+ * @param {string} pConnectionHash
162
+ * @returns {Object|false} The panel data, or false
163
+ */
164
+ openConnectionPanel(pConnectionHash)
165
+ {
166
+ let tmpConnection = this._FlowView.getConnection(pConnectionHash);
167
+ if (!tmpConnection) return false;
168
+
169
+ let tmpPanelConfig = this._FlowView.options.ConnectionPropertiesPanel;
170
+ if (!tmpPanelConfig) return false;
171
+
172
+ let tmpExisting = this._FlowView._FlowData.OpenPanels.find((pPanel) => pPanel.ConnectionHash === pConnectionHash);
173
+ if (tmpExisting) return tmpExisting;
174
+
175
+ let tmpMidpoint = this._FlowView.getConnectionMidpoint(pConnectionHash) || { x: 0, y: 0 };
176
+ let tmpWidth = tmpPanelConfig.DefaultWidth || 300;
177
+ let tmpHeight = tmpPanelConfig.DefaultHeight || 200;
178
+
179
+ let tmpPanelData =
180
+ {
181
+ Hash: `panel-${this.fable.getUUID()}`,
182
+ ConnectionHash: pConnectionHash,
183
+ NodeHash: null,
184
+ PanelType: tmpPanelConfig.PanelType || 'Base',
185
+ Title: tmpPanelConfig.Title || 'Connection',
186
+ X: tmpMidpoint.x + 40,
187
+ Y: tmpMidpoint.y + 20,
188
+ Width: tmpWidth,
189
+ Height: tmpHeight
190
+ };
191
+
192
+ this._FlowView._FlowData.OpenPanels.push(tmpPanelData);
193
+ this._FlowView.renderFlow();
194
+ this._FlowView.marshalFromView();
195
+
196
+ if (this._FlowView._EventHandlerProvider)
197
+ {
198
+ this._FlowView._EventHandlerProvider.fireEvent('onPanelOpened', tmpPanelData);
199
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView._FlowData);
200
+ }
201
+
202
+ return tmpPanelData;
203
+ }
204
+
205
+ /**
206
+ * Toggle a properties panel for a connection (open if closed, close if open).
207
+ * @param {string} pConnectionHash
208
+ * @returns {Object|false}
209
+ */
210
+ toggleConnectionPanel(pConnectionHash)
211
+ {
212
+ let tmpExisting = this._FlowView._FlowData.OpenPanels.find((pPanel) => pPanel.ConnectionHash === pConnectionHash);
213
+ if (tmpExisting)
214
+ {
215
+ this.closePanel(tmpExisting.Hash);
216
+ return false;
217
+ }
218
+ return this.openConnectionPanel(pConnectionHash);
219
+ }
220
+
221
+ /**
222
+ * Close all panels for a given connection.
223
+ * @param {string} pConnectionHash
224
+ * @returns {boolean}
225
+ */
226
+ closePanelForConnection(pConnectionHash)
227
+ {
228
+ let tmpPanelsToClose = this._FlowView._FlowData.OpenPanels.filter((pPanel) => pPanel.ConnectionHash === pConnectionHash);
229
+ if (tmpPanelsToClose.length === 0) return false;
230
+
231
+ for (let i = 0; i < tmpPanelsToClose.length; i++)
232
+ {
233
+ let tmpIndex = this._FlowView._FlowData.OpenPanels.indexOf(tmpPanelsToClose[i]);
234
+ if (tmpIndex >= 0)
235
+ {
236
+ this._FlowView._FlowData.OpenPanels.splice(tmpIndex, 1);
237
+ }
238
+ if (this._FlowView._PropertiesPanelView)
239
+ {
240
+ this._FlowView._PropertiesPanelView.destroyPanel(tmpPanelsToClose[i].Hash);
241
+ }
242
+ }
243
+
244
+ return true;
245
+ }
246
+
155
247
  /**
156
248
  * Update a panel's position (for drag).
157
249
  * @param {string} pPanelHash
@@ -180,8 +272,20 @@ class PictServiceFlowPanelManager extends libFableServiceProviderBase
180
272
  }
181
273
  }
182
274
 
183
- // Update the tether for this panel
184
- this._FlowView._renderTethersForNode(tmpPanel.NodeHash);
275
+ // Update the tether for this panel. A node panel refreshes just its node's tethers; a
276
+ // connection panel has no node, so reconcile the panels layer (redraws all tethers, which
277
+ // is where the connection-midpoint tether is recomputed).
278
+ if (tmpPanel.ConnectionHash)
279
+ {
280
+ if (this._FlowView._PropertiesPanelView && this._FlowView._PanelsLayer && this._FlowView._TethersLayer)
281
+ {
282
+ this._FlowView._PropertiesPanelView.renderPanels(this._FlowView._FlowData.OpenPanels, this._FlowView._PanelsLayer, this._FlowView._TethersLayer, this._FlowView._FlowData.ViewState.SelectedTetherHash);
283
+ }
284
+ }
285
+ else
286
+ {
287
+ this._FlowView._renderTethersForNode(tmpPanel.NodeHash);
288
+ }
185
289
  }
186
290
  }
187
291
 
@@ -242,6 +242,13 @@ class PictViewFlowPropertiesPanel extends libPictView
242
242
  */
243
243
  _renderPanelContent(pPanelData, pBodyContainer)
244
244
  {
245
+ // Connection (edge) panels resolve their config and data differently from node panels.
246
+ if (pPanelData.ConnectionHash)
247
+ {
248
+ this._renderConnectionPanelContent(pPanelData, pBodyContainer);
249
+ return;
250
+ }
251
+
245
252
  let tmpNodeData = this._FlowView.getNode(pPanelData.NodeHash);
246
253
  if (!tmpNodeData) return;
247
254
 
@@ -298,6 +305,54 @@ class PictViewFlowPropertiesPanel extends libPictView
298
305
  this._renderPortSummary(pBodyContainer, tmpNodeTypeConfig);
299
306
  }
300
307
 
308
+ /**
309
+ * Render the content of a connection (edge) panel. The config is the FlowView's single
310
+ * ConnectionPropertiesPanel (connections are not typed); the panel-type instance renders
311
+ * against the connection object, so a Form panel edits Connection.Data.* and a Template panel
312
+ * renders against the connection.
313
+ *
314
+ * @param {Object} pPanelData
315
+ * @param {HTMLDivElement} pBodyContainer
316
+ */
317
+ _renderConnectionPanelContent(pPanelData, pBodyContainer)
318
+ {
319
+ let tmpConnectionData = this._FlowView.getConnection(pPanelData.ConnectionHash);
320
+ if (!tmpConnectionData) return;
321
+
322
+ let tmpPanelConfig = this._FlowView.options.ConnectionPropertiesPanel;
323
+ if (!tmpPanelConfig)
324
+ {
325
+ pBodyContainer.innerHTML = '<em>No connection properties panel configured.</em>';
326
+ return;
327
+ }
328
+
329
+ let tmpPanelType = tmpPanelConfig.PanelType || 'Base';
330
+ let tmpServiceName = `PictFlowCardPropertiesPanel-${tmpPanelType}`;
331
+ let tmpInstance = this._PanelInstances[pPanelData.Hash];
332
+
333
+ if (!tmpInstance)
334
+ {
335
+ if (this.fable.servicesMap.hasOwnProperty(tmpServiceName))
336
+ {
337
+ tmpInstance = this.fable.instantiateServiceProviderWithoutRegistration(tmpServiceName, tmpPanelConfig);
338
+ }
339
+ else if (this.fable.servicesMap.hasOwnProperty('PictFlowCardPropertiesPanel'))
340
+ {
341
+ tmpInstance = this.fable.instantiateServiceProviderWithoutRegistration('PictFlowCardPropertiesPanel', tmpPanelConfig);
342
+ }
343
+ if (tmpInstance)
344
+ {
345
+ tmpInstance._FlowView = this._FlowView;
346
+ this._PanelInstances[pPanelData.Hash] = tmpInstance;
347
+ }
348
+ }
349
+
350
+ if (tmpInstance)
351
+ {
352
+ tmpInstance.render(pBodyContainer, tmpConnectionData);
353
+ }
354
+ }
355
+
301
356
  /**
302
357
  * Render an auto-generated info panel for nodes without a configured PropertiesPanel.
303
358
  * Shows the node type, description, and a summary of input/output ports with
@@ -716,11 +771,23 @@ class PictViewFlowPropertiesPanel extends libPictView
716
771
  let tmpTetherService = this._FlowView._TetherService;
717
772
  if (!tmpTetherService) return;
718
773
 
719
- let tmpNodeData = this._FlowView.getNode(pPanelData.NodeHash);
720
- if (!tmpNodeData) return;
774
+ // A connection panel tethers to the edge midpoint; model it as a zero-size anchor at that
775
+ // point so the same tether geometry applies. A node panel tethers to its node.
776
+ let tmpAnchorData;
777
+ if (pPanelData.ConnectionHash)
778
+ {
779
+ let tmpMidpoint = this._FlowView.getConnectionMidpoint(pPanelData.ConnectionHash);
780
+ if (!tmpMidpoint) return;
781
+ tmpAnchorData = { X: tmpMidpoint.x, Y: tmpMidpoint.y, Width: 0, Height: 0 };
782
+ }
783
+ else
784
+ {
785
+ tmpAnchorData = this._FlowView.getNode(pPanelData.NodeHash);
786
+ if (!tmpAnchorData) return;
787
+ }
721
788
 
722
789
  let tmpViewIdentifier = this._FlowView.options.ViewIdentifier;
723
- tmpTetherService.renderTether(pPanelData, tmpNodeData, pTethersLayer, pIsSelected, tmpViewIdentifier);
790
+ tmpTetherService.renderTether(pPanelData, tmpAnchorData, pTethersLayer, pIsSelected, tmpViewIdentifier);
724
791
  }
725
792
 
726
793
  /**
@@ -71,6 +71,11 @@ const _DefaultConfiguration =
71
71
  DefaultNodeWidth: 180,
72
72
  DefaultNodeHeight: 80,
73
73
 
74
+ // Properties panel for connections (edges). Connections are not typed, so one config serves
75
+ // them all: { PanelType, DefaultWidth, DefaultHeight, Title, Configuration }. When set, a
76
+ // double-click on a connection opens this panel; when false, double-click adds a bezier handle.
77
+ ConnectionPropertiesPanel: false,
78
+
74
79
  // Layout-algorithm subsystem defaults
75
80
  DefaultLayoutAlgorithm: 'Custom',
76
81
  DefaultLayoutParameters: {},
@@ -1328,6 +1333,54 @@ class PictViewFlow extends libPictView
1328
1333
  return this._PanelManager.togglePanel(pNodeHash);
1329
1334
  }
1330
1335
 
1336
+ /**
1337
+ * Open a properties panel for a connection (edge). Requires the ConnectionPropertiesPanel
1338
+ * option; returns false otherwise.
1339
+ * @param {string} pConnectionHash
1340
+ * @returns {Object|false}
1341
+ */
1342
+ openConnectionPanel(pConnectionHash)
1343
+ {
1344
+ return this._PanelManager.openConnectionPanel(pConnectionHash);
1345
+ }
1346
+
1347
+ /**
1348
+ * Toggle a properties panel for a connection.
1349
+ * @param {string} pConnectionHash
1350
+ * @returns {Object|false}
1351
+ */
1352
+ toggleConnectionPanel(pConnectionHash)
1353
+ {
1354
+ return this._PanelManager.toggleConnectionPanel(pConnectionHash);
1355
+ }
1356
+
1357
+ /**
1358
+ * Close all panels for a given connection.
1359
+ * @param {string} pConnectionHash
1360
+ * @returns {boolean}
1361
+ */
1362
+ closePanelForConnection(pConnectionHash)
1363
+ {
1364
+ return this._PanelManager.closePanelForConnection(pConnectionHash);
1365
+ }
1366
+
1367
+ /**
1368
+ * The midpoint of a connection in SVG coordinates, averaged from its two endpoint ports. Used
1369
+ * to place and tether a connection's properties panel. Returns null if the connection or
1370
+ * either port can not be resolved.
1371
+ * @param {string} pConnectionHash
1372
+ * @returns {{x: number, y: number}|null}
1373
+ */
1374
+ getConnectionMidpoint(pConnectionHash)
1375
+ {
1376
+ let tmpConnection = this.getConnection(pConnectionHash);
1377
+ if (!tmpConnection) return null;
1378
+ let tmpSource = this.getPortPosition(tmpConnection.SourceNodeHash, tmpConnection.SourcePortHash);
1379
+ let tmpTarget = this.getPortPosition(tmpConnection.TargetNodeHash, tmpConnection.TargetPortHash);
1380
+ if (!tmpSource || !tmpTarget) return null;
1381
+ return { x: (tmpSource.x + tmpTarget.x) / 2, y: (tmpSource.y + tmpTarget.y) / 2 };
1382
+ }
1383
+
1331
1384
  /**
1332
1385
  * Update a panel's position (for drag).
1333
1386
  * @param {string} pPanelHash
@@ -0,0 +1,172 @@
1
+ const libFable = require('fable');
2
+ const libChai = require('chai');
3
+ const libExpect = libChai.expect;
4
+
5
+ const libPanelManager = require('../source/services/PictService-Flow-PanelManager.js');
6
+
7
+ /**
8
+ * Connection (edge) properties panels. The node-panel path is well covered through the view; these
9
+ * focus on the connection additions: gating on ConnectionPropertiesPanel, placement near the edge
10
+ * midpoint, the open/toggle/close lifecycle, and that node panels are not disturbed.
11
+ */
12
+ suite
13
+ (
14
+ 'PictService-Flow-PanelManager (connection panels)',
15
+ function ()
16
+ {
17
+ let _Fable;
18
+ let _PanelManager;
19
+ let _MockFlowView;
20
+
21
+ setup
22
+ (
23
+ function ()
24
+ {
25
+ _Fable = new libFable({});
26
+
27
+ _MockFlowView =
28
+ {
29
+ fable: _Fable,
30
+ log: _Fable.log,
31
+ options:
32
+ {
33
+ ViewIdentifier: 'Test-Flow',
34
+ ConnectionPropertiesPanel: false
35
+ },
36
+ _FlowData:
37
+ {
38
+ Nodes:
39
+ [
40
+ { Hash: 'n1', Type: 'state', X: 0, Y: 0, Width: 100, Height: 60, Ports: [ { Hash: 'n1-out', Direction: 'output' } ] },
41
+ { Hash: 'n2', Type: 'state', X: 300, Y: 0, Width: 100, Height: 60, Ports: [ { Hash: 'n2-in', Direction: 'input' } ] }
42
+ ],
43
+ Connections:
44
+ [
45
+ { Hash: 'c1', SourceNodeHash: 'n1', SourcePortHash: 'n1-out', TargetNodeHash: 'n2', TargetPortHash: 'n2-in', Data: {} }
46
+ ],
47
+ OpenPanels: [],
48
+ ViewState: { SelectedTetherHash: null }
49
+ },
50
+ getConnection: function (pHash) { return this._FlowData.Connections.find((pConn) => pConn.Hash === pHash) || null; },
51
+ getNode: function (pHash) { return this._FlowData.Nodes.find((pNode) => pNode.Hash === pHash) || null; },
52
+ getConnectionMidpoint: function (pHash) { return this.getConnection(pHash) ? { x: 200, y: 30 } : null; },
53
+ _NodeTypeProvider:
54
+ {
55
+ getNodeType: function () { return { Label: 'State', PropertiesPanel: { PanelType: 'Form', DefaultWidth: 300, DefaultHeight: 220, Title: 'State' } }; }
56
+ },
57
+ renderFlow: function () {},
58
+ marshalFromView: function () {},
59
+ _PropertiesPanelView: { destroyPanel: function () {} },
60
+ _EventHandlerProvider: { fireEvent: function () {} }
61
+ };
62
+
63
+ _PanelManager = new libPanelManager(_Fable, { FlowView: _MockFlowView }, 'PM-Test');
64
+ }
65
+ );
66
+
67
+ test
68
+ (
69
+ 'openConnectionPanel returns false when no ConnectionPropertiesPanel is configured',
70
+ function ()
71
+ {
72
+ let tmpResult = _PanelManager.openConnectionPanel('c1');
73
+ libExpect(tmpResult).to.equal(false);
74
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(0);
75
+ }
76
+ );
77
+
78
+ test
79
+ (
80
+ 'openConnectionPanel returns false for an unknown connection',
81
+ function ()
82
+ {
83
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
84
+ let tmpResult = _PanelManager.openConnectionPanel('no-such-connection');
85
+ libExpect(tmpResult).to.equal(false);
86
+ }
87
+ );
88
+
89
+ test
90
+ (
91
+ 'openConnectionPanel opens a panel carrying the ConnectionHash, placed near the midpoint',
92
+ function ()
93
+ {
94
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form', DefaultWidth: 320, DefaultHeight: 240, Title: 'Transition' };
95
+ let tmpPanel = _PanelManager.openConnectionPanel('c1');
96
+
97
+ libExpect(tmpPanel).to.be.an('object');
98
+ libExpect(tmpPanel.ConnectionHash).to.equal('c1');
99
+ libExpect(tmpPanel.NodeHash).to.equal(null);
100
+ libExpect(tmpPanel.Title).to.equal('Transition');
101
+ libExpect(tmpPanel.Width).to.equal(320);
102
+ libExpect(tmpPanel.Height).to.equal(240);
103
+ // Midpoint is (200, 30); the panel is offset from it.
104
+ libExpect(tmpPanel.X).to.equal(240);
105
+ libExpect(tmpPanel.Y).to.equal(50);
106
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
107
+ }
108
+ );
109
+
110
+ test
111
+ (
112
+ 'openConnectionPanel is idempotent: a second open returns the same panel',
113
+ function ()
114
+ {
115
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
116
+ let tmpFirst = _PanelManager.openConnectionPanel('c1');
117
+ let tmpSecond = _PanelManager.openConnectionPanel('c1');
118
+ libExpect(tmpSecond.Hash).to.equal(tmpFirst.Hash);
119
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
120
+ }
121
+ );
122
+
123
+ test
124
+ (
125
+ 'toggleConnectionPanel opens then closes',
126
+ function ()
127
+ {
128
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
129
+ let tmpOpened = _PanelManager.toggleConnectionPanel('c1');
130
+ libExpect(tmpOpened).to.be.an('object');
131
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
132
+
133
+ let tmpClosed = _PanelManager.toggleConnectionPanel('c1');
134
+ libExpect(tmpClosed).to.equal(false);
135
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(0);
136
+ }
137
+ );
138
+
139
+ test
140
+ (
141
+ 'closePanelForConnection removes the connection panel',
142
+ function ()
143
+ {
144
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
145
+ _PanelManager.openConnectionPanel('c1');
146
+ let tmpRemoved = _PanelManager.closePanelForConnection('c1');
147
+ libExpect(tmpRemoved).to.equal(true);
148
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(0);
149
+ }
150
+ );
151
+
152
+ test
153
+ (
154
+ 'node panels still open alongside connection panels, keyed separately',
155
+ function ()
156
+ {
157
+ _MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
158
+ let tmpNodePanel = _PanelManager.openPanel('n1');
159
+ let tmpConnPanel = _PanelManager.openConnectionPanel('c1');
160
+
161
+ libExpect(tmpNodePanel.NodeHash).to.equal('n1');
162
+ libExpect(tmpConnPanel.ConnectionHash).to.equal('c1');
163
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(2);
164
+
165
+ // Closing the connection panel leaves the node panel intact.
166
+ _PanelManager.closePanelForConnection('c1');
167
+ libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
168
+ libExpect(_MockFlowView._FlowData.OpenPanels[0].NodeHash).to.equal('n1');
169
+ }
170
+ );
171
+ }
172
+ );