npm package: @phuoc2409/univer-toolkit
Description: Lightweight wrapper for Univer spreadsheet with templates, tags, and data API support
Author: PhuocMH
Latest version: 1.0.18

# Univer Toolkit – Nhúng Univer Spreadsheet + Data APIs (Select2) + Auto Form + Templates

Package: **`@phuoc2409/univer-toolkit`**

Univer Toolkit là một “bộ khung nhúng” (embed toolkit) dành cho **Univer Spreadsheet** với mục tiêu:  
> **Biến việc “kết nối API → lọc dữ liệu → đổ vào sheet” thành cấu hình (config) + UI có sẵn, không cần viết UI tay cho từng API.**

Bạn sẽ có:
- **Spreadsheet** (Univer) hiển thị trong `#sheet`
- **Sidebar** trong `#sidebar` để:
  - Chọn **nguồn dữ liệu** bằng **Select2**
  - **Tự sinh form input** theo `fields` (text/number/date/select/range…)
  - Thay thế biến `@param` trong URL/body/headers
  - Gọi API và **chèn dữ liệu** vào sheet theo `mapping.columns`

Ngoài ra còn có:
- **Templates API** để upload/insert template
- **Tags** để lọc theo template
👉 Đối với phần **Template**, khuyến nghị sử dụng **cấu trúc API được xây dựng bằng NestJS** (theo thiết kế sẵn của dự án) để đảm bảo tính mở rộng và đồng bộ giữa các service. Bạn có thể tải cấu trúc về để xem sử dụng hoặc là viết lại theo backend của bạn, nó chỉ đơn giản là API về templates và tags 
https://github.com/phuoc2426/samplate-template-api-nestjs.git

---

## Mục lục

- [1. Dùng nhanh theo CDN (unpkg) – chạy ở mọi dự án](#1-dùng-nhanh-theo-cdn-unpkg--chạy-ở-mọi-dự-án)
- [2. Dùng theo NPM (ESM/CJS) – phù hợp Vite/Webpack/Framework](#2-dùng-theo-npm-esmcjs--phù-hợp-vitewebpackframework)
- [3. Cấu hình quan trọng nhất: `window.USER_UNIVER_CONFIG`](#3-cấu-hình-quan-trọng-nhất-windowuser_univer_config)
- [4. Data APIs: Cấu hình nguồn dữ liệu + filter + mapping](#4-data-apis-cấu-hình-nguồn-dữ-liệu--filter--mapping)
- [5. Fields: Tự sinh UI input (text/select/date/range) + validate](#5-fields-tự-sinh-ui-input-textselectdaterange--validate)
- [6. Cơ chế `@param`: Ghép URL / body để lọc dữ liệu](#6-cơ-chế-param-ghép-url--body-để-lọc-dữ-liệu)
- [7. FAQ (Hỏi – Đáp) + ví dụ trả lời sẵn](#7-faq-hỏi--đáp--ví-dụ-trả-lời-sẵn)
- [8. Triển khai trên mọi loại dự án (Front framework & Fullstack)](#8-triển-khai-trên-mọi-loại-dự-án-front-framework--fullstack)
- [9. Troubleshooting nhanh](#9-troubleshooting-nhanh)

---

## 1. Dùng nhanh theo CDN (unpkg) – chạy ở mọi dự án

### 1.1. Load đúng thứ tự (rất quan trọng)

1) React + ReactDOM (Univer UMD cần)  
2) RxJS  
3) **jQuery + Select2** (**phải trước toolkit**)  
4) Univer presets UMD + locales  
5) CSS của Univer  
6) CSS của Toolkit  
7) Toolkit UMD  
8) `window.USER_UNIVER_CONFIG = {...}`  
9) `UniverToolkit.mount()`  

### 1.2. HTML mẫu (copy chạy ngay)

> **Bắt buộc có 2 div**: `#sidebar` và `#sheet`.

```html
<!doctype html>
<html lang="vi">
  <head>
    <meta charset="utf-8" />
    <title>Univer Toolkit UMD Demo (unpkg)</title>

    <!-- React -->
    <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

    <!-- RxJS -->
    <script src="https://unpkg.com/rxjs@7/dist/bundles/rxjs.umd.min.js"></script>

    <!-- jQuery + Select2 (PHẢI load trước toolkit) -->
    <script src="https://unpkg.com/jquery@3/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/select2@4.1.0-rc.0/dist/css/select2.min.css" />

    <!-- Univer core -->
    <script src="https://unpkg.com/@univerjs/presets/lib/umd/index.js"></script>
    <script src="https://unpkg.com/@univerjs/preset-sheets-core/lib/umd/index.js"></script>
    <script src="https://unpkg.com/@univerjs/preset-sheets-core/lib/umd/locales/en-US.js"></script>

    <!-- Univer styles -->
    <link rel="stylesheet" href="https://unpkg.com/@univerjs/preset-sheets-core/lib/index.css" />

    <!-- Univer Toolkit styles -->
    <link rel="stylesheet" href="https://unpkg.com/@phuoc2409/univer-toolkit/styles/univer-embed.css" />

    <!-- Optional: SheetJS -->
    <script src="https://unpkg.com/xlsx@0.18.5/dist/xlsx.full.min.js"></script>

    <!-- Univer Toolkit UMD -->
    <script src="https://unpkg.com/@phuoc2409/univer-toolkit/dist/univer-toolkit.umd.min.js"></script>
  </head>

  <body>
    <div id="page">
      <div id="sidebar"></div>
      <div id="sheet"></div>
    </div>

    <!-- CONFIG -->
    <script>
      const TEMPLATE_API_BASE = "http://127.0.0.1:8020";

      const DATA_APIS = [
        {
          id: "all_students",
          name: "👥 Tất cả sinh viên",
          method: "GET",
          url: "http://127.0.0.1:8000/users",
          mapping: {
            columns: [
              { key: "id", header: "ID" },
              { key: "name", header: "Họ tên" },
            ],
          },
        },
        {
          id: "products",
          name: "🛒 Sản phẩm",
          method: "GET",
          url: "https://dummyjson.com/products/search",
          responseSource: "products",
          body: {
            filters: {
              q: "@q",
            },
          },
          fields: {
            q: {
              label: "Từ khóa tìm kiếm",
              type: "text",
              placeholder: "Nhập từ khóa...",
            },
          },
          mapping: {
            columns: [
              { key: "id", header: "ID" },
              { key: "title", header: "Tên sản phẩm" },
              { key: "price", header: "Giá" },
              { key: "description", header: "Mô tả" },
              { key: "category", header: "Danh mục" },
            ],
          },
        },
      ];

      window.USER_UNIVER_CONFIG = {
        workbookName: "Demo Workbook",
        dataApis: DATA_APIS,
        templateApi: {
          baseUrl: TEMPLATE_API_BASE,
          headers: {},
        },
        defaultTemplateCategory: "default",
      };
    </script>

    <!-- BOOT -->
    <script>
      window.addEventListener("load", () => {
        requestAnimationFrame(() => {
          if (window.UniverToolkit && typeof window.UniverToolkit.mount === "function") {
            window.UniverToolkit.mount();
          } else {
            console.error("UniverToolkit not loaded! Check CDN loading order.");
          }
        });
      });
    </script>
  </body>
</html>
```

---

## 2. Dùng theo NPM (ESM/CJS) – phù hợp Vite/Webpack/Framework

Cài đặt:

```bash
npm i @phuoc2409/univer-toolkit
```

Import CSS + mount:

```ts
import "@phuoc2409/univer-toolkit/styles/univer-embed.css";
import { mount } from "@phuoc2409/univer-toolkit";

(window as any).USER_UNIVER_CONFIG = {
  workbookName: "My Workbook",
  dataApis: [],
};

mount();
```

> Nếu bạn chạy SSR (Next.js/Nuxt): chỉ gọi `mount()` ở client-side (`useEffect`, `onMounted`, `<client-only>`).

---

## 3. Cấu hình quan trọng nhất: `window.USER_UNIVER_CONFIG`

Toolkit đọc config từ biến toàn cục:

```js
window.USER_UNIVER_CONFIG = { ... };
```

### 3.1. Bảng tham số trong config tổng

| Tham số | Bắt buộc | Ý nghĩa | Ví dụ |
|---|---:|---|---|
| `workbookName` | ✅ | Tên workbook hiển thị | `"Demo Workbook"` |
| `dataApis` | ✅ | Danh sách nguồn dữ liệu để chọn và chèn vào sheet | `DATA_APIS` |
| `templateApi` | ❌ | Cấu hình backend template (tuỳ dự án) | `{ baseUrl, headers }` |
| `defaultTemplateCategory` | ❌ | Nhãn category default khi browse templates | `"default"` |
| `globalContext` | ❌ | Biến toàn cục dùng để resolve `@param` | `{ token, orgId }` |

### 3.2. `globalContext` dùng để làm gì?

`globalContext` là “context chung” (session context) dùng thay biến `@param` mà **không cần user nhập mỗi lần**.

<div style="background-color:#e6ffe6; border-left: 5px solid #00aa00; padding: 10px; border-radius: 5px; margin: 10px 0;">
<span style="color:#007700; font-weight:bold;">
💡 Thật ra là bạn nên viết riêng 1 controller get dữ liệu riêng, không cần phải gán token hay header gì cho cồng kềnh</span>
</div>

Ví dụ:
```js
window.USER_UNIVER_CONFIG = {
  // ...
  globalContext: {
    token: "Bearer abc...",
    orgId: 123,
  },
};
```
## 4. Data APIs: Cấu hình nguồn dữ liệu + filter + mapping

Mỗi phần tử trong `dataApis` đại diện cho **một nguồn dữ liệu** xuất hiện trong dropdown Select2.

### 4.1. Bảng tham số của một `DataApi`

| Tham số | Bắt buộc | Ý nghĩa |
|---|---:|---|
| `id` | ✅ | Định danh duy nhất |
| `name` | ✅ | Tên hiển thị (có thể kèm emoji) |
| `method` | ✅ | GET/POST/PUT… |
| `url` | ✅ | Endpoint (có thể chứa `@param` trong query) |
| `headers` | ❌ | Headers riêng cho API |
| `body` | ❌ | Payload gửi lên (có thể chứa `@param`) |
| `fields` | ❌ | Schema để toolkit tự sinh UI nhập liệu |
| `responseSource` | ❌ | Nếu response bọc array trong object (vd `products`, `rows`) |
| `mapping.columns` | ✅ | Mapping key → header để đổ vào sheet |

### 4.2. API đơn giản (GET không filter)

```js
{
  id: "all_students",
  name: "Tất cả sinh viên",
  method: "GET",
  url: "http://127.0.0.1:8000/users",
  mapping: {
    columns: [
      { key: "id", header: "ID" },
      { key: "name", header: "Họ tên" },
    ],
  },
}
```

### 4.3. API có filter (body.filters + `@param`)

```js
{
  id: "products",
  name: "Sản phẩm",
  method: "GET",
  url: "https://dummyjson.com/products/search",
  responseSource: "products",

  body: {
    filters: {
      q: "@q",          // <-- placeholder
    },
  },

  fields: {
    q: {
      label: "Từ khóa tìm kiếm",
      type: "text",
      placeholder: "Nhập từ khóa...",
      pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
    },
  },

  mapping: {
    columns: [
      { key: "id", header: "ID" },
      { key: "title", header: "Tên sản phẩm" },
      { key: "price", header: "Giá" },
    ],
  },
}
```

**Flow chạy:**
1) User chọn “Sản phẩm”
2) Toolkit render input `q`
3) User nhập `q = phone`
4) Toolkit tạo context `{ q: "phone" }`
5) Resolve `@q` → `"phone"` trong `body.filters.q`
6) Gọi API
7) Lấy list ở `responseSource="products"`
8) Insert vào sheet theo `mapping.columns`

---

## 5. Fields: Tự sinh UI input (text/select/date/range) + validate

`fields` là phần quan trọng để sidebar biết cần hiển thị input nào.

### 5.1. Các type thường dùng

#### Text
```js
q: { label: "Từ khóa", type: "text", placeholder: "Nhập..." }
```

#### Number (tuỳ dự án)
```js
limit: { label: "Giới hạn", type: "number", min: 1, max: 100 }
```

#### Date
```js
fromDate: { label: "Từ ngày", type: "date" }
toDate: { label: "Đến ngày", type: "date" }
```

#### Select – dữ liệu tĩnh
```js
status: {
  label: "Trạng thái",
  type: "select",
  options: {
    source: "static",
    items: [
      { value: "", label: "-- Tất cả --" },
      { value: "active", label: "Đang học" },
      { value: "graduated", label: "Tốt nghiệp" },
    ],
  },
}
```

#### Select – load options từ API
```js
termId: {
  label: "Học kỳ",
  type: "select",
  required: true,
  options: {
    source: "api",
    url: "http://127.0.0.1:8000/terms",
    valueKey: "id",
    labelKey: "name",
  },
}
```

#### Range – 2 input → 2 biến
```js
amountRange: {
  label: "Khoảng số tiền (VNĐ)",
  type: "range",
  rangeKeys: ["amountFrom", "amountTo"],
  placeholder: "Số tiền",
}
```

### 5.2. Validate (cách hiểu)
- `required: true` → bắt buộc nhập
- `pattern` (nếu bạn dùng) → regex validate
- `min/max` (number) → validate giá trị

---

## 6. Cơ chế `@param`: Ghép URL / body để lọc dữ liệu

Toolkit resolve biến theo nguyên tắc:

- Mọi chuỗi dạng `@xxx` sẽ được thay thế bằng:
  - giá trị từ `fields` user nhập (ưu tiên), hoặc
  - giá trị trong `globalContext`, hoặc
  - nếu không có → coi là thiếu tham số

### 6.1. Ghép URL query

Ví dụ:
```js
url: "http://127.0.0.1:8000/grades?courseId=@courseId&semester=@semester"
```

Khi user nhập `courseId=101`, `semester=2024-1`  
→ URL thực tế sẽ thành:
```
http://127.0.0.1:8000/grades?courseId=101&semester=2024-1
```

### 6.2. Ghép body.filters 
Ví dụ:
```js
body: {
  filters: {
    keyword: "@keyword",
    status: "@status",
    fromDate: "@fromDate",
    toDate: "@toDate"
  }
}
```

Khi user nhập các field tương ứng → toolkit thay vào body trước khi gọi API.

---

## 7. FAQ (Hỏi – Đáp) + ví dụ trả lời sẵn

### 7.1. “Nếu API có tham số đầu vào để lọc thì sao?”
**Không sao.** Bạn cấu hình `body.filters` (hoặc URL query) và đặt `@param` đúng tên.  
Sau đó cấu hình `fields` để toolkit hiển thị UI nhập tham số.

Ví dụ kiểu “báo cáo”:

```js
{
  id: "student_report",
  name: "Báo cáo sinh viên",
  method: "POST",
  url: "http://127.0.0.1:8000/reports/students",
  body: {
    filters: {
      keyword: "@keyword",
      termId: "@termId",
      departmentId: "@departmentId",
      status: "@status",
      fromDate: "@fromDate",
      toDate: "@toDate",
      // range example
      amountFrom: "@amountFrom",
      amountTo: "@amountTo",
    },
  },
  fields: {
    keyword: { label: "Từ khóa", type: "text" },
    termId: {
      label: "Học kỳ",
      type: "select",
      required: true,
      options: { source: "api", url: "http://127.0.0.1:8000/terms", valueKey: "id", labelKey: "name" },
    },
    status: {
      label: "Trạng thái",
      type: "select",
      options: {
        source: "static",
        items: [
          { value: "", label: "-- Tất cả --" },
          { value: "active", label: "Đang học" },
          { value: "graduated", label: "Tốt nghiệp" },
        ],
      },
    },
    amountRange: {
      label: "Khoảng số tiền (VNĐ)",
      type: "range",
      rangeKeys: ["amountFrom", "amountTo"],
      placeholder: "Số tiền",
    },
    fromDate: { label: "Từ ngày", type: "date" },
    toDate: { label: "Đến ngày", type: "date" },
  },
  mapping: {
    columns: [
      { key: "id", header: "ID" },
      { key: "name", header: "Tên" },
      { key: "balance", header: "Số dư" },
    ],
  },
}
```

**Chốt lại (nhớ 3 bước):**
1) Liệt kê tham số lọc trong `body.filters` hoặc URL query
2) Gắn `@param` vào đúng chỗ bạn muốn thay
3) Khai `fields.param` để có UI nhập và validate

---

### 7.2. “API trả về `{ products: [...] }` chứ không phải array thì sao?”
Nhiều trường hợp response trả về là data, rows, items,... thì ta đặt `responseSource: "rows"` (hoặc key chứa mảng).

---

### 7.3. “Làm dropdown select lấy options từ API?”
Trong field select:
```js
options: {
  source: "api",
  url: "http://.../terms",
  valueKey: "id",
  labelKey: "name",
}
```

---

## 8. Triển khai trên mọi loại dự án (Front framework & Fullstack)

Cách triển khai nhanh nhất là làm theo y như index.html mẫu ở mục 1, chỉ cần import theo unpkg là có thể sử dụng được
<div style="background-color:#fff4cc; border-left:5px solid #ff9800; padding:10px; border-radius:5px; margin:12px 0;">
  <span style="color:#cc6600; font-weight:bold;">
    ⚠️ Lưu ý: Với .NET hoặc các framework có view template riêng (Razor, MVC, Blazor, Thymeleaf…)
  </span>
  <br/>
  <span style="color:#cc6600;">
    Cần kiểm tra kỹ <b>URL import</b> khi nhúng script / asset.
    Một số view engine sử dụng ký tự <code>@</code> cho cú pháp template,
    có thể gây lỗi nếu URL CDN chứa <code>@</code> mà không được xử lý phù hợp.
  </span>
</div>

### 8.1. HTML thuần / CMS / Fullstack render server
- Render trang HTML
- Nhúng script CDN
- Set `window.USER_UNIVER_CONFIG`
- Gọi `UniverToolkit.mount()`

### 8.2. React/Vue/Angular (SPA)
- `npm i`
- Import CSS
- Set config trong runtime (hoặc fetch config từ backend rồi gán)
- `mount()` trong lifecycle (`useEffect`, `onMounted`, `ngAfterViewInit`)

### 8.3. Next.js / Nuxt (SSR)
- Chỉ mount phía client
- Không gọi mount ở server

---

## 9. Troubleshooting nhanh

- **`UniverToolkit not loaded`**
  - Sai thứ tự load script CDN
  - UMD URL sai
  - Toolkit load trước jQuery/Select2

- **Không hiện dropdown/select2**
  - Thiếu `select2.min.css`
  - Thiếu `jquery` hoặc `select2` script

- **Chọn API nhưng không hiện input**
  - Chưa khai `fields`
  - Bạn dùng `@param` nhưng chưa tạo `fields.param` (hoặc không có `globalContext`)

- **API gọi ok nhưng sheet rỗng**
  - Sai `responseSource`
  - `mapping.columns.key` không khớp dữ liệu trả về

---

## License

MIT