Passing variable between menu and client plugin

We are creating some plugins for git-integration. We are able to execute git commands (clone, pull, branch) from a menu plugin.

One of the requirements we have is to display the actual branch somewhere in the status bar so we need to pass this information from the menu context to a client plugin context.

What would be the preferred way of passing and updating such a variable value? We tried using config files but had issues with the dynamic nature in case of a change of branch.

I also see suggestions about using IPC, but it seems we cannot create custom events to pass and catch.

Hi there!

Great question about plugin communication in Camunda Modeler. You’re working on an interesting Git integration plugin!

Based on the Camunda Modeler plugin architecture, you have:

  • Menu plugins that run in the Electron main process (where you can execute Node.js modules like Git commands)
  • Client plugins that run in the renderer process (where you can extend the UI, including the status bar)

The Challenge

The official Camunda Modeler plugin documentation doesn’t provide explicit examples of communication between menu plugins and client plugins. However, since Camunda Modeler is built on Electron, you’re correct that IPC (Inter-Process Communication) is the standard approach for this scenario.

Recommended Solutions

1. Electron IPC with Custom Events

While you mentioned you can’t create custom events, you actually can! Here’s the pattern:

In your menu plugin (main process):

const { ipcMain } = require('electron');

// Listen for requests from renderer
ipcMain.handle('get-current-branch', async () => {
  // Your git logic here
  return getCurrentBranch();
});

// Send updates to renderer when branch changes
function notifyBranchChange(newBranch) {
  // Send to all renderer processes
  BrowserWindow.getAllWindows().forEach(window => {
    window.webContents.send('branch-changed', newBranch);
  });
}

In your client plugin (renderer process):

const { ipcRenderer } = require('electron');

// Request current branch
const currentBranch = await ipcRenderer.invoke('get-current-branch');

// Listen for branch changes
ipcRenderer.on('branch-changed', (event, newBranch) => {
  updateStatusBar(newBranch);
});

2. Alternative: Shared File System

Since both processes can access the file system, you could:

  • Write branch info to a temporary file from the menu plugin
  • Use a file watcher in the client plugin to detect changes
  • This avoids IPC complexity but may be less performant

3. Plugin Architecture Consideration

You might also consider consolidating more logic into the client plugin if possible, since it can still execute some Node.js operations (though it needs to be bundled).

Resources

The GitHub repositories might have community examples that show IPC patterns, even if not officially documented.

Would you like me to help you work through the specific IPC implementation details, or do you have questions about any of these approaches?

I have trouble importing electron in the client plugin (which renders a React component).

Ah, that’s the key issue! You’re running into the fundamental limitation of the renderer process in Electron.

The Problem

In Camunda Modeler’s client plugins (renderer process), you cannot directly import Electron modules like const { ipcRenderer } = require('electron') because:

  1. The renderer process runs in Chromium, not Node.js
  2. Node.js modules need to be bundled for the renderer process
  3. Electron’s security model restricts direct access to Node.js APIs

Solutions for IPC Communication

Option 1: Use Electron’s Context Bridge (Recommended)

If Camunda Modeler exposes a context bridge, you could access IPC through window.electronAPI or similar. However, this would need to be provided by Camunda Modeler itself.

Option 2: Alternative Communication Patterns

Since direct Electron IPC is challenging in your client plugin, here are practical alternatives:

A. File-based Communication

// In your menu plugin (main process)
const fs = require('fs');
const path = require('path');

function updateBranchInfo(branch) {
  const statusFile = path.join(__dirname, 'branch-status.json');
  fs.writeFileSync(statusFile, JSON.stringify({ branch, timestamp: Date.now() }));
}

// In your client plugin (renderer process)
// Use a polling mechanism or file watcher
setInterval(() => {
  fetch('./plugins/your-plugin/branch-status.json')
    .then(response => response.json())
    .then(data => updateStatusBar(data.branch))
    .catch(() => {}); // Handle file not found
}, 1000);

B. Local Storage/IndexedDB Bridge

// Menu plugin writes to a shared location
// Client plugin reads from the same location

C. HTTP/WebSocket Communication

Set up a simple local server in your menu plugin that your client plugin can communicate with.

Option 3: Consolidate Logic in Client Plugin

Consider moving more of your Git operations to the client plugin itself:

// In client plugin, use bundled git operations
import { execSync } from 'child_process'; // This needs to be bundled

// But remember: you'll need to bundle Node.js modules with Webpack

Webpack Configuration for Bundling

If you want to include Node.js modules in your client plugin, your webpack.config.js needs to handle this:

module.exports = {
  target: 'electron-renderer',
  node: {
    __dirname: false,
    __filename: false
  },
  externals: {
    // Mark certain modules as external if needed
  }
};

Recommendation

For your Git integration use case, I’d suggest:

  1. File-based communication as the simplest approach
  2. Keep Git commands in the menu plugin (main process)
  3. Use a JSON file to share branch status
  4. Poll or watch the file from your React component in the client plugin

This avoids the complexity of IPC while still achieving your goal of displaying branch information in the status bar.

Would you like me to help you implement the file-based approach, or do you have questions about any of these alternatives?

Does fetch in a client plugin allow access to the local file system? And does the current directory point to the resource folder in my library?

Great question! You’re absolutely right to question this approach.

File System Access Limitations

No, fetch() in a client plugin cannot access arbitrary local files from the file system. The renderer process in Electron (Chromium) has security restrictions that prevent direct file system access via fetch().

However, Camunda Modeler does provide some helper functions for client plugins:

Available Helper Functions

// These are globally available in client plugins:
getModelerDirectory() // Points to the Camunda Modeler installation directory
getPluginsDirectory() // Points to the plugins directory

// Example usage:
function MyPlugin(eventBus, canvas) {  
  var img = document.createElement('img');  
  img.src = getPluginsDirectory() + "/my-plugin/image.png";  
  canvas.getContainer().appendChild(img);  
}

But this only works for static resources (like images) that are served by the modeler, not for dynamic JSON files you want to read/write.

Better Approaches for Your Use Case

Option 1: Use DOM Events for Communication

Instead of files, use custom DOM events:

Menu Plugin (main process):

// When branch changes, send to renderer
function notifyBranchChange(branch) {
  const windows = require('electron').BrowserWindow.getAllWindows();
  windows.forEach(window => {
    window.webContents.executeJavaScript(`
      window.dispatchEvent(new CustomEvent('git-branch-changed', { 
        detail: { branch: '${branch}' } 
      }));
    `);
  });
}

Client Plugin (renderer process):

// Listen for branch changes
window.addEventListener('git-branch-changed', (event) => {
  const branch = event.detail.branch;
  updateStatusBar(branch);
});

// Request current branch
window.dispatchEvent(new CustomEvent('git-branch-request'));

Option 2: Use Local Storage Bridge

Menu Plugin: Write to a temp file that gets served as a static resource
Client Plugin: Access it via the plugin directory helpers

Option 3: Consolidate in Client Plugin

Move the Git operations to your client plugin and bundle the necessary Node.js modules:

// In your client plugin, bundle git operations
import { execSync } from 'child_process'; // Needs webpack bundling

function getCurrentBranch() {
  try {
    return execSync('git branch --show-current', { encoding: 'utf8' }).trim();
  } catch (error) {
    return 'unknown';
  }
}

Recommendation

For your Git integration, I’d suggest Option 1 (DOM Events) as it’s:

  • Clean and direct
  • Doesn’t require file system workarounds
  • Leverages Electron’s ability to execute JavaScript in the renderer

The menu plugin can execute Git commands and push updates to the renderer via executeJavaScript(), while the client plugin listens for these events and updates the status bar.

Would you like me to help you implement the DOM event approach, or would you prefer to explore bundling Git operations directly in the client plugin?

help me implement the DOM event approach, specifically for the client side which renders a react component

Perfect! Let me help you implement the DOM event approach for the client side with a React component.

Client Plugin with React Component

Here’s how to structure your client plugin to listen for DOM events and update your React component:

1. Main Client Plugin File (client.js)

import React from 'react';
import ReactDOM from 'react-dom';
import { registerBpmnJSPlugin } from 'camunda-modeler-plugin-helpers';
import GitStatusBar from './GitStatusBar'; // Your React component

// Create a container for your status bar component
let statusBarContainer = null;
let gitStatusComponent = null;

function GitIntegrationPlugin(eventBus, canvas) {
  // Initialize the status bar when the modeler is ready
  eventBus.on('import.done', function() {
    if (!statusBarContainer) {
      createStatusBar(canvas);
    }
  });

  // Listen for git branch changes from menu plugin
  window.addEventListener('git-branch-changed', (event) => {
    const { branch } = event.detail;
    if (gitStatusComponent) {
      gitStatusComponent.updateBranch(branch);
    }
  });

  // Request initial branch info when plugin loads
  window.addEventListener('DOMContentLoaded', () => {
    window.dispatchEvent(new CustomEvent('git-branch-request'));
  });
}

function createStatusBar(canvas) {
  // Create container in the modeler UI
  const modelerContainer = canvas.getContainer().parentElement;
  statusBarContainer = document.createElement('div');
  statusBarContainer.id = 'git-status-bar';
  statusBarContainer.style.cssText = `
    position: absolute;
    bottom: 10px;
    right: 10px;
    z-index: 1000;
  `;
  
  modelerContainer.appendChild(statusBarContainer);

  // Render React component
  gitStatusComponent = ReactDOM.render(
    <GitStatusBar />, 
    statusBarContainer
  );
}

// Register the plugin
registerBpmnJSPlugin({
  __init__: ['gitIntegrationPlugin'],
  gitIntegrationPlugin: ['type', GitIntegrationPlugin]
});

2. React Component (GitStatusBar.jsx)

import React, { useState, useEffect, useRef } from 'react';

const GitStatusBar = () => {
  const [currentBranch, setCurrentBranch] = useState('unknown');
  const [isLoading, setIsLoading] = useState(true);
  const componentRef = useRef();

  useEffect(() => {
    // Listen for branch updates from menu plugin
    const handleBranchChange = (event) => {
      setCurrentBranch(event.detail.branch);
      setIsLoading(false);
    };

    // Listen for initial branch response
    const handleBranchResponse = (event) => {
      setCurrentBranch(event.detail.branch);
      setIsLoading(false);
    };

    window.addEventListener('git-branch-changed', handleBranchChange);
    window.addEventListener('git-branch-response', handleBranchResponse);

    // Request initial branch info
    window.dispatchEvent(new CustomEvent('git-branch-request'));

    // Cleanup
    return () => {
      window.removeEventListener('git-branch-changed', handleBranchChange);
      window.removeEventListener('git-branch-response', handleBranchResponse);
    };
  }, []);

  // Expose update method for direct calls (alternative approach)
  useEffect(() => {
    if (componentRef.current) {
      componentRef.current.updateBranch = (branch) => {
        setCurrentBranch(branch);
        setIsLoading(false);
      };
    }
  }, []);

  const handleBranchClick = () => {
    // Request branch refresh
    setIsLoading(true);
    window.dispatchEvent(new CustomEvent('git-branch-request'));
  };

  return (
    <div 
      ref={componentRef}
      style={{
        backgroundColor: '#f0f0f0',
        border: '1px solid #ccc',
        borderRadius: '4px',
        padding: '4px 8px',
        fontSize: '12px',
        cursor: 'pointer',
        display: 'flex',
        alignItems: 'center',
        gap: '4px'
      }}
      onClick={handleBranchClick}
      title="Click to refresh branch info"
    >
      <span>🌿</span>
      {isLoading ? (
        <span>Loading...</span>
      ) : (
        <span>{currentBranch}</span>
      )}
    </div>
  );
};

export default GitStatusBar;

3. Alternative: Using React Hooks for Event Handling

If you prefer a more React-like approach, create a custom hook:

// useGitBranch.js
import { useState, useEffect } from 'react';

export const useGitBranch = () => {
  const [branch, setBranch] = useState('unknown');
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const handleBranchChange = (event) => {
      setBranch(event.detail.branch);
      setIsLoading(false);
    };

    const handleBranchResponse = (event) => {
      setBranch(event.detail.branch);
      setIsLoading(false);
    };

    window.addEventListener('git-branch-changed', handleBranchChange);
    window.addEventListener('git-branch-response', handleBranchResponse);

    // Request initial branch
    window.dispatchEvent(new CustomEvent('git-branch-request'));

    return () => {
      window.removeEventListener('git-branch-changed', handleBranchChange);
      window.removeEventListener('git-branch-response', handleBranchResponse);
    };
  }, []);

  const refreshBranch = () => {
    setIsLoading(true);
    window.dispatchEvent(new CustomEvent('git-branch-request'));
  };

  return { branch, isLoading, refreshBranch };
};

// Then in your component:
const GitStatusBar = () => {
  const { branch, isLoading, refreshBranch } = useGitBranch();
  
  return (
    <div onClick={refreshBranch}>
      🌿 {isLoading ? 'Loading...' : branch}
    </div>
  );
};

4. Menu Plugin Side (for reference)

Your menu plugin should dispatch these events:

// In your menu plugin
function notifyBranchChange(branch) {
  const windows = require('electron').BrowserWindow.getAllWindows();
  windows.forEach(window => {
    window.webContents.executeJavaScript(`
      window.dispatchEvent(new CustomEvent('git-branch-changed', { 
        detail: { branch: '${branch}' } 
      }));
    `);
  });
}

// Listen for branch requests
function setupBranchRequestListener() {
  const windows = require('electron').BrowserWindow.getAllWindows();
  windows.forEach(window => {
    window.webContents.executeJavaScript(`
      window.addEventListener('git-branch-request', () => {
        // This will be handled by your menu plugin logic
      });
    `);
  });
}

This approach gives you:

  • Clean React component that responds to git branch changes
  • Event-driven communication between menu and client plugins
  • Ability to refresh branch info on demand
  • Proper cleanup of event listeners

Would you like me to help you with the menu plugin side implementation, or do you have questions about integrating this with your existing React component?