Perform an action without pushing it to the Command Stack (without adding it to undo chain)


#1

Hello everyone,

Suppose you want to use the bpmn-js modeler in an app, so that the end user can edit a model, and have access to Undo/Redo.

Now, at the same time you want to program a few other actions that your app takes care of, but that the user shouldn’t be able to undo/redo.

EDIT: In other words, I want my user to have their own Undo/Redo chain for their manual actions on the modeler, which should be entirely separated from the programmed actions in my app.

In my case I want to color certain parts of their model when something happens. For this, I use methods such as modeling.setColor() in my code. Doing this naturally pushes the coloring command to the command stack. Therefore when the user does some action, then presses Ctrl+Z to undo that action, they can end up undoing my automatic coloring actions, which is not what they want (i.e they want to undo their own manual actions)

So far, I have tried :

  • Undoing specific actions with something like commandStack.undo(actionId). Unfortunately commandStack is a stack, so that can’t work, can it ?
  • Popping commandStack._stack manually every time I perform a non-user action in the code. This seems to break undo() on some occasions, I don’t know exactly when. EDIT: It does not break undo, it breaks the undo/redo chain. That’s because the action is pushed to the stack and then deleted, instead of not being added to it at all. Explaining why would take a whole post but basically this approach is not satisfying.
  • Preventing an action from being pushed to the command stack if it is not performed by the user. I looked at Command Interceptors for this, but I don’t understand this feature or how to use it… so I didn’t actually try it.

I’m sure this is a pattern that could be common to a lot of developer cases using modeler :+1: It could potentially be useful to a lot of people.

Thank you in advance.


#2

What you could possibly do is to subclass Modeling and provide a second instance of CommandStack to it by using additional modules, e.g. CustomCommandStack and CustomModeling. The CustomModeling should depend on the CustomCommandStack instead of the original one. Then, you should use the CustomModeling for your programmatic actions so that user could not undo such actions.

To achieve this, you can simply define the CustomCommandStack module as:

var customCommandStackModule = {
   __init__: [ 'customCommandStack' ],
   customCommandStack: [ 'type', CommandStack ]
 };

…and inject it to your CustomModeling:

CustomModeling.$inject = [ 'eventBus', 'elementFactory', 'customCommandStack' ];

Finally, provide the custom modules to Modeler instance:

var bpmnModeler = new Modeler({ additionalModules: [ customCommandStackModule, customModelingModule ] });

#3

Hello @barmac, thank you for your reply :slight_smile: I have tried this but sadly I can’t find a way to make it work. (To be honest it is pretty difficult…)

If I understand it, this solution implies to have two distinct Modeling modules provided to the Modeler : the custom one that has the custom command stack and the default one. Is that correct ?
I can have two distinct command stacks with no problem :+1: but as you expected that is useless without a second, custom modeling module.

As it turns out, you cannot have two modeling modules with different names in a modeler instance. This crashes when calling its constructor and returns an Error: overriding handler for command <shape.append> (see this post).
On the other hand, calling your custom modeling module “modeling” overrides the default one and it looks like you cannot use both.

Here is my code for the latter case :

In CustomModeling.js :

import Modeling from 'diagram-js/lib/features/modeling/Modeling.js'
import CommandStack from 'diagram-js/lib/command/CommandStack.js'

export var CustomCommandStackModule = {
    __init__: [ 'customCommandStack' ],
    customCommandStack: [ 'type', CommandStack ]
}

var CustomModeling = {
    __init__: [ 'modeling' ],
    customModeling: [ 'type', Modeling ]
}

CustomModeling.$inject = [ 'eventBus', 'elementFactory', 'customCommandStack' ]

export default CustomModeling

In my code using modeler :

import BpmnModeler from 'bpmn-js/lib/Modeler'
import CustomModelingModule, { CustomCommandStackModule } from '../../lib/custom-modeling/CustomModeling.js'

var modeler = new BpmnModeler({
    additionalModules: [
        CustomCommandStackModule,
        CustomModelingModule,
    ]
})

var modeling = modeler.get('modeling')
var commandStack = modeler.get('commandStack')
var customCommandStack = modeler.get('customCommandStack')

console.log(commandStack === customCommandStack) // returns false
// we have two different stacks :)

// Then, all actions performed using modeling,
// programmatically and by the user,
// are added to the default command stack.
// The custom command stack is never changed :(

Did I make a mistake ?

Thank you for your time, sorry for asking for more help again.


#4

Indeed, solution based on second instances of Modeling and CommandStack seem to be difficult to implement. In your code, the one thing which was for sure broken was this fragment:

var CustomModeling = {
    __init__: [ 'modeling' ],
    customModeling: [ 'type', Modeling ]
}

CustomModeling.$inject = [ 'eventBus', 'elementFactory', 'customCommandStack' ]

If you want to manipulate $inject property, you should change it for the class itself, i.e. Modeling. See how subclassing is done in BpmnModeling.

However, I found a simpler solution to your problem. If you don’t want to use undo/redo for your own actions, you can simply remove the action from the command stack like this:

var modeling = modeler.get('modeling'),
    commandStack = modeler.get('commandStack');

modeling.setColor(someElements, { fill: 'pink' });
commandStack._stack.pop();

Thus, nor the user nor the app will be able to use undo/redo behaviour on the setColor action from the sample code. I think this might be what you were looking for.


#5

Alright, it works :partying_face: Thanks @barmac, your help was on point :+1:

It took me a long time to figure it out and a lot of trial & error but I’ve managed to apply your first solution, the one with CustomModeling. The results were a bit surprising but work. Here’s my code for anyone who wants to do this, with a few comments to explain the results :

in a CustomModeling.js file :

import inherits from 'inherits'

import Modeling from 'bpmn-js/lib/features/modeling/Modeling.js'
import CommandStack from 'diagram-js/lib/command/CommandStack.js'

export var CustomCommandStackModule = {
    __init__: [ 'customCommandStack' ],
    customCommandStack: [ 'type', CommandStack ]
}

function CustomModeling(eventBus, elementFactory, commandStack, bpmnRules) {
    Modeling.call(this, eventBus, elementFactory, commandStack, bpmnRules)
}

inherits(CustomModeling, Modeling)

Modeling.$inject = [
    'eventBus',
    'elementFactory',
    'customCommandStack',
    'bpmnRules'
]

export default {
    __init__: [ 'customModeling' ],
    customModeling: [ 'type', CustomModeling ]
}

in my code using modeler :

import BpmnModeler from 'bpmn-js/lib/Modeler'
import CustomModelingModule, { CustomCommandStackModule } from '...some/path/CustomModeling.js'

var modeler = new BpmnModeler({
    additionalModules: [
        CustomModelingModule,
        CustomCommandStackModule
    ]
})

// And that's it. Now for using it properly :

var modeling = modeler.get('modeling') // for user actions
var customCommandStack = modeler.get('customCommandStack') // user's command stack (for Ctrl+Z, Y)

var customModeling = modeler.get('customModeling') // for programmed actions
var commandStack = modeler.get('commandStack') // programmed actions command stack

// To my surprise, any action performed on modeling will get pushed to customCommandStack,
// and vice versa.

// --------- example : 

customModeling.setColor(someshapes, {
    fill: 'orange',
    stroke: 'red'
})

// I have a listener bound to my modeler canvas for undo/redo :
onKeyDown = (evt) => {
    if (evt.ctrlKey) {
        if (evt.keyCode === 90) // Ctrl + Z
            customCommandStack.undo()
        if (evt.keyCode === 89) // Ctrl + Y
            customCommandStack.redo()
    }
}

This seems to work for me so far :v: One thing I should have done was rename the instances so that it made more sense, but I guess everyone will have their own vision about it.


Another thing just for the sake of the topic : I have to mention that the second solution you told me about kind of works, but not really. In my OP, I mentioned that I had already tried that :

  • Popping commandStack._stack manually every time I perform a non-user action in the code. [This] breaks the undo/redo chain. That’s because the action is pushed to the stack and then deleted, instead of not being added to it at all.

If you pop the stack while your stack index is at the top of the stack (i.e you currently have nothing to redo in your command stack) it works fine. However if you have previously undone a few actions before popping the stack, then at best you lose the actions you had stored for redo. At worst, I’m not sure but I think you may just break it.

If anyone still prefers that admittedly lighter solution, then there’s one last problem, you can’t simply do “commandStack._stack.pop()” and call it a day. The following code does it correctly for any action, I have written it during my experimentations so here you go, in case you’re in a rush :

    let lastCommand = commandStack._stack[commandStack._stack.length - 1]
    if (lastCommand && lastCommand.id) {
        let lastOperations = commandStack._stack.filter(com => com.id === lastCommand.id)
        let numberOps = lastOperations.length
        let firstIndexInStack = commandStack._stack.indexOf(firstOf(lastOperations))
        commandStack._stack.splice(firstIndexInStack, numberOps)
        commandStack._stackIdx -= numberOps
    }

tl;dr don’t do it unless you don’t care about Ctrl+Y.


#6

In my point of view this is not a common use case. Maybe this is because I still don’t understand why you’d like to do this. Could you give us a concrete example what you’re doing in the app and why the user should not be able to undo that?


#7

Hi @nikku, sure thing. Although I can’t get too specific because I’m working in a company and I should keep the real intent a secret, but hopefully you can see where I’m going. Sorry about that.

I’m making a prototype that automatically generates a BPMN diagram based on some input text file.

  • The goal is to show the generated model to the user in the modeler canvas, and also to allow them to edit some parts of it without breaking its structure, in order to keep it consistent with the input file.
  • Next to the bpmn-js frame, my UI also contains a simple list view of the high-level elements in the input file, which will look more familiar to the user. By hovering on the items in that list, the relevant parts of the model can be highlighted, to give them a better readability of the output model while they’re working on it. They should be able to do this at any time.
  • Obviously, I also want to allow them to undo/redo their actions.

Which means, if the user hovers on my list to highlight stuff in the diagram, the coloring actions will be added to the undo/redo chain in the command stack, so they will color and de-color the model by pressing Ctrl+Z/Y, instead of undoing their actual editing actions.

This is why simply popping the stack to prevent undoing the coloring actions wouldn’t work, because it makes all the actions that could have been redone disappear. It sounds like a rare case but it is actually very annoying to use.

Now I understand not a lot of people are making a modeler that technically “edits itself” on the fly, but for everyone who does, it seems to me that you can’t go about without meeting this problem.

(For instance : in my app, I can have very long labels. I thought “how about shortening them, and then making it so that hovering on a shape would show its full label ?”. I thought solving the problem in this topic would also help for that, but it turns out it’s more complicated this time :exploding_head:) EDIT: I found another way.

I hope this wasn’t too cryptic.


#8

So if I understand correctly you simply want to color elements temporarily. I’ve had the same issue when working on a bpmn-js plugin. What I did is to directly execute the low-level logic that would be executed by the command:


#9

Okay, this sounds exactly like what I was looking for in the first place. Thanks @philippfromme :ok_hand: I’ll try doing this instead of the multi-command-stack solution when I have the time.

Finding the right low-level actions will probably help with other things aside from just coloring.

(The reason I’m not marking this as solution for the topic is because I haven’t tried it)