dmx-api 2.0.0 → 3.0.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.
package/README.md CHANGED
@@ -2,6 +2,73 @@
2
2
 
3
3
  ## Version History
4
4
 
5
+ **3.0** -- May 19, 2023
6
+
7
+ Version 3.0 of the `dmx-api` library extends/modifies the API in order to support a wider variety of frontend
8
+ applications. Additionally, depending on application type, the launch time is reduced as less data is transferred from
9
+ server ([#497](https://git.dmx.systems/dmx-platform/dmx-platform/-/issues/497),
10
+ [#501](https://git.dmx.systems/dmx-platform/dmx-platform/-/issues/501)). This required some breaking changes in the
11
+ library's `init()` function.
12
+
13
+ * BREAKING CHANGES
14
+ - changed behavior of the library's `init()` function:
15
+ - The client-side type cache is not fully pre-populated by default anymore. Instead the application pass
16
+ `topicTypes` config to pre-populate selectively, or pass `all`. Depending on application type this results in
17
+ less data transfer at application launch.
18
+ - The SVG utility for FontAwesome icons is not initialized by default anymore. Instead an application can
19
+ initialize it on-demand (by calling `dmx.icons.init()`). Applications who don't need it launch quicker as
20
+ downloading the FontAwesome SVG data (450K) is omitted.
21
+ - rename class `Type` -> `DMXType`
22
+ - remove `getPosition()` from `ViewTopic`. Use the new `pos` getter instead.
23
+ * Improvement:
24
+ - The library's `init()` function optionally opens the WebSocket for client-synchronization (if `messageHandler`
25
+ config is passed). The application no longer depends on
26
+ [dmx-websocket](https://github.com/dmx-systems/dmx-websocket) module then.
27
+ - Vanilla [axios](https://github.com/axios/axios) http instance (without error interceptor set by application) is
28
+ exported as `rpc._http`.
29
+ * Model:
30
+ - change `Type`'s `newFormModel()`:
31
+ - `object` parameter is now optional
32
+ - add `allChildren` parameter (optional)
33
+ - add `panX`, `panY`, `zoom`, `bgImageUrl` getters to `Topicmap`
34
+ - add `updateTopic()`, `updateAssoc()` to `Topicmap` (part of
35
+ [dmx-topicmap-panel](https://github.com/dmx-systems/dmx-topicmap-panel) protocol)
36
+ - add `pos` getter to `ViewTopic`
37
+ * RPC:
38
+ - add `getTopicType()` and `getAssocType()`
39
+ - add `includeChildren` parameter to `getTopicmap()`
40
+ - add `includeChildren` and `includeAssocChildren` parameters to `getAssignedWorkspace()`
41
+ - add `getMemberships()` to get the members of a workspace
42
+ - add `bulkUpdateUserMemberships()` and `bulkUpdateWorkspaceMemberships()`
43
+ - add `deleteWorkspace()`
44
+ - change `updateTopic()`: returns `Topic` plus directives (formerly just directives)
45
+ - change `login()`: returned promise resolves with username (formerly undefined)
46
+ * Utils:
47
+ - add `stripHtml()`
48
+ * Fix:
49
+ - fixed a bug where nested entities loose their child values while update request
50
+
51
+ **2.1** -- Jun 13, 2021
52
+
53
+ * Model:
54
+ * add class `RoleType`
55
+ * add `isEditable` getter to `DMXObject`
56
+ * add `isRoleType` getter to `DMXObject`
57
+ * add `asRoleType()` to `Topic`
58
+ * add `arrowShape` and `hollow` getters to `Player`
59
+ * add `isNoneditable` getter to `Type`
60
+ * RPC:
61
+ * add `getAllRoleTypes()`
62
+ * add `getTopicTypeImplicitly()`, `getAssocTypeImplicitly()`, `getRoleTypeImplicitly()`
63
+ * add `getConfigDefs()`, `getConfigTopic()`, `updateConfigTopic()`
64
+ * Utils:
65
+ * add `formatFileSize()`
66
+ * add `round()`
67
+ * Fixes:
68
+ * Model: fix `TopicType.newTopicModel()` regarding identity attributes
69
+ * Model: `_newInstance()` fills in `0` object value as is (not as `''`)
70
+ * Type cache: `initTypeCache` action returns promise
71
+
5
72
  **2.0** -- Dec 30, 2020
6
73
 
7
74
  * BREAKING CHANGES
@@ -198,7 +265,8 @@
198
265
 
199
266
  **0.13** -- Mar 25, 2018
200
267
 
201
- * Model: `Topicmap.revealTopic()`'s `pos` param is optional. If not given it's up to the topicmap renderer to position the topic.
268
+ * Model: `Topicmap.revealTopic()`'s `pos` param is optional. If not given it's up to the topicmap renderer to position
269
+ the topic.
202
270
  * Utils: `clone()` for deep-cloning arbitrary objects
203
271
  * Depends on module `clone` instead `lodash.clonedeep`
204
272
 
@@ -248,4 +316,4 @@
248
316
 
249
317
  ------------
250
318
  Jörg Richter
251
- Dec 30, 2020
319
+ Jun 13, 2021
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "dmx-api",
3
- "version": "2.0.0",
4
- "description": "DMX 5 base types and API",
3
+ "version": "3.0.0",
4
+ "description": "API and utilities for DMX 5 based frontends",
5
5
  "author": "Jörg Richter <jri@dmx.berlin>",
6
6
  "license": "AGPL-3.0",
7
7
  "main": "src/index.js",
8
- "unpkg": "dist/dmx-api.min.js",
9
8
  "repository": {
10
9
  "type": "git",
11
10
  "url": "https://github.com/dmx-systems/dmx-api.git"
package/src/icons.js CHANGED
@@ -6,11 +6,17 @@ import rpc from './rpc'
6
6
  let faFont // Font Awesome SVG <font> element
7
7
  let faDefaultWidth // Default icon width
8
8
 
9
- // a promise resolved once the Font Awesome SVG is loaded
10
- const svgReady = rpc.getXML(fa).then(svg => {
11
- faFont = svg.querySelector('font')
12
- faDefaultWidth = faFont.getAttribute('horiz-adv-x')
13
- })
9
+ /**
10
+ * Initializes the Icons-Toolkit.
11
+ *
12
+ * @returns a promise resolved once the toolkit is ready for `faGlyph()` calls (Font Awesome SVG is loaded).
13
+ */
14
+ function init () {
15
+ return rpc.getXML(fa).then(svg => {
16
+ faFont = svg.querySelector('font')
17
+ faDefaultWidth = faFont.getAttribute('horiz-adv-x')
18
+ })
19
+ }
14
20
 
15
21
  function faGlyph (unicode) {
16
22
  try {
@@ -25,6 +31,6 @@ function faGlyph (unicode) {
25
31
  }
26
32
 
27
33
  export default {
28
- ready: svgReady,
34
+ init,
29
35
  faGlyph
30
36
  }
package/src/index.js CHANGED
@@ -1,22 +1,23 @@
1
- import {
2
- DMXObject, Topic, Assoc, Player, RelatedTopic, Type, TopicType, AssocType, Topicmap, ViewTopic, ViewAssoc,
3
- setIconRenderers
4
- } from './model'
5
- import rpc from './rpc'
6
- import typeCache from './type-cache'
7
- import permCache from './permission-cache'
8
- import utils from './utils'
9
- import icons from './icons'
10
-
11
- console.log('[DMX-API] 2.0')
1
+ import * as model from './model'
2
+ import rpc from './rpc'
3
+ import permCache from './permission-cache'
4
+ import utils from './utils'
5
+ import icons from './icons'
6
+ import DMXWebSocket from './websocket'
7
+ import {default as typeCache, init as initTypeCache, storeModule} from './type-cache'
8
+
9
+ console.log('[DMX-API] 3.0')
12
10
 
13
11
  let adminWorkspaceId // promise
14
12
 
15
- export default {
13
+ const clientId = newClientId()
14
+ updateClientIdCookie()
16
15
 
17
- /* eslint object-property-newline: "off" */
18
- DMXObject, Topic, Assoc, Player, RelatedTopic, Type, TopicType, AssocType, Topicmap, ViewTopic, ViewAssoc,
16
+ window.addEventListener('focus', updateClientIdCookie)
19
17
 
18
+ export default {
19
+
20
+ ...model,
20
21
  rpc,
21
22
  typeCache,
22
23
  permCache,
@@ -24,10 +25,12 @@ export default {
24
25
  icons,
25
26
 
26
27
  init (config) {
27
- config.iconRenderers && setIconRenderers(config.iconRenderers)
28
- config.onHttpError && rpc.setErrorHandler(config.onHttpError)
29
28
  adminWorkspaceId = rpc.getAdminWorkspaceId()
30
- return typeCache.init(config.store)
29
+ config.store.registerModule('typeCache', storeModule)
30
+ config.messageHandler && new DMXWebSocket(rpc.getWebsocketConfig(), config.messageHandler)
31
+ config.onHttpError && rpc.setErrorHandler(config.onHttpError)
32
+ config.iconRenderers && model.setIconRenderers(config.iconRenderers)
33
+ return initTypeCache(config.topicTypes)
31
34
  },
32
35
 
33
36
  /**
@@ -37,3 +40,12 @@ export default {
37
40
  return adminWorkspaceId.then(id => permCache.isWritable(id))
38
41
  }
39
42
  }
43
+
44
+ function updateClientIdCookie () {
45
+ // DEV && console.log('dmx_client_id', clientId)
46
+ utils.setCookie('dmx_client_id', clientId)
47
+ }
48
+
49
+ function newClientId () {
50
+ return Math.floor(Number.MAX_SAFE_INTEGER * Math.random())
51
+ }
package/src/model.js CHANGED
@@ -17,7 +17,7 @@ class DMXObject {
17
17
 
18
18
  constructor (object) {
19
19
  if (!object) {
20
- throw Error(`invalid object passed to DMXObject constructor: ${object}`)
20
+ throw Error(`invalid object passed to DMXObject constructor: ${JSON.stringify(object)}`)
21
21
  } else if (object.constructor.name !== 'Object') {
22
22
  throw Error(`DMXObject constructor expects Object, got ${object.constructor.name} ${JSON.stringify(object)}`)
23
23
  }
@@ -68,6 +68,17 @@ class DMXObject {
68
68
  return rpc.getRelatedTopicsWithoutChilds(this.id)
69
69
  }
70
70
 
71
+ /**
72
+ * @return true if this object is editable *in principle*, independent of user's authorization.
73
+ *
74
+ * For entity topics, assocs and types <code>true</code> is returned, provided "Noneditable" is
75
+ * not set in the type's view config.
76
+ * For value topics, or if "Noneditable" is set, <code>false</code> is returned.
77
+ */
78
+ get isEditable () {
79
+ return (this.isAssoc || this.isType || this.type.isEntity) && !this.type.isNoneditable
80
+ }
81
+
71
82
  /**
72
83
  * @return a promise for a true/false value
73
84
  */
@@ -153,6 +164,10 @@ class Topic extends DMXObject {
153
164
  this.typeUri === 'dmx.core.assoc_type'
154
165
  }
155
166
 
167
+ get isRoleType () {
168
+ return this.typeUri === 'dmx.core.role_type'
169
+ }
170
+
156
171
  get isCompDef () {
157
172
  return false // topics are never comp defs
158
173
  }
@@ -171,7 +186,6 @@ class Topic extends DMXObject {
171
186
  }
172
187
 
173
188
  update () {
174
- console.log('update', this)
175
189
  return rpc.updateTopic(this)
176
190
  }
177
191
 
@@ -207,6 +221,14 @@ class Topic extends DMXObject {
207
221
  throw Error(`not a type: ${this}`)
208
222
  }
209
223
  }
224
+
225
+ asRoleType () {
226
+ if (this.isRoleType) {
227
+ return typeCache.getRoleType(this.uri)
228
+ } else {
229
+ throw Error(`not a role type: ${this}`)
230
+ }
231
+ }
210
232
  }
211
233
 
212
234
  class Assoc extends DMXObject {
@@ -270,6 +292,10 @@ class Assoc extends DMXObject {
270
292
  return false // assocs are never types
271
293
  }
272
294
 
295
+ get isRoleType () {
296
+ return false // assocs are never role types
297
+ }
298
+
273
299
  get isCompDef () {
274
300
  return this.typeUri === 'dmx.core.composition_def'
275
301
  }
@@ -288,7 +314,6 @@ class Assoc extends DMXObject {
288
314
  }
289
315
 
290
316
  update () {
291
- console.log('update', this)
292
317
  return rpc.updateAssoc(this)
293
318
  }
294
319
 
@@ -343,6 +368,14 @@ class Player {
343
368
  return this.getRoleType().value
344
369
  }
345
370
 
371
+ get arrowShape () {
372
+ return this.getRoleType().getViewConfig('dmx.webclient.arrow_shape') || 'none'
373
+ }
374
+
375
+ get hollow () {
376
+ return this.getRoleType().getViewConfig('dmx.webclient.hollow') || false
377
+ }
378
+
346
379
  isTopicPlayer () {
347
380
  return this.topicId >= 0 // Note: 0 is a valid topic ID
348
381
  }
@@ -380,13 +413,13 @@ class RelatedTopic extends Topic {
380
413
  }
381
414
  }
382
415
 
383
- class Type extends Topic {
416
+ class DMXType extends Topic {
384
417
 
385
418
  constructor (type) {
386
419
  super(type)
387
420
  this.dataTypeUri = type.dataTypeUri
388
421
  this.compDefs = utils.instantiateMany(type.compDefs, CompDef)
389
- this.viewConfig = utils.mapByTypeUri(utils.instantiateMany(type.viewConfigTopics, Topic)) // TODO: rename prop?
422
+ this.viewConfig = utils.mapByTypeUri(utils.instantiateMany(type.viewConfigTopics, Topic))
390
423
  }
391
424
 
392
425
  get isSimple () {
@@ -418,6 +451,7 @@ class Type extends Topic {
418
451
  }
419
452
 
420
453
  // ### TODO: copy in CompDef
454
+ // ### TODO: copy in RoleType
421
455
  getViewConfig (childTypeUri) {
422
456
  // TODO: don't hardcode config type URI
423
457
  const configTopic = this.viewConfig['dmx.webclient.view_config']
@@ -437,18 +471,32 @@ class Type extends Topic {
437
471
  return this.getViewConfig('dmx.webclient.color#dmx.webclient.background_color')
438
472
  }
439
473
 
474
+ get isNoneditable () {
475
+ return this.getViewConfig('dmx.webclient.noneditable')
476
+ }
477
+
440
478
  /**
441
479
  * Creates a form model for this type.
442
480
  *
443
- * @param object optional, if given its values are filled in. ### FIXDOC
444
- * The object is expected to be of *this* type.
481
+ * @param object optional, if given 1) its values are filled in, and 2) is manipulated in-place, that
482
+ * includes a) adding empty fields, and b) removing fields when "Reduced Details" is
483
+ * switched on (the default, see "allChildren" parameter).
484
+ * The given object is expected to be of *this* type.
485
+ * @param allChildren optional, if true all children are included in the returned form model, that is
486
+ * "Reduced Details" is switched off. Default is false.
445
487
  *
446
- * @return the created form model. ### FIXDOC
488
+ * @return the created form model. If "object" is given the (in-place manipulated) object is returned.
489
+ * Otherwise a newly created form model with empty values is returned.
447
490
  */
448
- newFormModel (object) {
449
- const o = this._newFormModel(object)
450
- object.children = utils.instantiateChildren(o.children)
451
- return object
491
+ newFormModel (object, allChildren) {
492
+ const o = this._newFormModel(object, allChildren)
493
+ if (object) {
494
+ object.children = utils.instantiateChildren(o.children)
495
+ // FIXME: o.assoc is not used
496
+ return object
497
+ } else {
498
+ return o
499
+ }
452
500
  }
453
501
 
454
502
  /**
@@ -457,14 +505,14 @@ class Type extends Topic {
457
505
  *
458
506
  * @return a newly constructed plain object
459
507
  */
460
- _newFormModel (object) {
508
+ _newFormModel (object, allChildren, level = 0, compDef) {
461
509
 
462
510
  function _newFormModel (object, type, level, compDef) {
463
511
  const o = type._newInstance(object)
464
512
  if (type.isComposite) {
465
513
  type.compDefs.forEach(compDef => {
466
514
  // Reduced details: at deeper levels for entity types only their identity attributes are included
467
- if (level === 0 || type.isValue || compDef.isIdentityAttr) {
515
+ if (allChildren || level === 0 || type.isValue || compDef.isIdentityAttr) {
468
516
  const compDefUri = compDef.compDefUri
469
517
  const childType = compDef.childType
470
518
  const child = object && object.children[compDefUri]
@@ -487,45 +535,42 @@ class Type extends Topic {
487
535
  return o
488
536
  }
489
537
 
490
- return _newFormModel(object, this, 0)
538
+ return _newFormModel(object, this, level, compDef)
491
539
  }
492
540
 
493
541
  _newInstance (object) {
494
- // console.log('_newInstance', this, object)
495
- const o = {
496
- id: object && object.id || -1, /* eslint no-mixed-operators: "off" */
497
- uri: object && object.uri || '',
498
- typeUri: object && object.typeUri || this.uri,
499
- value: object && object.value || '',
542
+ return {
543
+ id: object ? object.id : -1,
544
+ uri: object ? object.uri : '',
545
+ typeUri: object ? object.typeUri : this.uri,
546
+ value: object ? object.value : '',
500
547
  children: {}
501
548
  }
502
- return o
503
549
  }
504
550
  }
505
551
 
506
- class TopicType extends Type {
552
+ class TopicType extends DMXType {
507
553
 
508
- // TODO: drop this method in favor of newFormModel() and fill in the default value(s) afterwards?
509
554
  /**
510
- * @returns a plain object.
555
+ * Creates a form model for this topic type, and fills in the given value.
556
+ *
557
+ * @returns a newly constructed plain object.
511
558
  */
512
559
  newTopicModel (simpleValue) {
560
+ const topic = this._newFormModel()
561
+ initTopicValue(topic, this)
562
+ return topic
513
563
 
514
- function _newTopicModel (type) {
515
- const topic = {typeUri: type.uri}
564
+ function initTopicValue (topic, type) {
516
565
  if (type.isSimple) {
517
566
  topic.value = simpleValue
518
567
  } else {
519
568
  const compDef = type.compDefs[0]
520
- const child = _newTopicModel(compDef.childType)
521
- topic.children = {
522
- [compDef.compDefUri]: compDef.isOne ? child : [child]
523
- }
569
+ const children = topic.children[compDef.compDefUri]
570
+ const child = compDef.isOne ? children : children[0]
571
+ initTopicValue(child, compDef.childType)
524
572
  }
525
- return topic
526
573
  }
527
-
528
- return _newTopicModel(this)
529
574
  }
530
575
 
531
576
  get icon () {
@@ -549,7 +594,7 @@ class TopicType extends Type {
549
594
  }
550
595
  }
551
596
 
552
- class AssocType extends Type {
597
+ class AssocType extends DMXType {
553
598
 
554
599
  get isTopicType () {
555
600
  return false
@@ -591,8 +636,6 @@ class CompDef extends Assoc {
591
636
  if (isIdentityAttr) {
592
637
  this.isIdentityAttr = isIdentityAttr.value
593
638
  } else {
594
- // ### TODO: should an isIdentityAttr child always exist?
595
- // console.warn(`Comp def ${this.compDefUri} has no identity_attr child (parent type: ${this.parentTypeUri})`)
596
639
  this.isIdentityAttr = false
597
640
  }
598
641
  //
@@ -600,8 +643,6 @@ class CompDef extends Assoc {
600
643
  if (includeInLabel) {
601
644
  this.includeInLabel = includeInLabel.value
602
645
  } else {
603
- // ### TODO: should an includeInLabel child always exist?
604
- // console.warn(`Comp def ${this.compDefUri} has no include_in_label child (parent type: ${this.parentTypeUri})`)
605
646
  this.includeInLabel = false
606
647
  }
607
648
  }
@@ -638,7 +679,7 @@ class CompDef extends Assoc {
638
679
  return topic && topic.value
639
680
  }
640
681
 
641
- // ### TODO: principal copy in Type
682
+ // ### TODO: principal copy in DMXType
642
683
  _getViewConfig (childTypeUri) {
643
684
  // TODO: don't hardcode config type URI
644
685
  const configTopic = this.viewConfig['dmx.webclient.view_config']
@@ -658,13 +699,32 @@ class CompDef extends Assoc {
658
699
  return 'dmx.core.composition'
659
700
  }
660
701
 
661
- emptyChildInstance () {
662
- const topic = this.childType._newFormModel()
702
+ emptyChildInstance (level) {
703
+ const topic = this.childType._newFormModel(undefined, false, level, this)
663
704
  topic.assoc = this.instanceLevelAssocType._newFormModel()
664
705
  return new Topic(topic)
665
706
  }
666
707
  }
667
708
 
709
+ class RoleType extends Topic {
710
+
711
+ constructor (roleType) {
712
+ super(roleType)
713
+ this.viewConfig = utils.mapByTypeUri(utils.instantiateMany(roleType.viewConfigTopics, Topic))
714
+ }
715
+
716
+ getViewConfig (childTypeUri) {
717
+ // TODO: don't hardcode config type URI
718
+ const configTopic = this.viewConfig['dmx.webclient.view_config']
719
+ if (!configTopic) {
720
+ // console.warn(`Type "${this.uri}" has no view config`)
721
+ return
722
+ }
723
+ const topic = configTopic.children[childTypeUri]
724
+ return topic && topic.value
725
+ }
726
+ }
727
+
668
728
  class Topicmap extends Topic {
669
729
 
670
730
  constructor (topicmap) {
@@ -741,17 +801,16 @@ class Topicmap extends Topic {
741
801
  }
742
802
 
743
803
  /**
744
- * Returns the position of the given topic/assoc.
745
- *
746
- * Note: ViewTopic has getPosition() too but ViewAssoc has not
747
- * as a ViewAssoc doesn't know the Topicmap it belongs to.
804
+ * Returns the position of the given topic/assoc of this topicmap.
748
805
  *
749
806
  * @param id a topic ID or an assoc ID
750
807
  */
751
808
  getPosition (id) {
809
+ // Note: a ViewAssoc can't have a `pos` getter (like ViewTopic) as assoc pos is calculated from the player objects
810
+ // and the assoc only has its IDs and no reference to the Topicmap. So we make `getPosition()` a Topicmap method.
752
811
  const o = this.getObject(id)
753
812
  if (o.isTopic) {
754
- return o.getPosition()
813
+ return o.pos
755
814
  } else {
756
815
  const pos1 = this.getPosition(o.player1.id)
757
816
  const pos2 = this.getPosition(o.player2.id)
@@ -763,22 +822,28 @@ class Topicmap extends Topic {
763
822
  }
764
823
 
765
824
  /**
825
+ * Adds the given topic to this topicmap.
826
+ * If the topic is contained in the topicmap already (ID-check) it is replaced.
827
+ *
766
828
  * @param topic a dmx.ViewTopic
767
829
  */
768
830
  addTopic (topic) {
769
831
  if (!(topic instanceof ViewTopic)) {
770
- throw Error(`addTopic() expects a ViewTopic, got ${topic.constructor.name}`)
832
+ throw Error(`addTopic() expects ViewTopic, got ${topic.constructor.name}`)
771
833
  }
772
834
  // reactivity is required to trigger "visibleTopicIds" getter (module dmx-cytoscape-renderer)
773
835
  Vue.set(this._topics, topic.id, topic)
774
836
  }
775
837
 
776
838
  /**
839
+ * Adds the given assoc to this topicmap.
840
+ * If the assoc is contained in the topicmap already (ID-check) it is replaced.
841
+ *
777
842
  * @param assoc a dmx.ViewAssoc
778
843
  */
779
844
  addAssoc (assoc) {
780
845
  if (!(assoc instanceof ViewAssoc)) {
781
- throw Error(`addAssoc() expects a ViewAssoc, got ${assoc.constructor.name}`)
846
+ throw Error(`addAssoc() expects ViewAssoc, got ${assoc.constructor.name}`)
782
847
  }
783
848
  // reactivity is required to trigger "visibleAssocIds" getter (module dmx-cytoscape-renderer)
784
849
  Vue.set(this._assocs, assoc.id, assoc)
@@ -850,6 +915,21 @@ class Topicmap extends Topic {
850
915
  return op
851
916
  }
852
917
 
918
+ // the following 4 methods are part of the Topicmap Panel protocol
919
+
920
+ updateTopic (topic) {
921
+ if (this.id === topic.id) {
922
+ Vue.set(
923
+ this.children,
924
+ 'dmx.base.url#dmx.topicmaps.background_image',
925
+ topic.children['dmx.base.url#dmx.topicmaps.background_image']
926
+ )
927
+ }
928
+ }
929
+
930
+ updateAssoc (assoc) {
931
+ }
932
+
853
933
  /**
854
934
  * Note: if the topic is not in this topicmap nothing is performed.
855
935
  */
@@ -910,6 +990,23 @@ class Topicmap extends Topic {
910
990
 
911
991
  // Topicmap
912
992
 
993
+ get panX () {
994
+ return this.viewProps['dmx.topicmaps.pan_x']
995
+ }
996
+
997
+ get panY () {
998
+ return this.viewProps['dmx.topicmaps.pan_y']
999
+ }
1000
+
1001
+ get zoom () {
1002
+ return this.viewProps['dmx.topicmaps.zoom']
1003
+ }
1004
+
1005
+ get bgImageUrl () {
1006
+ const url = this.children['dmx.base.url#dmx.topicmaps.background_image']
1007
+ return url && url.value
1008
+ }
1009
+
913
1010
  setViewport (pan, zoom) {
914
1011
  this.viewProps['dmx.topicmaps.pan_x'] = pan.x
915
1012
  this.viewProps['dmx.topicmaps.pan_y'] = pan.y
@@ -965,8 +1062,7 @@ class ViewTopic extends viewPropsMixin(Topic) {
965
1062
  return rpc.getTopic(this.id, true, true)
966
1063
  }
967
1064
 
968
- // TODO: make it a "pos" getter?
969
- getPosition () {
1065
+ get pos () {
970
1066
  return {
971
1067
  x: this.getViewProp('dmx.topicmaps.x'),
972
1068
  y: this.getViewProp('dmx.topicmaps.y')
@@ -1004,9 +1100,10 @@ export {
1004
1100
  Assoc,
1005
1101
  Player,
1006
1102
  RelatedTopic,
1007
- Type,
1103
+ DMXType,
1008
1104
  TopicType,
1009
1105
  AssocType,
1106
+ RoleType,
1010
1107
  Topicmap,
1011
1108
  ViewTopic,
1012
1109
  ViewAssoc,
package/src/rpc.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import http from 'axios'
2
2
  import permCache from './permission-cache'
3
3
  import utils from './utils'
4
- import {Topic, Assoc, RelatedTopic, TopicType, AssocType, Topicmap} from './model'
4
+ import {Topic, Assoc, RelatedTopic, TopicType, AssocType, RoleType, Topicmap} from './model'
5
5
 
6
6
  // Vanilla instance without error interceptor.
7
7
  // In contrast the default http instance allows the caller to set an error handler (see setErrorHandler()).
@@ -108,9 +108,11 @@ export default {
108
108
  },
109
109
 
110
110
  updateTopic (topicModel) {
111
- return http.put(`/core/topic/${topicModel.id}`, topicModel).then(response =>
112
- response.data
113
- )
111
+ return http.put(`/core/topic/${topicModel.id}`, topicModel).then(response => {
112
+ const topic = new Topic(response.data)
113
+ topic.directives = response.data.directives
114
+ return topic
115
+ })
114
116
  },
115
117
 
116
118
  deleteTopic (id) {
@@ -204,6 +206,18 @@ export default {
204
206
 
205
207
  // Topic Types
206
208
 
209
+ getTopicType (topicTypeUri) {
210
+ return http.get(`/core/topic-type/${topicTypeUri}`).then(response =>
211
+ new TopicType(response.data)
212
+ )
213
+ },
214
+
215
+ getTopicTypeImplicitly (topicId) {
216
+ return http.get(`/core/topic-type/topic/${topicId}`).then(response =>
217
+ new TopicType(response.data)
218
+ )
219
+ },
220
+
207
221
  getAllTopicTypes () {
208
222
  return http.get('/core/topic-types').then(response =>
209
223
  utils.instantiateMany(response.data, TopicType)
@@ -224,6 +238,18 @@ export default {
224
238
 
225
239
  // Association Types
226
240
 
241
+ getAssocType (assocTypeUri) {
242
+ return http.get(`/core/assoc-type/${assocTypeUri}`).then(response =>
243
+ new AssocType(response.data)
244
+ )
245
+ },
246
+
247
+ getAssocTypeImplicitly (assocId) {
248
+ return http.get(`/core/assoc-type/assoc/${assocId}`).then(response =>
249
+ new AssocType(response.data)
250
+ )
251
+ },
252
+
227
253
  getAllAssocTypes () {
228
254
  return http.get('/core/assoc-types').then(response =>
229
255
  utils.instantiateMany(response.data, AssocType)
@@ -244,9 +270,21 @@ export default {
244
270
 
245
271
  // Role Types
246
272
 
247
- createRoleType (topicModel) {
248
- return http.post('/core/roletype', topicModel).then(response =>
249
- new Topic(response.data)
273
+ getRoleTypeImplicitly (assocId, roleTypeUri) {
274
+ return http.get(`/core/role-type/${roleTypeUri}/assoc/${assocId}`).then(response =>
275
+ new RoleType(response.data)
276
+ )
277
+ },
278
+
279
+ getAllRoleTypes () {
280
+ return http.get('/core/role-types').then(response =>
281
+ utils.instantiateMany(response.data, RoleType)
282
+ )
283
+ },
284
+
285
+ createRoleType (roleTypeModel) {
286
+ return http.post('/core/role-type', roleTypeModel).then(response =>
287
+ new RoleType(response.data)
250
288
  )
251
289
  },
252
290
 
@@ -276,8 +314,12 @@ export default {
276
314
  )
277
315
  },
278
316
 
279
- getTopicmap (topicmapId) {
280
- return http.get(`/topicmaps/${topicmapId}`).then(response =>
317
+ getTopicmap (topicmapId, includeChildren) {
318
+ return http.get(`/topicmaps/${topicmapId}`, {
319
+ params: {
320
+ children: includeChildren,
321
+ }
322
+ }).then(response =>
281
323
  new Topicmap(response.data)
282
324
  )
283
325
  },
@@ -344,7 +386,6 @@ export default {
344
386
  },
345
387
 
346
388
  setTopicmapViewport: utils.debounce((topicmapId, pan, zoom) => {
347
- // console.log('setTopicmapViewport')
348
389
  roundPos(pan, 'x', 'y')
349
390
  http.put(`/topicmaps/${topicmapId}/pan/${pan.x}/${pan.y}/zoom/${zoom}`)
350
391
  }, 3000),
@@ -362,6 +403,10 @@ export default {
362
403
  )
363
404
  },
364
405
 
406
+ deleteWorkspace (workspaceId) {
407
+ http.delete(`/workspaces/${workspaceId}`)
408
+ },
409
+
365
410
  getAssignedTopics (workspaceId, topicTypeUri, includeChildren, includeAssocChildren) {
366
411
  return http.get(`/workspaces/${workspaceId}/topics/${topicTypeUri}`, {
367
412
  params: {
@@ -376,8 +421,13 @@ export default {
376
421
  /**
377
422
  * @return the workspace topic, or empty string if no workspace is assigned
378
423
  */
379
- getAssignedWorkspace (objectId) {
380
- return http.get(`/workspaces/object/${objectId}`).then(response =>
424
+ getAssignedWorkspace (objectId, includeChildren, includeAssocChildren) {
425
+ return http.get(`/workspaces/object/${objectId}`, {
426
+ params: {
427
+ children: includeChildren,
428
+ assocChildren: includeAssocChildren
429
+ }
430
+ }).then(response =>
381
431
  // Note: if no workspace is assigned the response is 204 No Content; "data" is the empty string then
382
432
  response.data && new Topic(response.data)
383
433
  )
@@ -398,7 +448,10 @@ export default {
398
448
  headers: {
399
449
  Authorization: authMethod + ' ' + btoa(credentials.username + ':' + credentials.password)
400
450
  }
401
- }).then(() => permCache.clear())
451
+ }).then(() => {
452
+ permCache.clear()
453
+ return credentials.username
454
+ })
402
455
  },
403
456
 
404
457
  logout () {
@@ -424,6 +477,34 @@ export default {
424
477
  )
425
478
  },
426
479
 
480
+ getMemberships (id) {
481
+ return http.get(`/access-control/workspace/${id}/memberships`).then(response =>
482
+ utils.instantiateMany(response.data, RelatedTopic)
483
+ )
484
+ },
485
+
486
+ bulkUpdateUserMemberships (username, addWorkspaceIds, removeWorkspaceIds) {
487
+ return http.put(`/access-control/user/${username}`, undefined, {
488
+ params: {
489
+ addWorkspaceIds: addWorkspaceIds.join(','),
490
+ removeWorkspaceIds: removeWorkspaceIds.join(',')
491
+ }
492
+ }).then(response =>
493
+ utils.instantiateMany(response.data, RelatedTopic)
494
+ )
495
+ },
496
+
497
+ bulkUpdateWorkspaceMemberships (workspaceId, addUserIds, removeUserIds) {
498
+ return http.put(`/access-control/workspace/${workspaceId}`, undefined, {
499
+ params: {
500
+ addUserIds: addUserIds.join(','),
501
+ removeUserIds: removeUserIds.join(',')
502
+ }
503
+ }).then(response =>
504
+ utils.instantiateMany(response.data, RelatedTopic)
505
+ )
506
+ },
507
+
427
508
  getAdminWorkspaceId () {
428
509
  return http.get('/access-control/workspace/admin/id').then(response =>
429
510
  response.data
@@ -467,6 +548,31 @@ export default {
467
548
  )
468
549
  },
469
550
 
551
+ // === Config ===
552
+
553
+ getConfigDefs () {
554
+ return http.get(`/config`).then(response =>
555
+ response.data
556
+ )
557
+ },
558
+
559
+ getConfigTopic (configTypeUri, topicId, includeChildren, includeAssocChildren) {
560
+ return http.get(`/config/${configTypeUri}/topic/${topicId}`, {
561
+ params: {
562
+ children: includeChildren,
563
+ assocChildren: includeAssocChildren
564
+ }
565
+ }).then(response =>
566
+ new RelatedTopic(response.data)
567
+ )
568
+ },
569
+
570
+ updateConfigTopic(topicId, configTopic) {
571
+ return http.put(`/config/topic/${topicId}`, configTopic).then(response =>
572
+ response.data.directives
573
+ )
574
+ },
575
+
470
576
  // === Timestamps ===
471
577
 
472
578
  getCreationTime (id) {
@@ -499,7 +605,9 @@ export default {
499
605
  return Promise.reject(error)
500
606
  }
501
607
  )
502
- }
608
+ },
609
+
610
+ _http
503
611
  }
504
612
 
505
613
  function toPath (idLists) {
package/src/type-cache.js CHANGED
@@ -1,16 +1,17 @@
1
- import {Topic, TopicType, AssocType} from './model'
1
+ import {Topic, TopicType, AssocType, RoleType} from './model'
2
2
  import rpc from './rpc'
3
3
  import utils from './utils'
4
4
  import Vue from 'vue'
5
5
 
6
+ const typeP = {} // intermediate type promises
7
+
6
8
  // Note: the type cache is reactive state. E.g. new topic types appear in the Search Widget's
7
9
  // type menu automatically (see "createTopicTypes" getter in search.js of module dmx-search).
8
-
9
10
  const state = {
10
- topicTypes: undefined, // object: type URI (string) -> TopicType
11
- assocTypes: undefined, // object: type URI (string) -> AssocType
12
- dataTypes: undefined, // object: data type URI (string) -> data type (Topic)
13
- roleTypes: undefined // object: role type URI (string) -> role type (Topic)
11
+ topicTypes: {}, // object: topic type URI (string) -> TopicType
12
+ assocTypes: {}, // object: assoc type URI (string) -> AssocType
13
+ roleTypes: {}, // object: role type URI (string) -> RoleType
14
+ dataTypes: {} // object: data type URI (string) -> data type (Topic)
14
15
  }
15
16
 
16
17
  const actions = {
@@ -27,6 +28,13 @@ const actions = {
27
28
  _putRoleType(roleType)
28
29
  },
29
30
 
31
+ /**
32
+ * Re-populates the type cache with *all* types readable by current user.
33
+ */
34
+ initTypeCache () {
35
+ return initAllTypes()
36
+ },
37
+
30
38
  // WebSocket messages
31
39
 
32
40
  _newTopicType (_, {topicType}) {
@@ -70,9 +78,12 @@ const actions = {
70
78
  case 'UPDATE_TOPIC_TYPE':
71
79
  putTopicType(dir.arg)
72
80
  break
73
- case 'UPDATE_ASSOCIATION_TYPE':
81
+ case 'UPDATE_ASSOC_TYPE':
74
82
  putAssocType(dir.arg)
75
83
  break
84
+ case 'UPDATE_ROLE_TYPE':
85
+ putRoleType(dir.arg)
86
+ break
76
87
  }
77
88
  // Note: role types are never updated as they are simple values and thus immutable
78
89
  })
@@ -83,10 +94,10 @@ const actions = {
83
94
  case 'DELETE_TOPIC_TYPE':
84
95
  removeTopicType(dir.arg.uri)
85
96
  break
86
- case 'DELETE_ASSOCIATION_TYPE':
97
+ case 'DELETE_ASSOC_TYPE':
87
98
  removeAssocType(dir.arg.uri)
88
99
  break
89
- case 'DELETE_TOPIC':
100
+ case 'DELETE_TOPIC': // TODO: DELETE_ROLE_TYPE
90
101
  if (dir.arg.typeUri === 'dmx.core.role_type') {
91
102
  removeRoleType(dir.arg.uri)
92
103
  }
@@ -97,29 +108,17 @@ const actions = {
97
108
  }
98
109
  }
99
110
 
100
- function init (store) {
101
- store.registerModule('typeCache', {
102
- state,
103
- actions
104
- })
105
- // init state
106
- return Promise.all([
107
- rpc.getAllTopicTypes().then(topicTypes => {
108
- state.topicTypes = utils.mapByUri(topicTypes)
109
- _putTopicType(bootstrapType())
110
- }),
111
- rpc.getAllAssocTypes().then(assocTypes => {
112
- state.assocTypes = utils.mapByUri(assocTypes)
113
- }),
114
- rpc.getTopicsByType('dmx.core.data_type').then(dataTypes => {
115
- state.dataTypes = utils.mapByUri(dataTypes)
116
- }),
117
- rpc.getTopicsByType('dmx.core.role_type').then(roleTypes => {
118
- state.roleTypes = utils.mapByUri(roleTypes)
119
- })
120
- ]).then(() => {
121
- // console.log('### Type cache ready!')
122
- })
111
+ function init (topicTypes) {
112
+ if (topicTypes) {
113
+ let p
114
+ if (topicTypes === 'all') {
115
+ p = initAllTypes()
116
+ } else {
117
+ topicTypes.forEach(_initTopicType)
118
+ // TODO: return promise
119
+ }
120
+ return p
121
+ }
123
122
  }
124
123
 
125
124
  // ---
@@ -141,9 +140,9 @@ function getRoleType (uri) {
141
140
  }
142
141
 
143
142
  function getType (uri, className, prop) {
144
- const type = state[prop] && state[prop][uri]
143
+ const type = _getType(uri, prop)
145
144
  if (!type) {
146
- throw Error(`unknown ${className} "${uri}"`)
145
+ throw Error(`${className} "${uri}" not in type cache`)
147
146
  }
148
147
  return type
149
148
  }
@@ -161,7 +160,7 @@ function getTypeById (id) {
161
160
 
162
161
  // TODO: the following 4 functions return async data so they should return promise
163
162
 
164
- // IMPORTANT: call these methods only from a reactive context, otherwise you might get undefined
163
+ // IMPORTANT: calling these methods must be synced, otherwise you might get undefined
165
164
 
166
165
  function getAllTopicTypes () {
167
166
  return getAllTypes('topicTypes')
@@ -188,6 +187,67 @@ function getAllTypes (prop) {
188
187
 
189
188
  // ---
190
189
 
190
+ function initAllTypes () {
191
+ return Promise.all([
192
+ // init state
193
+ rpc.getAllTopicTypes().then(topicTypes => {
194
+ state.topicTypes = utils.mapByUri(topicTypes)
195
+ _putTopicType(bootstrapType())
196
+ }),
197
+ rpc.getAllAssocTypes().then(assocTypes => {
198
+ state.assocTypes = utils.mapByUri(assocTypes)
199
+ }),
200
+ rpc.getAllRoleTypes().then(roleTypes => {
201
+ state.roleTypes = utils.mapByUri(roleTypes)
202
+ }),
203
+ rpc.getTopicsByType('dmx.core.data_type').then(dataTypes => {
204
+ state.dataTypes = utils.mapByUri(dataTypes)
205
+ })
206
+ ])
207
+ }
208
+
209
+ function _initTopicType (uri) {
210
+ _initType(uri, 'topicTypes', TopicType, rpc.getTopicType).then(topicType => {
211
+ topicType.compDefs.forEach(compDef => {
212
+ _initTopicType(compDef.childTypeUri)
213
+ _initAssocType(compDef.instanceLevelAssocTypeUri)
214
+ })
215
+ })
216
+ }
217
+
218
+ function _initAssocType (uri) {
219
+ _initType(uri, 'assocTypes', AssocType, rpc.getAssocType).then(assocType => {
220
+ assocType.compDefs.forEach(compDef => {
221
+ _initTopicType(compDef.childTypeUri)
222
+ // TODO: call _initAssocType()?
223
+ })
224
+ })
225
+ }
226
+
227
+ function _initType (uri, prop, typeClass, fetchFunc) {
228
+ const type = _getType(uri, prop)
229
+ if (type) {
230
+ return Promise.resolve(type)
231
+ } else {
232
+ let p = typeP[uri]
233
+ if (!p) {
234
+ p = fetchFunc(uri).then(type => {
235
+ _putType(type, typeClass, prop)
236
+ delete typeP[uri]
237
+ return type
238
+ })
239
+ typeP[uri] = p
240
+ }
241
+ return p
242
+ }
243
+ }
244
+
245
+ function _getType (uri, prop) {
246
+ return state[prop] && state[prop][uri]
247
+ }
248
+
249
+ // ---
250
+
191
251
  function putTopicType (topicType) {
192
252
  _putTopicType(new TopicType(topicType))
193
253
  }
@@ -197,7 +257,7 @@ function putAssocType (assocType) {
197
257
  }
198
258
 
199
259
  function putRoleType (roleType) {
200
- _putRoleType(new Topic(roleType))
260
+ _putRoleType(new RoleType(roleType))
201
261
  }
202
262
 
203
263
  // ---
@@ -211,7 +271,7 @@ function _putAssocType (assocType) {
211
271
  }
212
272
 
213
273
  function _putRoleType (roleType) {
214
- _putType(roleType, Topic, 'roleTypes')
274
+ _putType(roleType, RoleType, 'roleTypes')
215
275
  }
216
276
 
217
277
  function _putType (type, typeClass, prop) {
@@ -252,8 +312,9 @@ function bootstrapType () {
252
312
  })
253
313
  }
254
314
 
315
+ // public API
316
+
255
317
  export default {
256
- init,
257
318
  getTopicType,
258
319
  getAssocType,
259
320
  getDataType,
@@ -264,3 +325,8 @@ export default {
264
325
  getAllDataTypes,
265
326
  getAllRoleTypes
266
327
  }
328
+
329
+ // module internal API
330
+
331
+ export {init}
332
+ export const storeModule = {state, actions}
package/src/utils.js CHANGED
@@ -159,6 +159,37 @@ function fulltextQuery (input, allowSingleLetterSearch) {
159
159
 
160
160
  // ---
161
161
 
162
+ /**
163
+ * @param size File size in bytes.
164
+ */
165
+ function formatFileSize (size) {
166
+ const units = ["bytes", "KB", "MB", "GB"]
167
+ let i
168
+ for (i = 0; i <= 2; i++) {
169
+ if (size < 1024) {
170
+ return result()
171
+ }
172
+ size /= 1024
173
+ }
174
+ return result()
175
+
176
+ function result() {
177
+ const decimals = Math.max(i - 1, 0)
178
+ return round(size, decimals) + " " + units[i]
179
+ }
180
+ }
181
+
182
+ function round (val, decimals) {
183
+ const factor = Math.pow(10, decimals)
184
+ return Math.round(factor * val) / factor
185
+ }
186
+
187
+ function stripHtml (html) {
188
+ return html.replace(/<.*?>/g, '') // *? is the reluctant version of the * quantifier (which is greedy)
189
+ }
190
+
191
+ // ---
192
+
162
193
  export default {
163
194
  instantiateMany,
164
195
  instantiateChildren,
@@ -173,5 +204,8 @@ export default {
173
204
  getCookie,
174
205
  setCookie,
175
206
  deleteCookie,
176
- fulltextQuery
207
+ fulltextQuery,
208
+ formatFileSize,
209
+ round,
210
+ stripHtml
177
211
  }
@@ -0,0 +1,77 @@
1
+ const IDLE_INTERVAL = 60 * 1000 // 60s
2
+ const RECONNECT_DELAY = 5 * 1000 // 5s
3
+
4
+ /**
5
+ * A WebSocket connection to the DMX server.
6
+ *
7
+ * The URL to connect to is determined automatically, based on the server-side `dmx.websockets.url` config property.
8
+ * WebSocket messages are expected to be JSON. Serialization/Deserialization performs automatically.
9
+ *
10
+ * Properties:
11
+ * `url` - url of the WebSocket server
12
+ * `ws` - the native WebSocket object
13
+ */
14
+ export default class DMXWebSocket {
15
+
16
+ /**
17
+ * @param messageHandler
18
+ * the function that processes incoming messages.
19
+ * One argument is passed: the message pushed by the server (a deserialzed JSON object).
20
+ */
21
+ constructor (config, messageHandler) {
22
+ this.messageHandler = messageHandler
23
+ config.then(config => {
24
+ this.url = config['dmx.websockets.url']
25
+ // DEV && console.log('[DMX] CONFIG: WebSocket server is reachable at', this.url)
26
+ this._connect()
27
+ })
28
+ }
29
+
30
+ /**
31
+ * Sends a message to the server.
32
+ *
33
+ * @param message the message to be sent (arbitrary type). Will be serialized as JSON.
34
+ */
35
+ send (message) {
36
+ this.ws.send(JSON.stringify(message))
37
+ }
38
+
39
+ _connect () {
40
+ DEV && console.log('[DMX] Opening WebSocket connection to', this.url)
41
+ this.ws = new WebSocket(this.url)
42
+ this.ws.onopen = e => {
43
+ this._startIdling()
44
+ }
45
+ this.ws.onmessage = e => {
46
+ const message = JSON.parse(e.data)
47
+ DEV && console.log('[DMX] Receiving message', message)
48
+ this.messageHandler(message)
49
+ }
50
+ this.ws.onclose = e => {
51
+ DEV && console.log('[DMX] Closing WebSocket connection (' + e.reason + '), try reconnect in ' +
52
+ RECONNECT_DELAY / 1000 + ' seconds')
53
+ this._stopIdling()
54
+ this._reconnect()
55
+ }
56
+ this.ws.onerror = e => {
57
+ DEV && console.warn('[DMX] WebSocket error')
58
+ }
59
+ }
60
+
61
+ _reconnect () {
62
+ setTimeout(this._connect.bind(this), RECONNECT_DELAY)
63
+ }
64
+
65
+ _startIdling () {
66
+ this.idleId = setInterval(this._idle.bind(this), IDLE_INTERVAL)
67
+ }
68
+
69
+ _stopIdling () {
70
+ clearInterval(this.idleId)
71
+ }
72
+
73
+ _idle () {
74
+ DEV && console.log('[DMX] WebSocket connection idle')
75
+ this.send({type: 'idle'})
76
+ }
77
+ }