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!

6 Likes

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!

4 Likes

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

1 Like

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

1 Like

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

1 Like