Workflow Visualization for Bubble Editor

Hi everyone!

Like many of you, I’ve been struggling with the new Bubble Workflow Editor.

To help navigate this transition, I developed a workaround: a visual logs panel that simulates the original workflow logic.

Gravação de Tela 2025-04-28 às 21.00.08 (1)

Main features:

– Logs are automatically created when you click mapped workflow buttons.

– Logs are clickable and trigger the same behavior as the original workflow buttons.

– Mapped buttons are highlighted in yellow.

– The currently clicked log or button is highlighted in blue.

– You can reorder the logs by dragging and dropping them to simulate the workflow flow.

Important notes:

– It currently works only inside the Backend Workflows page or Frontend Workflows page. If you change pages, the script needs to be refreshed.

– Some bugs are expected — this is a workaround, not a final solution!

Future ideas:

– We hope to turn it into a Chrome Extension soon.

– Collaboration is 100% welcome! Feel free to improve it, fork it, or help evolve it into a better tool.

(function() {
    if (window.workflowTrackerInitialized) return;
    window.workflowTrackerInitialized = true;

    let savedLogs = [];
    let currentLogSelected = null;
    let currentSelectedButton = null;
    let draggedEntry = null;
    let workflowResizeArea = null;
    let mouseOverLogArea = false;

    const logsContainer = document.createElement('div');
    logsContainer.id = 'logs-container';
    logsContainer.style.cssText = `
        display: flex;
        flex-direction: column;
        gap: 10px;
        overflow-y: auto;
        height: 100%;
        padding: 10px;
    `;

    const workflowLog = document.createElement('div');
    workflowLog.id = 'workflow-log';
    workflowLog.style.cssText = `
        display: flex;
        flex-direction: column;
        width: 280px;
        background: #fff;
        border-left: 1px solid #ccc;
        border-right: 1px solid #B0B0B0;
        height: 100%;
        box-sizing: border-box;
        font-family: 'Helvetica Neue', sans-serif;
        font-size: 13px;
        color: #333;
        pointer-events: auto;
    `;

    const logHeader = document.createElement('div');
    logHeader.style.cssText = `
        font-weight: bold;
        font-size: 13px;
        padding: 15px 12px;
        color: #000;
        text-align: left;
        border-bottom: 1px solid #ccc;
        height: 50px;
        display: flex;
        align-items: center;
        justify-content: space-between;
    `;
    logHeader.innerHTML = `
        <span>Workflow Click Logs</span>
        <button id="reset-logs" style="
            background: red;
            color: white;
            border: none;
            border-radius: 4px;
            padding: 4px 8px;
            cursor: pointer;
            font-size: 11px;
        ">Reset</button>
    `;

    workflowLog.appendChild(logHeader);
    workflowLog.appendChild(logsContainer);

    function resetHighlights() {
        document.querySelectorAll('.log-entry').forEach(entry => {
            entry.style.backgroundColor = '#f9f9f9';
            entry.classList.remove('active-log');
        });
        document.querySelectorAll('[role="treeitem"]').forEach(btn => {
            if (btn._hasLog) {
                btn.style.backgroundColor = 'rgba(255, 255, 0, 0.3)';
            } else {
                btn.style.backgroundColor = '';
            }
        });
    }

    function addLogEntry(label, targetButton, selectImmediately = false) {
        const entry = document.createElement('div');
        entry.className = 'log-entry';
        entry.draggable = true;
        entry.style.cssText = `
            cursor: pointer;
            padding: 8px 40px 8px 10px;
            background: #f9f9f9;
            border-radius: 5px;
            line-height: 1.8;
            overflow-wrap: break-word;
            position: relative;
            user-select: none;
        `;
        entry.textContent = label;
        entry._targetButton = targetButton;

        targetButton._hasLog = true;
        targetButton.style.backgroundColor = 'rgba(255, 255, 0, 0.3)';

        const deleteBtn = document.createElement('div');
        deleteBtn.textContent = 'Delete';
        deleteBtn.style.cssText = `
            position: absolute;
            top: 6px;
            right: 8px;
            font-size: 11px;
            color: red;
            cursor: pointer;
        `;

        deleteBtn.addEventListener('click', function(e) {
            e.stopPropagation();
            const index = savedLogs.indexOf(label);
            if (index !== -1) savedLogs.splice(index, 1);
            logsContainer.removeChild(entry);
            if (targetButton) {
                targetButton._hasLog = false;
                targetButton.style.backgroundColor = '';
            }
        });

        entry.addEventListener('click', function(e) {
            e.stopPropagation();
            
            if ((e.metaKey || e.ctrlKey) && entry._targetButton) {
                const itemId = entry._targetButton.getAttribute('data-item-id');
                if (itemId) {
                    const url = `https://bubble.io/page?id=startcollection&tab=BackendWorkflows&type=api&wf_item=${itemId}`;
                    window.open(url, '_blank');
                    return;
                }
            }

            resetHighlights();
            currentLogSelected = entry;
            currentLogSelected.classList.add('active-log');
            entry.style.backgroundColor = 'rgba(13, 41, 171, 0.15)';
            if (entry._targetButton) {
                currentSelectedButton = entry._targetButton;
                currentSelectedButton.style.backgroundColor = 'rgba(13, 41, 171, 0.15)';
                currentSelectedButton.scrollIntoView({ behavior: 'smooth', block: 'center' });
                currentSelectedButton.click();
            }
        });

        entry.addEventListener('dragstart', function() {
            draggedEntry = entry;
            setTimeout(() => {
                entry.style.opacity = '0.5';
            }, 0);
        });

        entry.addEventListener('dragover', function(e) {
            e.preventDefault();
            const bounding = this.getBoundingClientRect();
            const offset = bounding.y + (bounding.height / 2);
            if (e.clientY - offset > 0) {
                this.style.borderBottom = '2px solid #0D29AB';
                this.style.borderTop = '';
            } else {
                this.style.borderTop = '2px solid #0D29AB';
                this.style.borderBottom = '';
            }
        });

        entry.addEventListener('dragleave', function() {
            this.style.borderBottom = '';
            this.style.borderTop = '';
        });

        entry.addEventListener('drop', function(e) {
            e.preventDefault();
            if (draggedEntry !== this) {
                if (this.style.borderTop) {
                    logsContainer.insertBefore(draggedEntry, this);
                } else {
                    logsContainer.insertBefore(draggedEntry, this.nextSibling);
                }
            }
            this.style.borderBottom = '';
            this.style.borderTop = '';
            draggedEntry.style.opacity = '1';
            draggedEntry = null;
        });

        entry.addEventListener('dragend', function() {
            if (draggedEntry) {
                draggedEntry.style.opacity = '1';
                draggedEntry = null;
            }
        });

        entry.addEventListener('mouseenter', () => { mouseOverLogArea = true; });
        entry.addEventListener('mouseleave', () => { mouseOverLogArea = false; });

        entry.appendChild(deleteBtn);
        logsContainer.appendChild(entry);

        if (selectImmediately) {
            resetHighlights();
            entry.classList.add('active-log');
            entry.style.backgroundColor = 'rgba(13, 41, 171, 0.15)';
            if (targetButton) {
                targetButton.style.backgroundColor = 'rgba(13, 41, 171, 0.15)';
            }
        }
    }

    function monitorWorkflowButtons() {
        const parent = document.querySelector('div[data-name="workflowList"]') || document.querySelector('._1ql74v3k');
        if (!parent) return;

        parent.addEventListener('click', (e) => {
            const btn = e.target.closest('div[role="treeitem"]');
            if (btn && parent.contains(btn)) {
                const label = btn.innerText.trim();
                const entries = [...logsContainer.querySelectorAll('.log-entry')];
                const matchingEntry = entries.find(entry => entry.textContent.trim() === label);

                if (!savedLogs.includes(label)) {
                    savedLogs.push(label);
                    addLogEntry(label, btn, true);
                } else if (matchingEntry) {
                    matchingEntry.click();
                }
            }
        }, true);
    }

    function insertWorkflowLogPanel() {
        const rightDiv = document.querySelector('._101sfx31');
        if (rightDiv) {
            rightDiv.style.width = '280px';
            rightDiv.style.flexShrink = '0';
            rightDiv.style.pointerEvents = 'none';
            workflowLog.style.pointerEvents = 'auto';
            rightDiv.appendChild(workflowLog);

            const resizeArea = rightDiv.parentElement.querySelector('._16zpnji2');
            if (resizeArea) {
                workflowResizeArea = resizeArea;
                resizeArea.addEventListener('mousedown', (e) => {
                    if (mouseOverLogArea) {
                        e.stopPropagation();
                        e.preventDefault();
                    }
                }, true);
            }
        }
    }

    function observePageReady() {
        const observer = new MutationObserver(() => {
            const workflowArea = document.querySelector('div[data-name="workflowList"]') || document.querySelector('._1ql74v3k');
            if (workflowArea) {
                observer.disconnect();
                insertWorkflowLogPanel();
                monitorWorkflowButtons();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    document.addEventListener('click', (e) => {
        if (e.target.id === 'reset-logs') {
            savedLogs = [];
            logsContainer.innerHTML = '';
            document.querySelectorAll('[role="treeitem"]').forEach(btn => {
                btn._hasLog = false;
                btn.style.backgroundColor = '';
            });
        }
    });

    observePageReady();
})();

Ideally, Bubble will fix these problems soon, and we won’t even need this anymore.

The project is fully open for the community!

Let’s help each other during this difficult transition.

Thanks!

I would definitely try it if turned a chrome extension.

The Codeless Love Chrome extension features a lot of useful tools that enhances the Bubble editor experience. It would be nice to keep as many (useful) addons as possible in the same extension.

Thanks for the contribution!

wow, smart. - great contribution :clap:

Perhaps make a Push Request (PR) and @brenton.strine will wrap it into “Powerup” - an opensource chrome extension full of goodies like this.

You can copy this one to figure out how to make the Push Request or a Chrome extension … Left-to-right view mod in new workflow editor

lol i just checked out codeless love - yeah you defenetly shoud integrate it there

and this should be more wide available - i needed this before hahaha but did not knew it existed

BTW nice work the the log

the log would also be highly helpfull in the “redo” and “undo” section btw…