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 +70 -2
- package/package.json +2 -3
- package/src/icons.js +12 -6
- package/src/index.js +29 -17
- package/src/model.js +149 -52
- package/src/rpc.js +122 -14
- package/src/type-cache.js +104 -38
- package/src/utils.js +35 -1
- package/src/websocket.js +77 -0
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
|
|
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
|
-
|
|
319
|
+
Jun 13, 2021
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dmx-api",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "DMX 5
|
|
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
|
-
|
|
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] 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
|
-
|
|
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,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
|
|
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))
|
|
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
|
|
444
|
-
*
|
|
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.
|
|
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
|
-
|
|
451
|
-
|
|
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,
|
|
538
|
+
return _newFormModel(object, this, level, compDef)
|
|
491
539
|
}
|
|
492
540
|
|
|
493
541
|
_newInstance (object) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
return http.
|
|
249
|
-
new
|
|
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}
|
|
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}
|
|
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(() =>
|
|
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:
|
|
11
|
-
assocTypes:
|
|
12
|
-
|
|
13
|
-
|
|
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 '
|
|
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 '
|
|
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 (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 =
|
|
143
|
+
const type = _getType(uri, prop)
|
|
145
144
|
if (!type) {
|
|
146
|
-
throw Error(
|
|
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:
|
|
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
|
|
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,
|
|
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
|
}
|
package/src/websocket.js
ADDED
|
@@ -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
|
+
}
|