dmx-api 2.1.0 → 3.0.1
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 +55 -2
- package/package.json +2 -2
- package/src/icons.js +12 -6
- package/src/index.js +28 -17
- package/src/model.js +74 -33
- package/src/rpc.js +69 -10
- package/src/type-cache.js +92 -34
- package/src/utils.js +6 -1
- package/src/websocket.js +79 -0
package/README.md
CHANGED
|
@@ -2,6 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
## Version History
|
|
4
4
|
|
|
5
|
+
**3.0.1** -- Jun 6, 2023
|
|
6
|
+
|
|
7
|
+
* Improvement:
|
|
8
|
+
- In case of a dropped WebSocket connection an alert is shown and the page is reloaded
|
|
9
|
+
|
|
10
|
+
**3.0** -- May 19, 2023
|
|
11
|
+
|
|
12
|
+
Version 3.0 of the `dmx-api` library extends/modifies the API in order to support a wider variety of frontend
|
|
13
|
+
applications. Additionally, depending on application type, the launch time is reduced as less data is transferred from
|
|
14
|
+
server ([#497](https://git.dmx.systems/dmx-platform/dmx-platform/-/issues/497),
|
|
15
|
+
[#501](https://git.dmx.systems/dmx-platform/dmx-platform/-/issues/501)). This required some breaking changes in the
|
|
16
|
+
library's `init()` function.
|
|
17
|
+
|
|
18
|
+
* BREAKING CHANGES
|
|
19
|
+
- changed behavior of the library's `init()` function:
|
|
20
|
+
- The client-side type cache is not fully pre-populated by default anymore. Instead the application pass
|
|
21
|
+
`topicTypes` config to pre-populate selectively, or pass `all`. Depending on application type this results in
|
|
22
|
+
less data transfer at application launch.
|
|
23
|
+
- The SVG utility for FontAwesome icons is not initialized by default anymore. Instead an application can
|
|
24
|
+
initialize it on-demand (by calling `dmx.icons.init()`). Applications who don't need it launch quicker as
|
|
25
|
+
downloading the FontAwesome SVG data (450K) is omitted.
|
|
26
|
+
- rename class `Type` -> `DMXType`
|
|
27
|
+
- remove `getPosition()` from `ViewTopic`. Use the new `pos` getter instead.
|
|
28
|
+
* Improvement:
|
|
29
|
+
- The library's `init()` function optionally opens the WebSocket for client-synchronization (if `messageHandler`
|
|
30
|
+
config is passed). The application no longer depends on
|
|
31
|
+
[dmx-websocket](https://github.com/dmx-systems/dmx-websocket) module then.
|
|
32
|
+
- Vanilla [axios](https://github.com/axios/axios) http instance (without error interceptor set by application) is
|
|
33
|
+
exported as `rpc._http`.
|
|
34
|
+
* Model:
|
|
35
|
+
- change `Type`'s `newFormModel()`:
|
|
36
|
+
- `object` parameter is now optional
|
|
37
|
+
- add `allChildren` parameter (optional)
|
|
38
|
+
- add (optional) `level` parameter to `CompDef`'s `emptyChildInstance()`
|
|
39
|
+
- add `panX`, `panY`, `zoom`, `bgImageUrl` getters to `Topicmap`
|
|
40
|
+
- add `updateTopic()`, `updateAssoc()` to `Topicmap` (part of
|
|
41
|
+
[dmx-topicmap-panel](https://github.com/dmx-systems/dmx-topicmap-panel) protocol)
|
|
42
|
+
- add `pos` getter to `ViewTopic`
|
|
43
|
+
* RPC:
|
|
44
|
+
- add `getTopicType()` and `getAssocType()`
|
|
45
|
+
- add `includeChildren` parameter to `getTopicmap()`
|
|
46
|
+
- add `includeChildren` and `includeAssocChildren` parameters to `getAssignedWorkspace()`
|
|
47
|
+
- add `getMemberships()` to get the members of a workspace
|
|
48
|
+
- add `bulkUpdateUserMemberships()` and `bulkUpdateWorkspaceMemberships()`
|
|
49
|
+
- add `deleteWorkspace()`
|
|
50
|
+
- change `updateTopic()`: returns `Topic` plus directives (formerly just directives)
|
|
51
|
+
- change `login()`: returned promise resolves with username (formerly undefined)
|
|
52
|
+
* Utils:
|
|
53
|
+
- add `stripHtml()`
|
|
54
|
+
* Fix:
|
|
55
|
+
- fixed a bug where nested entities loose their child values while update request (thanks to @gevlish)
|
|
56
|
+
|
|
5
57
|
**2.1** -- Jun 13, 2021
|
|
6
58
|
|
|
7
59
|
* Model:
|
|
@@ -219,7 +271,8 @@
|
|
|
219
271
|
|
|
220
272
|
**0.13** -- Mar 25, 2018
|
|
221
273
|
|
|
222
|
-
* Model: `Topicmap.revealTopic()`'s `pos` param is optional. If not given it's up to the topicmap renderer to position
|
|
274
|
+
* Model: `Topicmap.revealTopic()`'s `pos` param is optional. If not given it's up to the topicmap renderer to position
|
|
275
|
+
the topic.
|
|
223
276
|
* Utils: `clone()` for deep-cloning arbitrary objects
|
|
224
277
|
* Depends on module `clone` instead `lodash.clonedeep`
|
|
225
278
|
|
|
@@ -269,4 +322,4 @@
|
|
|
269
322
|
|
|
270
323
|
------------
|
|
271
324
|
Jörg Richter
|
|
272
|
-
Jun
|
|
325
|
+
Jun 6, 2023
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dmx-api",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "DMX 5
|
|
3
|
+
"version": "3.0.1",
|
|
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",
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
34
|
+
init,
|
|
29
35
|
faGlyph
|
|
30
36
|
}
|
package/src/index.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
console.log('[DMX-API] 2021/06/13')
|
|
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.1')
|
|
12
10
|
|
|
13
11
|
let adminWorkspaceId // promise
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
const clientId = newClientId()
|
|
14
|
+
updateClientIdCookie()
|
|
16
15
|
|
|
17
|
-
|
|
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
|
-
|
|
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,11 @@ export default {
|
|
|
37
40
|
return adminWorkspaceId.then(id => permCache.isWritable(id))
|
|
38
41
|
}
|
|
39
42
|
}
|
|
43
|
+
|
|
44
|
+
function updateClientIdCookie () {
|
|
45
|
+
utils.setCookie('dmx_client_id', clientId)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function newClientId () {
|
|
49
|
+
return Math.floor(Number.MAX_SAFE_INTEGER * Math.random())
|
|
50
|
+
}
|
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
|
}
|
|
@@ -186,7 +186,6 @@ class Topic extends DMXObject {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
update () {
|
|
189
|
-
// console.log('update', this)
|
|
190
189
|
return rpc.updateTopic(this)
|
|
191
190
|
}
|
|
192
191
|
|
|
@@ -315,7 +314,6 @@ class Assoc extends DMXObject {
|
|
|
315
314
|
}
|
|
316
315
|
|
|
317
316
|
update () {
|
|
318
|
-
console.log('update', this)
|
|
319
317
|
return rpc.updateAssoc(this)
|
|
320
318
|
}
|
|
321
319
|
|
|
@@ -415,8 +413,7 @@ class RelatedTopic extends Topic {
|
|
|
415
413
|
}
|
|
416
414
|
}
|
|
417
415
|
|
|
418
|
-
|
|
419
|
-
class Type extends Topic {
|
|
416
|
+
class DMXType extends Topic {
|
|
420
417
|
|
|
421
418
|
constructor (type) {
|
|
422
419
|
super(type)
|
|
@@ -481,15 +478,25 @@ class Type extends Topic {
|
|
|
481
478
|
/**
|
|
482
479
|
* Creates a form model for this type.
|
|
483
480
|
*
|
|
484
|
-
* @param object
|
|
485
|
-
*
|
|
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.
|
|
486
487
|
*
|
|
487
|
-
* @return the created form model.
|
|
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.
|
|
488
490
|
*/
|
|
489
|
-
newFormModel (object) {
|
|
490
|
-
const o = this._newFormModel(object)
|
|
491
|
-
|
|
492
|
-
|
|
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
|
+
}
|
|
493
500
|
}
|
|
494
501
|
|
|
495
502
|
/**
|
|
@@ -498,14 +505,14 @@ class Type extends Topic {
|
|
|
498
505
|
*
|
|
499
506
|
* @return a newly constructed plain object
|
|
500
507
|
*/
|
|
501
|
-
_newFormModel (object) {
|
|
508
|
+
_newFormModel (object, allChildren, level = 0, compDef) {
|
|
502
509
|
|
|
503
510
|
function _newFormModel (object, type, level, compDef) {
|
|
504
511
|
const o = type._newInstance(object)
|
|
505
512
|
if (type.isComposite) {
|
|
506
513
|
type.compDefs.forEach(compDef => {
|
|
507
514
|
// Reduced details: at deeper levels for entity types only their identity attributes are included
|
|
508
|
-
if (level === 0 || type.isValue || compDef.isIdentityAttr) {
|
|
515
|
+
if (allChildren || level === 0 || type.isValue || compDef.isIdentityAttr) {
|
|
509
516
|
const compDefUri = compDef.compDefUri
|
|
510
517
|
const childType = compDef.childType
|
|
511
518
|
const child = object && object.children[compDefUri]
|
|
@@ -528,23 +535,21 @@ class Type extends Topic {
|
|
|
528
535
|
return o
|
|
529
536
|
}
|
|
530
537
|
|
|
531
|
-
return _newFormModel(object, this,
|
|
538
|
+
return _newFormModel(object, this, level, compDef)
|
|
532
539
|
}
|
|
533
540
|
|
|
534
541
|
_newInstance (object) {
|
|
535
|
-
|
|
536
|
-
const o = {
|
|
542
|
+
return {
|
|
537
543
|
id: object ? object.id : -1,
|
|
538
544
|
uri: object ? object.uri : '',
|
|
539
545
|
typeUri: object ? object.typeUri : this.uri,
|
|
540
546
|
value: object ? object.value : '',
|
|
541
547
|
children: {}
|
|
542
548
|
}
|
|
543
|
-
return o
|
|
544
549
|
}
|
|
545
550
|
}
|
|
546
551
|
|
|
547
|
-
class TopicType extends
|
|
552
|
+
class TopicType extends DMXType {
|
|
548
553
|
|
|
549
554
|
/**
|
|
550
555
|
* Creates a form model for this topic type, and fills in the given value.
|
|
@@ -589,7 +594,7 @@ class TopicType extends Type {
|
|
|
589
594
|
}
|
|
590
595
|
}
|
|
591
596
|
|
|
592
|
-
class AssocType extends
|
|
597
|
+
class AssocType extends DMXType {
|
|
593
598
|
|
|
594
599
|
get isTopicType () {
|
|
595
600
|
return false
|
|
@@ -674,7 +679,7 @@ class CompDef extends Assoc {
|
|
|
674
679
|
return topic && topic.value
|
|
675
680
|
}
|
|
676
681
|
|
|
677
|
-
// ### TODO: principal copy in
|
|
682
|
+
// ### TODO: principal copy in DMXType
|
|
678
683
|
_getViewConfig (childTypeUri) {
|
|
679
684
|
// TODO: don't hardcode config type URI
|
|
680
685
|
const configTopic = this.viewConfig['dmx.webclient.view_config']
|
|
@@ -694,8 +699,8 @@ class CompDef extends Assoc {
|
|
|
694
699
|
return 'dmx.core.composition'
|
|
695
700
|
}
|
|
696
701
|
|
|
697
|
-
emptyChildInstance () {
|
|
698
|
-
const topic = this.childType._newFormModel()
|
|
702
|
+
emptyChildInstance (level) {
|
|
703
|
+
const topic = this.childType._newFormModel(undefined, false, level, this)
|
|
699
704
|
topic.assoc = this.instanceLevelAssocType._newFormModel()
|
|
700
705
|
return new Topic(topic)
|
|
701
706
|
}
|
|
@@ -796,17 +801,16 @@ class Topicmap extends Topic {
|
|
|
796
801
|
}
|
|
797
802
|
|
|
798
803
|
/**
|
|
799
|
-
* Returns the position of the given topic/assoc.
|
|
800
|
-
*
|
|
801
|
-
* Note: ViewTopic has getPosition() too but ViewAssoc has not
|
|
802
|
-
* as a ViewAssoc doesn't know the Topicmap it belongs to.
|
|
804
|
+
* Returns the position of the given topic/assoc of this topicmap.
|
|
803
805
|
*
|
|
804
806
|
* @param id a topic ID or an assoc ID
|
|
805
807
|
*/
|
|
806
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.
|
|
807
811
|
const o = this.getObject(id)
|
|
808
812
|
if (o.isTopic) {
|
|
809
|
-
return o.
|
|
813
|
+
return o.pos
|
|
810
814
|
} else {
|
|
811
815
|
const pos1 = this.getPosition(o.player1.id)
|
|
812
816
|
const pos2 = this.getPosition(o.player2.id)
|
|
@@ -818,22 +822,28 @@ class Topicmap extends Topic {
|
|
|
818
822
|
}
|
|
819
823
|
|
|
820
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
|
+
*
|
|
821
828
|
* @param topic a dmx.ViewTopic
|
|
822
829
|
*/
|
|
823
830
|
addTopic (topic) {
|
|
824
831
|
if (!(topic instanceof ViewTopic)) {
|
|
825
|
-
throw Error(`addTopic() expects
|
|
832
|
+
throw Error(`addTopic() expects ViewTopic, got ${topic.constructor.name}`)
|
|
826
833
|
}
|
|
827
834
|
// reactivity is required to trigger "visibleTopicIds" getter (module dmx-cytoscape-renderer)
|
|
828
835
|
Vue.set(this._topics, topic.id, topic)
|
|
829
836
|
}
|
|
830
837
|
|
|
831
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
|
+
*
|
|
832
842
|
* @param assoc a dmx.ViewAssoc
|
|
833
843
|
*/
|
|
834
844
|
addAssoc (assoc) {
|
|
835
845
|
if (!(assoc instanceof ViewAssoc)) {
|
|
836
|
-
throw Error(`addAssoc() expects
|
|
846
|
+
throw Error(`addAssoc() expects ViewAssoc, got ${assoc.constructor.name}`)
|
|
837
847
|
}
|
|
838
848
|
// reactivity is required to trigger "visibleAssocIds" getter (module dmx-cytoscape-renderer)
|
|
839
849
|
Vue.set(this._assocs, assoc.id, assoc)
|
|
@@ -905,6 +915,21 @@ class Topicmap extends Topic {
|
|
|
905
915
|
return op
|
|
906
916
|
}
|
|
907
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
|
+
|
|
908
933
|
/**
|
|
909
934
|
* Note: if the topic is not in this topicmap nothing is performed.
|
|
910
935
|
*/
|
|
@@ -965,6 +990,23 @@ class Topicmap extends Topic {
|
|
|
965
990
|
|
|
966
991
|
// Topicmap
|
|
967
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
|
+
|
|
968
1010
|
setViewport (pan, zoom) {
|
|
969
1011
|
this.viewProps['dmx.topicmaps.pan_x'] = pan.x
|
|
970
1012
|
this.viewProps['dmx.topicmaps.pan_y'] = pan.y
|
|
@@ -1020,8 +1062,7 @@ class ViewTopic extends viewPropsMixin(Topic) {
|
|
|
1020
1062
|
return rpc.getTopic(this.id, true, true)
|
|
1021
1063
|
}
|
|
1022
1064
|
|
|
1023
|
-
|
|
1024
|
-
getPosition () {
|
|
1065
|
+
get pos () {
|
|
1025
1066
|
return {
|
|
1026
1067
|
x: this.getViewProp('dmx.topicmaps.x'),
|
|
1027
1068
|
y: this.getViewProp('dmx.topicmaps.y')
|
|
@@ -1059,7 +1100,7 @@ export {
|
|
|
1059
1100
|
Assoc,
|
|
1060
1101
|
Player,
|
|
1061
1102
|
RelatedTopic,
|
|
1062
|
-
|
|
1103
|
+
DMXType,
|
|
1063
1104
|
TopicType,
|
|
1064
1105
|
AssocType,
|
|
1065
1106
|
RoleType,
|
package/src/rpc.js
CHANGED
|
@@ -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,12 @@ 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
|
+
|
|
207
215
|
getTopicTypeImplicitly (topicId) {
|
|
208
216
|
return http.get(`/core/topic-type/topic/${topicId}`).then(response =>
|
|
209
217
|
new TopicType(response.data)
|
|
@@ -230,6 +238,12 @@ export default {
|
|
|
230
238
|
|
|
231
239
|
// Association Types
|
|
232
240
|
|
|
241
|
+
getAssocType (assocTypeUri) {
|
|
242
|
+
return http.get(`/core/assoc-type/${assocTypeUri}`).then(response =>
|
|
243
|
+
new AssocType(response.data)
|
|
244
|
+
)
|
|
245
|
+
},
|
|
246
|
+
|
|
233
247
|
getAssocTypeImplicitly (assocId) {
|
|
234
248
|
return http.get(`/core/assoc-type/assoc/${assocId}`).then(response =>
|
|
235
249
|
new AssocType(response.data)
|
|
@@ -300,8 +314,12 @@ export default {
|
|
|
300
314
|
)
|
|
301
315
|
},
|
|
302
316
|
|
|
303
|
-
getTopicmap (topicmapId) {
|
|
304
|
-
return http.get(`/topicmaps/${topicmapId}
|
|
317
|
+
getTopicmap (topicmapId, includeChildren) {
|
|
318
|
+
return http.get(`/topicmaps/${topicmapId}`, {
|
|
319
|
+
params: {
|
|
320
|
+
children: includeChildren,
|
|
321
|
+
}
|
|
322
|
+
}).then(response =>
|
|
305
323
|
new Topicmap(response.data)
|
|
306
324
|
)
|
|
307
325
|
},
|
|
@@ -368,7 +386,6 @@ export default {
|
|
|
368
386
|
},
|
|
369
387
|
|
|
370
388
|
setTopicmapViewport: utils.debounce((topicmapId, pan, zoom) => {
|
|
371
|
-
// console.log('setTopicmapViewport')
|
|
372
389
|
roundPos(pan, 'x', 'y')
|
|
373
390
|
http.put(`/topicmaps/${topicmapId}/pan/${pan.x}/${pan.y}/zoom/${zoom}`)
|
|
374
391
|
}, 3000),
|
|
@@ -386,6 +403,10 @@ export default {
|
|
|
386
403
|
)
|
|
387
404
|
},
|
|
388
405
|
|
|
406
|
+
deleteWorkspace (workspaceId) {
|
|
407
|
+
http.delete(`/workspaces/${workspaceId}`)
|
|
408
|
+
},
|
|
409
|
+
|
|
389
410
|
getAssignedTopics (workspaceId, topicTypeUri, includeChildren, includeAssocChildren) {
|
|
390
411
|
return http.get(`/workspaces/${workspaceId}/topics/${topicTypeUri}`, {
|
|
391
412
|
params: {
|
|
@@ -400,8 +421,13 @@ export default {
|
|
|
400
421
|
/**
|
|
401
422
|
* @return the workspace topic, or empty string if no workspace is assigned
|
|
402
423
|
*/
|
|
403
|
-
getAssignedWorkspace (objectId) {
|
|
404
|
-
return http.get(`/workspaces/object/${objectId}
|
|
424
|
+
getAssignedWorkspace (objectId, includeChildren, includeAssocChildren) {
|
|
425
|
+
return http.get(`/workspaces/object/${objectId}`, {
|
|
426
|
+
params: {
|
|
427
|
+
children: includeChildren,
|
|
428
|
+
assocChildren: includeAssocChildren
|
|
429
|
+
}
|
|
430
|
+
}).then(response =>
|
|
405
431
|
// Note: if no workspace is assigned the response is 204 No Content; "data" is the empty string then
|
|
406
432
|
response.data && new Topic(response.data)
|
|
407
433
|
)
|
|
@@ -422,7 +448,10 @@ export default {
|
|
|
422
448
|
headers: {
|
|
423
449
|
Authorization: authMethod + ' ' + btoa(credentials.username + ':' + credentials.password)
|
|
424
450
|
}
|
|
425
|
-
}).then(() =>
|
|
451
|
+
}).then(() => {
|
|
452
|
+
permCache.clear()
|
|
453
|
+
return credentials.username
|
|
454
|
+
})
|
|
426
455
|
},
|
|
427
456
|
|
|
428
457
|
logout () {
|
|
@@ -448,6 +477,34 @@ export default {
|
|
|
448
477
|
)
|
|
449
478
|
},
|
|
450
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
|
+
|
|
451
508
|
getAdminWorkspaceId () {
|
|
452
509
|
return http.get('/access-control/workspace/admin/id').then(response =>
|
|
453
510
|
response.data
|
|
@@ -548,7 +605,9 @@ export default {
|
|
|
548
605
|
return Promise.reject(error)
|
|
549
606
|
}
|
|
550
607
|
)
|
|
551
|
-
}
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
_http
|
|
552
611
|
}
|
|
553
612
|
|
|
554
613
|
function toPath (idLists) {
|
package/src/type-cache.js
CHANGED
|
@@ -3,14 +3,15 @@ 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:
|
|
11
|
-
assocTypes:
|
|
12
|
-
roleTypes:
|
|
13
|
-
dataTypes:
|
|
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,8 +28,11 @@ 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
|
+
*/
|
|
30
34
|
initTypeCache () {
|
|
31
|
-
return
|
|
35
|
+
return initAllTypes()
|
|
32
36
|
},
|
|
33
37
|
|
|
34
38
|
// WebSocket messages
|
|
@@ -104,30 +108,17 @@ const actions = {
|
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
function init (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}),
|
|
119
|
-
rpc.getAllAssocTypes().then(assocTypes => {
|
|
120
|
-
state.assocTypes = utils.mapByUri(assocTypes)
|
|
121
|
-
}),
|
|
122
|
-
rpc.getAllRoleTypes().then(roleTypes => {
|
|
123
|
-
state.roleTypes = utils.mapByUri(roleTypes)
|
|
124
|
-
}),
|
|
125
|
-
rpc.getTopicsByType('dmx.core.data_type').then(dataTypes => {
|
|
126
|
-
state.dataTypes = utils.mapByUri(dataTypes)
|
|
127
|
-
})
|
|
128
|
-
]).then(() => {
|
|
129
|
-
// console.log('### Type cache ready!')
|
|
130
|
-
})
|
|
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
|
+
}
|
|
131
122
|
}
|
|
132
123
|
|
|
133
124
|
// ---
|
|
@@ -149,9 +140,9 @@ function getRoleType (uri) {
|
|
|
149
140
|
}
|
|
150
141
|
|
|
151
142
|
function getType (uri, className, prop) {
|
|
152
|
-
const type =
|
|
143
|
+
const type = _getType(uri, prop)
|
|
153
144
|
if (!type) {
|
|
154
|
-
throw Error(
|
|
145
|
+
throw Error(`${className} "${uri}" not in type cache`)
|
|
155
146
|
}
|
|
156
147
|
return type
|
|
157
148
|
}
|
|
@@ -169,7 +160,7 @@ function getTypeById (id) {
|
|
|
169
160
|
|
|
170
161
|
// TODO: the following 4 functions return async data so they should return promise
|
|
171
162
|
|
|
172
|
-
// IMPORTANT:
|
|
163
|
+
// IMPORTANT: calling these methods must be synced, otherwise you might get undefined
|
|
173
164
|
|
|
174
165
|
function getAllTopicTypes () {
|
|
175
166
|
return getAllTypes('topicTypes')
|
|
@@ -196,6 +187,67 @@ function getAllTypes (prop) {
|
|
|
196
187
|
|
|
197
188
|
// ---
|
|
198
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
|
+
|
|
199
251
|
function putTopicType (topicType) {
|
|
200
252
|
_putTopicType(new TopicType(topicType))
|
|
201
253
|
}
|
|
@@ -260,8 +312,9 @@ function bootstrapType () {
|
|
|
260
312
|
})
|
|
261
313
|
}
|
|
262
314
|
|
|
315
|
+
// public API
|
|
316
|
+
|
|
263
317
|
export default {
|
|
264
|
-
init,
|
|
265
318
|
getTopicType,
|
|
266
319
|
getAssocType,
|
|
267
320
|
getDataType,
|
|
@@ -272,3 +325,8 @@ export default {
|
|
|
272
325
|
getAllDataTypes,
|
|
273
326
|
getAllRoleTypes
|
|
274
327
|
}
|
|
328
|
+
|
|
329
|
+
// module internal API
|
|
330
|
+
|
|
331
|
+
export {init}
|
|
332
|
+
export const storeModule = {state, actions}
|
package/src/utils.js
CHANGED
|
@@ -184,6 +184,10 @@ function round (val, decimals) {
|
|
|
184
184
|
return Math.round(factor * val) / factor
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
function stripHtml (html) {
|
|
188
|
+
return html.replace(/<.*?>/g, '') // *? is the reluctant version of the * quantifier (which is greedy)
|
|
189
|
+
}
|
|
190
|
+
|
|
187
191
|
// ---
|
|
188
192
|
|
|
189
193
|
export default {
|
|
@@ -202,5 +206,6 @@ export default {
|
|
|
202
206
|
deleteCookie,
|
|
203
207
|
fulltextQuery,
|
|
204
208
|
formatFileSize,
|
|
205
|
-
round
|
|
209
|
+
round,
|
|
210
|
+
stripHtml
|
|
206
211
|
}
|
package/src/websocket.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const IDLE_INTERVAL = 60 * 1000 // 60s
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A WebSocket connection to the DMX server.
|
|
5
|
+
*
|
|
6
|
+
* The URL to connect to is determined automatically, based on the server-side `dmx.websockets.url` config property.
|
|
7
|
+
* WebSocket messages are expected to be JSON. Serialization/Deserialization performs automatically.
|
|
8
|
+
*
|
|
9
|
+
* Properties:
|
|
10
|
+
* `url` - url of the WebSocket server
|
|
11
|
+
* `ws` - the native WebSocket object
|
|
12
|
+
*/
|
|
13
|
+
export default class DMXWebSocket {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param messageHandler
|
|
17
|
+
* the function that processes incoming messages.
|
|
18
|
+
* One argument is passed: the message pushed by the server (a deserialzed JSON object).
|
|
19
|
+
*/
|
|
20
|
+
constructor (config, messageHandler) {
|
|
21
|
+
this.messageHandler = messageHandler
|
|
22
|
+
config.then(config => {
|
|
23
|
+
this.url = config['dmx.websockets.url']
|
|
24
|
+
// DEV && console.log('[DMX] CONFIG: WebSocket server is reachable at', this.url)
|
|
25
|
+
this._connect()
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sends a message to the server.
|
|
31
|
+
*
|
|
32
|
+
* @param message the message to be sent (arbitrary type). Will be serialized as JSON.
|
|
33
|
+
*/
|
|
34
|
+
send (message) {
|
|
35
|
+
this.ws.send(JSON.stringify(message))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_connect () {
|
|
39
|
+
DEV && console.log('[DMX] Opening WebSocket connection to', this.url)
|
|
40
|
+
this.ws = new WebSocket(this.url)
|
|
41
|
+
this.ws.onopen = e => {
|
|
42
|
+
this._startIdling()
|
|
43
|
+
}
|
|
44
|
+
this.ws.onmessage = e => {
|
|
45
|
+
const message = JSON.parse(e.data)
|
|
46
|
+
DEV && console.log('[DMX] Receiving message', message)
|
|
47
|
+
this.messageHandler(message)
|
|
48
|
+
}
|
|
49
|
+
this.ws.onclose = e => {
|
|
50
|
+
DEV && console.log('[DMX] WebSocket connection closed (' + e.reason + ')')
|
|
51
|
+
this._stopIdling()
|
|
52
|
+
this._reload() // a closed ws connection is regarded an (backend/network) error which requires page reloading
|
|
53
|
+
}
|
|
54
|
+
this.ws.onerror = e => {
|
|
55
|
+
DEV && console.warn('[DMX] WebSocket error')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_startIdling () {
|
|
60
|
+
this.idleId = setInterval(this._idle.bind(this), IDLE_INTERVAL)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_stopIdling () {
|
|
64
|
+
clearInterval(this.idleId)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_idle () {
|
|
68
|
+
DEV && console.log('[DMX] WebSocket connection idle')
|
|
69
|
+
this.send({type: 'idle'})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_reload () {
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
alert('There is a problem with the server or network.\n\nPlease press OK to reload page.\n' +
|
|
75
|
+
'If it fails try manual page reload.')
|
|
76
|
+
location.reload()
|
|
77
|
+
}, 1000) // timeout to not interfere with interactive page reload (which also closes websocket connection)
|
|
78
|
+
}
|
|
79
|
+
}
|