alyssaxuu / flowy

The minimal javascript library to create flowcharts ✨
MIT License
11.47k stars 998 forks source link

Conditional Blocks #29

Open johndkn opened 4 years ago

johndkn commented 4 years ago

Hi Alyssa X,

Thanks for the amazing work ! Flowy looks awesome.

Would it be possible to create "Conditional Blocks" (one with one input and two outputs) ?

Screenshot example :

image

Thank you in advance

alyssaxuu commented 4 years ago

How would you see it best implemented w/ the current mechanics? Special blocks that instead of being able to have infinite children, have 2 children max, each for a different outcome? Doable for sure, just trying to think of the best UX for this sort of scenario (also because in this case the order of the "children blocks" is important, and you should be able to choose if you put it on the left or the right side, something that isn't possible w/ the current engine).

johndkn commented 4 years ago

Thanks for your reply :)

I gave it a lot of thought since yesterday, and maybe the best UX is to have instead "Filtering Blocks" in which we could implement conditional logic.

Example :

image
fqure commented 4 years ago

There are dozens of flowchart type scripts out there packed with numerous features and UI/UX artifacts. What's significant about Flowy is its very vanilla-like simplicity. If it's going to be a library encapsulated into other larger projects, it's important to keep simplicity in mind to keep options open for unforeseen behavior types. Here's a possible litmus test to consider when adopting new behavior: Could these blocks and relationships be translated 1:1 into a textual format (ie. Markdown) for documentation? The current "If is from then " or "When days have pass" statements inside these blocks should be simple enough to create conditional prompts for developers to add time delays, pending tasks, logic dependencies, and from another issue groups without adding artifacts that could increase user complexity.

I guess we're at a fork. Does Flowy build additional visual UI/UX artifacts to denote behavior or does it leave it as-is in its current textual state and leave it to the beholder to decide.

robhoward commented 4 years ago

We solved this with a block controller that owns how parent/child relationships get setup, e.g. what children can snap to what parent and some parents are 'decision' types.

And, my vote would be to keep flowy simple per @fqure

image

johndkn commented 4 years ago

This is awesome @robhoward! Can you give us more details on how you achieved this?

robhoward commented 4 years ago

Sure, @johndkn - hopefully I can explain it in a way that makes sense :)

We created a "blockBase" that wraps flowly. For example, it manages an internal array of blocks and the loading/saving. It also handles all the logic of what block and snap to what block/etc. We started with the example @alyssaxuu put together and took it further. Then we added functions for getting blocks by id/etc.

Each block is a separate object. For example, below is the "Yes" block.

The blockBase then renders all the loaded blocks into the UI by blockType. It places them in the UI by group, e.g. Trigger, Condition, Action. It then calls methods on the the block when it is dragged, rendered, needs to show properties, check parents, etc.

We're so happy with how this turned out. We always had workflow in our system, but we couldn't find a solid, simple way to visualize it. So much so, we just updated our website with a screen shot highlighting it :)

https://www.dailystory.com/ (just scroll down slightly)

And we also made some updates to flowy itself. I'd like to try to get those submitted as pull requests soon.

// ┌──────────────────────────────────────────────────────────────────── // │ Decision block Yes // └──────────────────────────────────────────────────────────────────── var blockYes = {

// ┌────────────────────────────────────────────────────────────────────
// │ Required - unique name of the block matches enum AutomationDecisionType.Yes
// └────────────────────────────────────────────────────────────────────
name: "Yes",

// ┌────────────────────────────────────────────────────────────────────
// │ Required - friendly name of the block, shown in messages
// └────────────────────────────────────────────────────────────────────
friendlyName: "Yes",

// ┌────────────────────────────────────────────────────────────────────
// │ Required - list of parents this block can snap to. Empty array means all
// └────────────────────────────────────────────────────────────────────
allowedParents: ['HasOpenedEmail', 'HasClickedEmail', 'HasRepliedToTextMessage', 'HasTextMessageReply','HasCustomRule'],

// ┌────────────────────────────────────────────────────────────────────
// │ Required - type of block
// └────────────────────────────────────────────────────────────────────
blockType: 'Condition',

// ┌────────────────────────────────────────────────────────────────────
// │ Required - does this block have properties
// └────────────────────────────────────────────────────────────────────
hasProperties: false,

// ┌────────────────────────────────────────────────────────────────────
// │ Required - validates the block, called on save
// └────────────────────────────────────────────────────────────────────
validate: function (props) {
    return { status: true };
},

// ┌────────────────────────────────────────────────────────────────────
// │ Required - renders HTML for block in toolbar
// └────────────────────────────────────────────────────────────────────
getToolbarBlock: function () {
    return `<div class="blockelem create-flowy noselect">
                    <input type="hidden" name="blockelemtype" class="blockelemtype" value="${this.name}">                            
                        <div class="blockin">
                            <div class="blockico"><span></span><i class="fad fa-thumbs-up"></i></div>
                            <div class="blocktext"><p class="blocktitle">${this.friendlyName}</p><p class="blockdesc">Used with a condition</p></div>
                        </div>
                </div>`;
},

// ┌────────────────────────────────────────────────────────────────────
// │ Required - renders HTML when block is placed in designer
// └────────────────────────────────────────────────────────────────────
renderDrag: function (elem, parent) {
    elem.innerHTML += `<div class='blockyinfo-basic'><p class='blockyname-basic'>${this.friendlyName}</p></div>`;
    elem.classList.add('decision-yes');
}

}; blockBase.register(blockYes);

johndkn commented 4 years ago

Woow! Thanks @robhoward for these details! This "wrapper" seems like an amazing solution to organize the blocks and the logic. Would love to see this in a pull request.

fj-vega commented 4 years ago

Thanks for the contribution @robhoward . Do you plan on creating a pull request? I want to use flowy but I really need the conditional blocks feature.

yellow1912 commented 4 years ago

Thank you @robhoward , I think it's an amazing idea. With this you can create "virtual" blocks that group things together. You can even decide to hide the children block if you dont want to show, amazing.

alyssaxuu commented 4 years ago

I am considering adding the functionality of creating "grouped blocks", for example, as shown by @robhoward in this image (which can be used for conditional blocks):

image

That way it could be dragged from the "panel" as a single unit, and have two outputs prebuilt.

I don't think I can come up with another way to implement conditional/logic blocks differently. Adding multiple arrows to the output of a block would be really tricky to implement with the current library, as it would cause issues with spacing and centering.

I'm going to explore the grouping functionalty for the next release (which will be bundled with new features and improvements), but it is not guaranteed yet.

robhoward commented 4 years ago

Just my $0.02 on this as we have this in production now:

An action, such as "text message replied to" can evaluate to true or false. We're finding that in some cases customers only care about one condition vs both.

So, as long as the group block could have the concept of removing one of the items in the group on the design surface.

alyssaxuu commented 4 years ago

Just my $0.02 on this as we have this in production now:

An action, such as "text message replied to" can evaluate to true or false. We're finding that in some cases customers only care about one condition vs both.

So, as long as the group block could have the concept of removing one of the items in the group on the design surface.

Interesting. I suppose that could be possible by adding an optional parameter when creating blocks that allows it to be broken apart by the user (or else locked together).

fqure commented 4 years ago

Possibly a new view to create a group block that is saved to the global block list to save space on the main view while keeping easy usability for those that don't need group blocks?

aadityak commented 4 years ago

Are conditional blocks as described by @johndkn planned anytime soon?

brijesh-k-bharattechlabs-com commented 4 years ago

@alyssaxuu are you planning to implement conditional blocks anytime soon?

brijesh-k-bharattechlabs-com commented 4 years ago

@robhoward have you implement conditional blocks and submit a pull request?

cdebattista commented 4 years ago

Here is my way.

I have a "Class" who is loading modules window.Automation. Each module has his own functions but have same requirements info. All forms elements is linked with his hidden input.

window.ConditionSource = {
    init: function () {
        this.info = {
            class: 'ConditionSource',
            name: 'source',
            title: 'Source',
            blocks: {
                canvas: '...../block.html'
            },
            selects: [
                {name: 'ConditionSource_selectState', hidden: 'source_state'},
                {name: 'ConditionSource_selectSource', hidden: 'source_id', populate: true, function: 'selectSource'}
            ]
        }
    },
....
}

I have a test button to match with the first contact in DB. Success = green, failed = red. if success but his parent is false = red

WIP, have to make block allowParent, etc...

I didn't want a popup to enter settings. I wanted to do all in the canvas, to be faster.

check video img

papeventures commented 3 years ago

HS does a good job with having the arrows pre-rendered for decision points. Labels on the arrows would be amazing! https://blog.hubspot.com/customers/sales-management-pain-points-workflow-automation

jatinderbhola commented 3 years ago

Here is my way.

I have a "Class" who is loading modules window.Automation. Each module has his own functions but have same requirements info. All forms elements is linked with his hidden input.

window.ConditionSource = {
    init: function () {
        this.info = {
            class: 'ConditionSource',
            name: 'source',
            title: 'Source',
            blocks: {
                canvas: '...../block.html'
            },
            selects: [
                {name: 'ConditionSource_selectState', hidden: 'source_state'},
                {name: 'ConditionSource_selectSource', hidden: 'source_id', populate: true, function: 'selectSource'}
            ]
        }
    },
....
}

I have a test button to match with the first contact in DB. Success = green, failed = red. if success but his parent is false = red

WIP, have to make block allowParent, etc...

I didn't want a popup to enter settings. I wanted to do all in the canvas, to be faster.

check video img

@cdebattista amazing work! Can you be kind enough to jump start toward the similar direction. I am trying to build similar application (user flow). Any help/ direction would be really appreciated.

loralll0 commented 2 years ago

@robhoward I want to have only one child when you drop the block like you have in DailyStory, not like this one: multipleParents Can you help me with this? Thanks

robhoward commented 2 years ago

Hi @laura040796 unfortunately my version of flowy is pretty much completely re-written, so I'm not sure how much help the code would be. But I can tell you how it works:

During the snapping event a check is done to see if the block is allowed to snap to the parent:

            // check if can snap these blocks together
            if (!blockBase.allowedToSnapToParent(drag, parent, first))
                return false;

The allowedToSnapToParent method walks through the conditions to determine if snapping will return true/false. For example:

    // Is this parent in the child's allowed parents list
    if (isParentInChildAllowedList()) {
        allowed_to_snap = true;
    } else {
        DsUtility.statusMessage(`'${child_block.friendlyName}' cannot be used with '${parent_block.friendlyName}'`);
        allowed_to_snap = false;
    }

    // Is this child in the parent's allowed childs list
    if (isChildInParentAllowedList()) {
        allowed_to_snap = true;
    } else {
        DsUtility.statusMessage(`'${child_block.friendlyName}' cannot be used with '${parent_block.friendlyName}'`);
        allowed_to_snap = false;
    }

    // Does the parent allow children
    if (allowed_to_snap && parent_block && undefined !== parent_block.allowChildren && !parent_block.allowChildren) {
        DsUtility.statusMessage(`'${parent_block.friendlyName}' cannot have additional steps`);
        allowed_to_snap = false;
    }

    // Does this parent already have children?
    if (allowed_to_snap) {
        if (isChildAllowedInParent(parent, child_block)) {
            allowed_to_snap = true;
        } else {
            allowed_to_snap = true;
        }
    }
jhaineymilevis commented 2 years ago

@robhoward is possible see your version?

robhoward commented 2 years ago

@jhaineymilevis unfortunately it's pretty much been re-written from the original flowy at this point. What are you trying to solve for?