Implementing custom commands with plugin.js: Utilizing webclient state and the dmx-api

I am currently migration my webpages application from DM4 to run with DMX. In DM4 webpages integrated with the dm4c API which, as of DMX, is quite different. I would be glad if you could give me some help on how to update my webclient extension efficiently.

Here is a brief overview of what I need to adapt:

I guess, the payload models might have changed slightly syntactically (not using frankenstein notation, and other URIs), but the same functionality is exposed by the dmx-webclient or dmx-topicmaps module in form of Vuex “actions”, right?

  1. Here is the webclient state I utilized in DM4
 dm4c.selected_object
 // -> to inspect "children"

I guess in DMX there is a similar object always representing the currently selected topic in a given topcimap - only managed by the Vuex store now. Is this still available from withi my plugin.js e.g. somewhere like store.topicmaps.selected_object?

  1. Furthermore I used the following calls in combination to add custom webpages functionality to the Webclient and orchestrate the topicmap renderer following my commands:
2.1: dm4c.do_reveal_topic(id, "show")
2.2: dm4c.do_reveal_related_topic(id, "show")
2.3: dm4c.create_topic("de.mikromedia.page")
2.4: dm4c.create_assoc("dmx.core.association",
  {topic_id: webpage.id, role_type_uri: "dmx.core.default"},
  {topic_id: section.id, role_type_uri: "dmx.core.default"}
)
2.5: dm4c.show_topic(section, "edit", undefined, true) // do_center=true
2.6: dm4c.show_association(assoc, "none")
2.7: dm4c.get_topic_related_topics(webpageId, {
   "assoc_type": "dmx.core.association",
   "others_topic_type_uri": "de.mikromedia.site"
}, false)

I would be glad for some support e.g. links to follow so I could read about how to adapt my webclient extensions without much trial and error.

Last but not least

  1. I asked myself if I have access to a Vue instance in (3.a) my plugin.js and (3.b) my `plugin_store.js" to invoke the ElementUI notify component, like “this.$notify” directly while not being within the scope of a vue template.

I would be very thankful for some guidance and support here…

Just a short update. It turns out I have (3.a) already working from within the plugin.js of the dmx-csv plugin.

  1. I asked myself if I have access to a Vue instance in (3.a) my plugin.js and (3.b) my `plugin_store.js" to invoke the ElementUI notify component, like “this.$notify” directly while not being within the scope of a vue template.
this.$notify.error({
   title: 'CSV File Upload Failed', message: 'Error: ' + JSON.stringify(error)
})
.... (or)
Vue.prototype.$notify({
  title: "Import operation needs to know the <i>Topic Type</>",
  dangerouslyUseHTMLString: true, duration: 10000,
  message: "You must relate a <i>Topic Type</i> to <i>"+topic.value+"</i> before importing data. "
    + "Create a <i>File Import</i> association to the <i>Topic Type</i> you want to import data to.",
    type: "error"
})
(both seem to work)

(3.b) would be useful to call from within the dmx-notifications, notification-store.js

Here another update on what I found so far.

Regarding Q1: I guess I have found what I was searching for in the comments of dmx-plugin-template’s plugin.js:

To see if the user is logged in I can inspect the dmx-accesscontrol plugin store in my plugin.js like e.g.

// following: store.state.pluginstorename.statename
store.state.accesscontrol.username

And the equivalent to dm4.selected_object seems to be in the webclient stores global state

store.state.object

Regarding 2.1: dm4c.do_reveal_topic(id, "show"): I found an answer in looking at the actions provided by the dmx-topicmaps store module.

 /**
   * Reveals a topic on the topicmap panel.
   *
   * @param   topic     the topic to reveal (dmx.Topic).
   * @param   pos       Optional: the topic position in model coordinates (object with "x", "y" props).
   *                    If not given it's up to the topicmap renderer to position the topic.
   * @param   noSelect  Optional: if trueish the programmatic topic selection is suppressed.
   */
  revealTopic ({dispatch}, {topic, pos, noSelect}) {

I was able to command the webclient to perform this action through calling the following in my customCommand handler:

store.dispatch("revealTopic", {topic: new dmx.Topic(response.data)})

Displaying a Notification

Yes, within a DMX plugin you can display an Element UI Notification by calling Vue.prototype.$notify().

This works because the DMX Webclient extends the Vue prototype as follows (from /dmx-webclient/src/main/js/element-ui.js):

  import Vue from 'vue'
  import { Notification, ... } from 'element-ui'

  Vue.prototype.$notify = Notification

This prototype extension is also the cause why this.$notify is available within every Vue instance.

Dependency Injection

Apparently you need access to the Vue constructor. But there is a PITFALL: in a DMX plugin you can NOT do import Vue from 'vue'. This way your plugin would bundle its own Vue library. Instead it must use the Vue library bundled with the DMX Webclient.

To enable your plugin to do so at runtime the Webclient injects the Vue constructor (besides other things) into relevant plugin assets:

  • The main file (plugin.js):
  • The plugin’s Vuex store module (if any)
  • The Maptype’s Vuex store module (in case your plugin defines a Maptype)

All 3 assets have the same form: you either export the respective asset (e.g. a store module) OR you export a function which returns that asset.

Use the latter for dependency injection:

  export default ({Vue, ...}) => ...

Here your default export is a function. This function is called by DMX Webclient while it loads your plugin. The Webclient passes the dependencies your plugin might need, like the Vue constructor.

Find more details about Webclient dependency injection in the source code comments of the dmx-plugin-template project. In particular the main file (plugin.js) and the plugin’s store module (greeting.js).

This should answer your questions 3a and 3b, and a bit more :smile:

Thank you for the excellent questions!

Thanks for your answer. It would be great to get some help on these two methods in particular as topicModel and assocModel are undocumented parameters in dmx-api rpc. I now tried to issue dmx.rpc.create_topic("de.mikromedia.page") but that fails with a 415 Unsupported Media Type.

I suppose the same is true for dmx.rpc.createAssoc - values in my assocModel needs to be adapted. Can you help me with this?

Two additional questions came up during the migration of the notifications module from DM4 to DMX:

4.1) Is there a way I can use dmx-api to check if an assoc exists if I know everything else (=player1Id, player2Id, assocType, roleType1, roleType2, player1Type, player2Type, etc.) but the ID?

Background:
If - for a given user and topic - the plugin should only show either an “Unsubscribe” or a “Subscribe” command and not both. Here is a screenshot of how the context-menu currently looks like:

Screenshot 2020-12-13 102945

The state is represented by the exitence of an subscription_edge assoc in between the topc and the user in the semantic storage. From what I found in dmx-api rpc is that dmx.rpc.getAssoc is only available if one knows the ID of an assoc but that does not help me here.

4.2) How to deal with asynchronocity in the construction of contextCommands which depend on such a state, like described in Subscribe / Unsubscribe? Can I force the dmx-api rpc into synchronicity for some requests? What alternatives might be worth exploring?

Thanks for your support.

Edited: For clarity. Added an image of the “Subscribe/Unsubscribe” command issue.

Hey there, another questions which just came up:

The listing of notifications allows users to reveal the topic which triggered the notification in the current topicmap. This could be an edited Note, a new Event entry etc.

  1. If the notification is concerned with e.g. a new or updated Topicmap, I don’t want to simply reveal the resp. Topicmap topic in the current topicmap. Instead I want to provide users with a command that selects the topicmap from the notification listing so that this topicmap becomes the new topicmap rendered in the topicmap panel.

How can I achieve that from wthin my plugin.js? I suspect I need to utilize some functionality from the topicmaps.js service, right? Note that this might also imply a change of the current workspace.

Thanks for your help in bringing this notifications plugin to DMX!

A context menu command is an object, basically with label and handler props. Optional are multi and disabled.

You can construct contextCommands in 3 ways:

Statically

The topic prop is for the topic commands (assoc analogue): an array of commands.

This example is from platform’s dmx-topicmaps module (plugin.js):

    contextCommands: {
      topic: [
        {
          label: 'Hide',
          multi: true,
          handler: idLists => store.dispatch('hideMulti', idLists)
        },
        {
          label: 'Edit',
          handler: id => store.dispatch('callTopicDetailRoute', {id, detail: 'edit'}),
          disabled: isEditDisabled
        }
      ]
    }

Dynamically – possibly based on the clicked object

For topic you define a function that returns an array of commands. The array can be empty. (You can also return nothing/undefined).
The Webclient calls this function in the moment the user invokes the context menu. It passes the target topic/association, of type dmx.Topic resp. dmx.Assoc (actually subtypes of these which contain view-data as well).

This example adds a “Run” topic-command to the context menu, but only on topics of a certain type:

  contextCommands: {
    topic: topic => {
      if (topic.typeUri === 'dmx.dita.processor') {
        return [{
          label: 'Run',
          handler: id => {
            const topicmapId = store.getters.topicmapId
            http.put(`/dita/process/${id}/topicmap/${topicmapId}`)
          }
        }]
      }
    }
  }

Dynamically + Asynchronously – involving an async operation

For topic you define a function that returns a Promise that resolves to an array of commands:

  contextCommands: {
    topic: topic => asyncOp(topic)
      .then(result => [result ? cmd1 : cmd2])
  }

Tell me if you need more information.
There is also support for multi-selection commands, and for disabling commands.

Thank you for posting the question :smile:

The DMX backend’s CoreService provides such a call, but indeed there is no client-side counterpart in dmx.rpc. Please consider opening a ticket.

In the meantime you could send the request yourself:

GET /core/assoc/{assocTypeUri}/{topic1Id}/{topic2Id}/{roleType1Uri}/{roleType2Uri}

You’ll get either an assoc JSON object (you could construct an dmx.Assoc from) or 204 No Content if there is no such assoc.

Hint: to find out the HTTP-API for a certain CoreService call one can inspect the platform’s dmx-webservice module (WebservicePlugin.java).

Thank you for your detailed answers regarding Q4, this definitely helps me in finishing the dmx-notifications release!

Meanwhile I tried to implement the “Dynamically + Asynchronously” approach but it looks like this is either not yet supported or I made mistake in my implementation.

To me it looks like the promise is (or can) not be resolved by the contextMenu as my label and ‘handler’ properties in the array of command-objects evaluate to “undefined” (despite my asyncOp def. returning an array with an object that has these props, see Browser console in screenshot below).

Here is the commit showing my plugin.js determineCommand (asyncOp) impl.

Here is what the webclient renders in the context menu of a supported topic and the returned commands object is logged into the browser console :

AsyncOp-forDetermining-TopicCommands-Screen

It would be great if you could have a look at the changes, if you see an error or can find the issue.

Thanks for your support!

Confirmed! There was a bug in the async code. This is now fixed. See #423.

However it was necessary to change the protocol of the “Dynamically + Asynchronously” case. Formerly:

For topic you define a function that returns a Promise that resolves to an array of commands:

Now: your function is supposed to return an array. Every item is either a command, or a promise which resolves to a command.

I did this protocol change because otherwise I would have to patch the Cytoscape Contextmenu extension another time, which may cause delays.

Tell me if this does work for you.

For your subscribe/unsubscribe case you could return an array of a single command promise, and decide the command’s label and handler (and possibly disabled flag) as soon as your response has arrived.

Thank you for the problem report! :+1:

In the meantime I found the answers to 2.3 and 2.4 by myself through inspecting the payload of the dmx-webclient module when creating topics and associations. So I am posting this here only to share the results:

2.3) For creating a topic of type “Webpage Section” from within a command handler in the plugin.js I use the webclients restclient module (dmx.rpc`) withn the following payload:

dmx.createTopic({typeUri: "de.mikromedia.section", children: 
  {"de.mikromedia.section.title": "New Section: " + Math.floor((Math.random() * 10000) + 1), 
    "de.mikromedia.section.layout": "ref_uri:de.mikromedia.layout.single_tile",
     "de.mikromedia.section.placement": "ref_uri:de.mikromedia.placement.above"
})

Note: I don’t know where I got this ref_uri stuff from but I found it in the platform sources somewhere. It instructs the value integrator to reuse these specific topics in the creation of the webpage section. The given children properties are “Identity attributes” of a webpage section and therefore contain some random and default values (as these are are required for topic creation).

2.4) For creating an assoc of type “Notification Subscription” from within a command handler in the plugin.js I use the webclients restclient module (dmx.rpc`) withn the following payload:

dmx.rpc.createAssoc({"typeUri":"dmx.notifications.subscription_edge", 
  "player1":{"roleTypeUri":"dmx.core.default","topicId":1234},
  "player2":{"roleTypeUri":"dmx.core.default","topicId":5678}
})

Regarding the questions 2.5 and 2.6 dispatching the following two actions did the trick (with assoc and otherPlayer being initialized objects)

store.dispatch("revealTopic", {topic: otherPlayer, pos: undefined, noSelect: true})
store.dispatch("revealAssoc", {assoc: assoc, noSelect: true})

Regarding 2.7 I used the following restclient call to fetch a related topic (which in this case loads the “Website” topic related to the topic with the id 1234)

dmx.rpc.getTopicRelatedTopics(1234, {
  "assocTypeUri": "dmx.core.association",
  "othersTopicTypeUri": "de.mikromedia.site"
}) 

This will then all be part of the now finally upcoming dmx-webpages 0.8 release.