npm package: @markopolo_ai_inc/markopolo-email-editor
Description: Drag-and-drop React email template editor: blocks, live preview, export as JSON or HTML. Built with Create React App for the demo app; the **published package** is produced with Rollup (`npm run build:lib` → `lib/`).
Latest version: 1.0.29

# @markopolo_ai_inc/markopolo-email-editor

Drag-and-drop React email template editor: blocks, live preview, export as JSON or HTML. Built with Create React App for the demo app; the **published package** is produced with Rollup (`npm run build:lib` → `lib/`).

---

## Install

Your app must provide peer dependencies (single React tree, one copy of Ant Design / Font Awesome, etc.):

```bash
npm install @markopolo_ai_inc/markopolo-email-editor react react-dom antd @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome framer-motion react-color clsx axios
```

Always import the editor **and** its CSS:

```js
import EmailEditor, {
  exportTemplateJson,
  validateExportPayload,
  exportNormalizerToHtml,
  TEMPLATE_DATA_SCHEMA,
  createDefaultTemplateData,
  validateTemplateDataShape,
} from "@markopolo_ai_inc/markopolo-email-editor";
import "@markopolo_ai_inc/markopolo-email-editor/lib/index.css";
```

---

## Quick start

```jsx
import { useRef } from "react";
import EmailEditor, {
  exportTemplateJson,
} from "@markopolo_ai_inc/markopolo-email-editor";
import "@markopolo_ai_inc/markopolo-email-editor/lib/index.css";

export function TemplateEditor({ apiConfig = {} }) {
  const editorRef = useRef(null);

  const handleSave = () => {
    const { payload, issues } = exportTemplateJson(editorRef, {
      validate: true,
    });
    const errors = issues.filter((i) => i.severity === "error");
    if (errors.length) return;
    // payload: { blockList, bodySettings, aiContentRefs }
  };

  return (
    <>
      <button type="button" onClick={handleSave}>
        Save
      </button>
      <EmailEditor
        ref={editorRef}
        defaultBlockList={initialBlockList}
        defaultBodySettings={initialBodySettings}
        templateData={templateData}
        language="en"
        apiConfig={apiConfig}
      />
    </>
  );
}
```

---

## Main props

| Prop                      | Type                                                    | Default                  | Description                                                                                                                                                                                                                    |
| ------------------------- | ------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `defaultBlockList`        | `array` or `{ blockList, bodySettings, aiContentRefs }` | —                        | Initial template (see **Template shape** below).                                                                                                                                                                               |
| `defaultBodySettings`     | `object`                                                | from template / built-in | Global email body (width, colors, font, preheader).                                                                                                                                                                            |
| `aiContentRefs`           | `object`                                                | `{}`                     | Optional AI metadata map when loading saved templates.                                                                                                                                                                         |
| `templateData`            | `object`                                                | —                        | Live data for products, company, brand, placeholders (see **templateData**).                                                                                                                                                   |
| `language`                | `string`                                                | `"en"`                   | UI language key.                                                                                                                                                                                                               |
| `customLanguageLibraries` | `object`                                                | —                        | Extra i18n strings.                                                                                                                                                                                                            |
| `variableDefinitions`     | `object`                                                | built-in                 | Override placeholder metadata.                                                                                                                                                                                                 |
| `apiConfig`               | `object`                                                | `{}`                     | Usually **`companyId`** + **`headers`** only; upload / products / ML URLs default inside the package. See **`apiConfig`**.                                                                                                     |
| **`aiEnabled`**           | `boolean`                                               | **`true`**               | If **`false`**, the sidebar hides the “AI powered” text entry and the **Write using AI** switch and prompts in text settings. Existing dynamic blocks still export as before; users simply cannot turn AI mode on from the UI. |

Ref API: `ref.current.exportJson()`, `ref.current.exportHtml()`, plus `blockList` / `bodySettings` / `aiContentRefs` mirrors for reading state.

---

## `templateData`

Pass backend data as **`templateData`** so blocks and HTML export resolve placeholders consistently.

| Area         | Path                                                     | Role               |
| ------------ | -------------------------------------------------------- | ------------------ |
| Products     | `content.products`, `content.primaryProductId`           | Product containers |
| Menu         | `companyContext.navigation_links`                        | Menu blocks        |
| Social       | `companyContext.social_links`                            | Social blocks      |
| Branding     | `brandContext.brand_color`                               | Default CTA / button background color |
| Placeholders | `brandContext`, `companyContext`, `content`, `companyId` | Export / preview   |

Helpers (optional): `TEMPLATE_DATA_SCHEMA`, `createDefaultTemplateData(overrides)`, `validateTemplateDataShape(data)` → `{ ok, warnings }`.

Keep **`templateData.companyId`** in sync with **`apiConfig.companyId`** when you scope by company.

If a button/CTA block has no explicit background color set, the editor preview and HTML export fall back to **`templateData.brandContext.brand_color`**.

---

## `apiConfig`

The editor merges your **`apiConfig`** with built-in Markopolo-style endpoints (default host **`https://api-alpha.markopolo.ai/v1`**):

- **`…/upload-file`** (query `show=false`, company id as form field **`comment`**)
- **`…/knowledge-base/products/{companyId}`** (`page`, `limit`, `search` — used by the column product picker + Dashboard seed)
- **AI text (dynamic blocks)** uses a **separate ML host** by default: **`…/ml-service/content/generate-email-nodes`** on **`https://nbq-ml-api-stg.markopolo.ai/v1`** (override with **`mlGenerationApiBaseUrlV1`** or **`REACT_APP_ML_API_BASE_V1`**). Legacy **`…/ml/generate-content`** on the alpha API is **not** defaulted; set **`mlGenerationEndpoint`** only if you still use that route.
- **`emailTemplateId`** is set by default to **`450e1238-e962-4ef0-a854-817583f6e75d`** (`DEFAULT_EMAIL_TEMPLATE_ID`); it is sent in the ML request body as **`emailTemplateId`**. Override with **`apiConfig.emailTemplateId`** or **`REACT_APP_EMAIL_TEMPLATE_ID`**.

**You normally only pass:**

| Field           | Role                                            |
| --------------- | ----------------------------------------------- |
| **`companyId`** | Company / resource id (catalog + upload scope). |
| **`headers`**   | e.g. `{ Authorization: 'Bearer …' }`.           |

**Optional:**

| Field                                                                                                                                                                                | Role                                                                                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **`apiBaseUrlV1`** (alias **`apiBaseV1`**)                                                                                                                                           | Non-default **alpha** API root (upload + products). ML text uses **`mlGenerationApiBaseUrlV1`** unless you override **`mlEmailNodesEndpoint`**. |
| **`emailTemplateId`**                                                                                                                                                                | **`custom_email_templates`** id in the **generate-email-nodes** JSON body; defaults to **`450e1238-e962-4ef0-a854-817583f6e75d`** unless you override (or set **`REACT_APP_EMAIL_TEMPLATE_ID`**). |
| **`mlGenerationApiBaseUrlV1`** (alias **`mlApiBaseUrlV1`**)                                                                                                                          | ML service root for generate-email-nodes (default staging nbq-ml host).                                                                         |
| **`imageUploadEndpoint`**, **`productsEndpoint`**, **`mlEmailNodesEndpoint`**, **`mlGenerationEndpoint`** (legacy only), **`imageUploadQueryParams`**, **`imageUploadCompanyField`** | Override defaults when needed.                                                                                                                  |

**Typical integration:**

```jsx
<EmailEditor
  ref={editorRef}
  defaultBlockList={blocks}
  templateData={{ ...templateData, companyId: resourceId }}
  apiConfig={{
    companyId: resourceId,
    headers: { Authorization: `Bearer ${token}` },
  }}
/>
```

If you load products **outside** **`EmailEditor`**, you can build the same URLs with **`mergeMarkopoloApiConfig`** (exported from the package). The merged object includes **`emailTemplateId`** for the ML API unless you pass a different value.

```js
import { mergeMarkopoloApiConfig, DEFAULT_EMAIL_TEMPLATE_ID } from "@markopolo_ai_inc/markopolo-email-editor";
// mergeMarkopoloApiConfig({}).emailTemplateId === DEFAULT_EMAIL_TEMPLATE_ID
```

The repo’s demo **`Dashboard`** merges its **`companyId`** prop into **`apiConfig`** before **`EmailEditor`**.

---

## Export JSON (no demo `Header`)

Use a ref and **`exportTemplateJson`** (or `ref.current.exportJson()`):

```js
import { useRef } from "react";
import EmailEditor, {
  exportTemplateJson,
} from "@markopolo_ai_inc/markopolo-email-editor";

const { payload, issues } = exportTemplateJson(editorRef, { validate: true });
// validate: false → same payload shape, issues: []
```

### `validateExportPayload`

Same rules as the export modal; pure function:

```js
import { validateExportPayload } from "@markopolo_ai_inc/markopolo-email-editor";

const issues = validateExportPayload({ blockList, bodySettings });
```

Issue objects include **`severity`**, **`message`**, and a human-readable **`location`** (breadcrumb path with `→`, e.g. `Text — Discount → 3rd of 5 stacked blocks`).

---

## JSON → HTML

```js
import { exportNormalizerToHtml } from "@markopolo_ai_inc/markopolo-email-editor";

const html = exportNormalizerToHtml({ blockList, bodySettings, templateData });
```

---

## Template shape for `defaultBlockList`

1. Legacy: `defaultBlockList={[ ...blocks ]}`
2. Export object: `defaultBlockList={{ blockList, bodySettings, aiContentRefs }}` (or `blocks` instead of `blockList`).

---

## Custom variable definitions

Override defaults by passing **`variableDefinitions`**: `{ global: [...], product: [...], discount: [...] }` with `key`, `label`, `labelKey`, `source`, `type`, `token`, etc. (see package examples).

---

## Scripts (this repo)

| Script                      | Purpose                                                              |
| --------------------------- | -------------------------------------------------------------------- |
| `npm run build:lib`         | Rollup → `lib/index.js`, `lib/index.mjs`, `lib/index.css` (publish). |
| `npm start` / `npm run dev` | CRA demo (local development).                                        |

Install in this repo: `npm install`.

---

## Note on the demo app

`src/pages/Dashboard` (including its `Header` and export modal) is for local development only and is **not** part of the published public API. Integrate export and validation in your app as shown above. The demo merges **`companyId`** into **`apiConfig`** and uses the same built-in API defaults as **`EmailEditor`**.