npm package: @industream/flowmaker-flowbox-ui
Description: FlowMaker box configuration UI toolkit
Latest version: 1.0.17

# @industream/flowmaker-flowbox-ui

Svelte-based configuration UI toolkit for FlowMaker boxes.

## Installation

```bash
npm install @industream/flowmaker-flowbox-ui
```

## Quick Start

```bash
# In your box's frontend folder
cd your-box/frontend

# Initialize project (creates package.json, vite.config.js)
npx flowbox-ui init

# Add a new config component
npx flowbox-ui add my-config

# Live preview (dev server with hot reload)
npx flowbox-ui start my-config

# Build
npm run build
```

## Project Structure

After initialization:

```
your-box/
├── frontend/
│   ├── package.json
│   ├── vite.config.js
│   └── src/
│       └── my-config/
│           ├── main.js
│           └── ConfigForm.svelte
└── config/
    └── config.my-config.jsbundle.js   # Build output
```

## CLI Commands

| Command | Description |
|---------|-------------|
| `npx flowbox-ui init` | Initialize a new project (auto-detects package manager) |
| `npx flowbox-ui add <name>` | Add a new config component |
| `npx flowbox-ui start <name>` | Start dev server with hot reload |
| `npx flowbox-ui update [name]` | Update vite configs and index.html from package templates |
| `npx flowbox-ui help` | Show help |

### Dev Server

The `start` command launches a development server with:
- Hot module replacement (HMR) for instant updates
- Carbon Design System components and styles pre-loaded
- Test data from `src/<component>/test/config.json` (auto-created if missing)
- Live display of current values and context

```bash
npx flowbox-ui start my-config
```

Edit `src/my-config/test/config.json` to customize test data:
```json
{
  "values": { "myField": "test value" },
  "context": {
    "nodeDefinition": {
      "flowElement": { "id": "vendor/my-box/1.0.0", "name": "My Box", "type": "pipe" },
      "inputs": ["input-1"],
      "outputs": ["output-1"]
    }
  }
}
```

Press `Ctrl+S` in the browser to simulate a save event.

### Update Command

The `update` command refreshes template files after upgrading `@industream/flowmaker-flowbox-ui`:

```bash
npx flowbox-ui update           # Auto-detects components from src/
npx flowbox-ui update my-config # Update for specific component only
```

**Files updated:**
- `vite.config.js` - Production build config (with @cdn-cache plugin support)
- `vite.config.dev.js` - Dev server config (with @cdn-cache plugin support)
- `index.html` - Dev server HTML with component name substituted

**Component handling:**
- Single component in `src/`: Creates `index.html`
- Multiple components: Creates `index.<name>.html` for each (e.g., `index.sql-editor.html`)
- No components found: Creates `index.template.html` with `__COMPONENT__` placeholders

## Usage

### main.js - Entry point

```js
import ConfigForm from './ConfigForm.svelte';
import { FlowBoxSvelteUI } from '@industream/flowmaker-flowbox-ui';

register('box-config-frontend', new FlowBoxSvelteUI(ConfigForm));
```

### ConfigForm.svelte - Your UI component

```svelte
<script>
    import { flowMakerBridge, getValue } from '@industream/flowmaker-flowbox-ui';

    const props = $props();
    const { context, values, validity, emit } = flowMakerBridge(props);

    const onInputChange = (e) => {
        $values.myField = getValue(e);

        // Optional: validation
        const isValid = ($values.myField ?? '').length > 0;
        if (isValid !== $validity.myField) {
            $validity.myField = isValid;
            emit('validity', $validity);
        }
    };
</script>

<cds-text-input
    label="My Field"
    value={$values?.myField ?? ''}
    invalid={$validity?.myField}
    oninput={onInputChange}>
</cds-text-input>

<style>
    /* Your scoped styles */
</style>
```

## Box Registration

In your box's registration decorator, set `uiConfig.type` to `"js-bundle"`:

```typescript
import { readFileSync } from 'fs';

const jsBundle = readFileSync('./config/config.my-config.jsbundle.js', 'utf-8');
const base64Bundle = Buffer.from(jsBundle).toString('base64');

@flowboxregistration({
    // ... other config
    uiConfig: {
        type: "js-bundle",
        implementation: base64Bundle,  // base64-encoded JS bundle (required)
        defaultOptions: {
            // your default form values
        }
    }
})
```

### uiConfig.implementation

- **For `type: "js-bundle"`**: Must contain the base64-encoded JS bundle. `null` is not compatible with js-bundle type.
- **When `null`**: FlowMaker uses the default UI maker project (only valid when `type` is `"ui-maker"` or omitted).

## Context

The `$context` store provides information about the current box and flow:

- **nodeDefinition** - The current box instance in the flow
  - `flowElement.id` - Box identifier (e.g., `vendor/box-name/1.0.0`)
  - `flowElement.name` - Instance name in the flow
  - `flowElement.options` - Current configuration options
  - `flowElement.type` - Box type (`source`, `pipe`, or `sink`)
  - `position` - Position on the canvas (x, y)
  - `inputs` - Array of input names (as redefined in the flow)
  - `outputs` - Array of output names (as redefined in the flow)

- **flowRepoItem** - The current flow
  - `repositoryItem` - Flow definition
  - `lastEditAt` - Last edit timestamp
  - `published` - Whether the flow is published
  - `tags` - Associated tags

- **flowBoxRegistrationInfo** - The box registration metadata
  - `id` - Box identifier
  - `displayName` - Human-readable name
  - `currentVersion` - Version string
  - `ioInterfaces` - Input/output definitions
  - `uiConfig` - UI configuration

Access in your component:
```svelte
<p>Box: {$context?.nodeDefinition?.flowElement?.name}</p>
<p>Type: {$context?.nodeDefinition?.flowElement?.type}</p>
<p>Flow published: {$context?.flowRepoItem?.published}</p>
```

## Multiple Components

Create multiple folders under `src/` for multiple config UIs:

```
src/
├── js-expression/
│   ├── main.js
│   └── ConfigForm.svelte
├── merge/
│   ├── main.js
│   └── ConfigForm.svelte
└── filter/
    ├── main.js
    └── ConfigForm.svelte
```

Running `npm run build` generates:
- `config/config.js-expression.jsbundle.js`
- `config/config.merge.jsbundle.js`
- `config/config.filter.jsbundle.js`

## CDN Imports (@cdn-cache)

Load external packages from CDN at runtime using `@cdn-cache/package@version`.

### JavaScript - Async Import

```svelte
<script>
    import { onMount } from 'svelte';

    onMount(async () => {
        const cmModule = await import("@cdn-cache/codemirror@5.65.18/lib/codemirror.js");
        await import("@cdn-cache/codemirror@5.65.18/mode/sql/sql.js");

        const CodeMirror = cmModule.default || cmModule;
        // Use CodeMirror...
    });
</script>
```

### CSS - svelte:head

```svelte
<svelte:head>
    <link rel="stylesheet" href="http://@cdn-cache/codemirror@5.65.18/lib/codemirror.css">
</svelte:head>
```

Add packages as peerDependencies (npm 7+ auto-installs them for dev mode, and they can be detected for CDN commissioning).
Use `@cdn-cache` to import external packages that will be loaded from a CDN at runtime. This keeps your bundle small while allowing access to any npm package.

### Usage in main.js

Since IIFE bundles don't support top-level await, wrap your code in an async IIFE and use dynamic `import()` with `Promise.all`:

```js
import ConfigForm from './ConfigForm.svelte';
import { FlowBoxSvelteUI } from '@industream/flowmaker-flowbox-ui';

(async () => {
    // Load CDN dependencies
    const [lodash, dayjs] = await Promise.all([
        import("@cdn-cache/lodash-es@4.17.21"),
        import("@cdn-cache/dayjs@1.11.10"),
    ]);

    console.log(lodash.range(1, 10));
    console.log(dayjs().format());

    register('box-config-frontend', new FlowBoxSvelteUI(ConfigForm));
})();
```

### Format

```
@cdn-cache/package@version[/path]
@cdn-cache/@scope/package@version[/path]
```

- **package**: npm package name (required)
- **version**: exact version number (required) - build will error if missing
- **path**: path within the package (optional)

### How it works

| Mode | Behavior |
|------|----------|
| **Dev** (`flowbox-ui start`) | Resolves to `node_modules/package[/path]` for local development |
| **Build** (`npm run build`) | Transforms to `http://@cdn-cache/...` in output |

### Setup

Add CDN packages as peerDependencies in your `package.json`:

```json
{
  "peerDependencies": {
    "lodash-es": "4.17.21",
    "dayjs": "1.11.10"
  }
}
```

Then run `npm install` to install them for dev mode. npm 7+ auto-installs peer dependencies.

### Runtime replacement

At runtime, FlowMaker replaces `http://@cdn-cache/` with the actual CDN URL before executing the bundle. The exact version in the import path ensures reproducible builds.

## Tips

### Accessing store values
Use `$` prefix to access/modify store values:
```svelte
$values.myField = 'new value';
console.log($values.myField);
```

### Getting raw value from input events
```js
import { getValue } from '@industream/flowmaker-flowbox-ui';

const onChange = (e) => {
    $values.field = getValue(e);  // extracts e.target.value
};
```

### Emitting changes
```js
// Emit updated values
emit('values', $values);

// Emit validation state
emit('validity', $validity);
```

### Using Carbon Design System components
The host provides Carbon Design System web components:
```svelte
<cds-text-input label="Name" value={$values.name} oninput={onNameChange} />
<cds-dropdown label="Type" oninput={onTypeChange}>
    <cds-dropdown-item value="a">Option A</cds-dropdown-item>
    <cds-dropdown-item value="b">Option B</cds-dropdown-item>
</cds-dropdown>
```

## API

### FlowBoxSvelteUI

Class that wraps a Svelte component for FlowMaker integration.

```js
import { FlowBoxSvelteUI } from '@industream/flowmaker-flowbox-ui';

const ui = new FlowBoxSvelteUI(MyComponent);
ui.mount('target-element-id');
ui.setProps({ values: {}, context: {} });
ui.on('values', (data) => console.log(data));
ui.destroy();
```

### flowMakerBridge(props)

Creates Svelte stores for FlowMaker communication.

```js
const { values, context, validity, emit } = flowMakerBridge(props);
```

Returns:
- `values` - Writable store for form values
- `context` - Writable store for flow context
- `validity` - Writable store for validation state
- `emit` - Function to emit events to FlowMaker

### getValue(event)

Extracts value from input events.

```js
const value = getValue(e);  // returns e.target.value
```