BUILD YOUR FORM UI

用自己的介面填寫 OpenDashboard 表單

新報名網站請先使用流程接入與自訂 UI:在流程內啟用專屬 API,不需要應用管理金鑰。本頁是單份連接應用表單的合約;已有此類表單或只需單份回覆的應用可繼續使用。它不是流程填表 API。

你的網站可以自行設計頁面、按鈕、分頁及品牌。從 OpenDashboard 取得表單 JSON,按欄位 key 收集答案,再由網站後端送回 OpenDashboard。同一份定義可以用 React、Vue、原生 HTML 或其他技術顯示;答案格式保持相同。

本教程只有虛構資料,可下載表單定義 JSON及提交答案 JSON。應用金鑰留在後端,瀏覽器只呼叫你自己的網站。先用接入方式選擇填表/查看入口;新流程由 OpenDashboard 保存正式答案。普通連接應用表單不自動產生公開填表頁;OpenDashboard 流程/競賽私人頁可與外站 UI 共用同一申請。原站回寫 intake 是另一種接法。

1.先選接入方式,再選資料模型

要完成的工作使用哪套端點保存方式
需要草稿、審核補件及兩種 UI 共用同一流程workflows/workflow-runsOpenDashboard 保存申請和版本,外站 UI 與私人流程頁操作同一 run,見多步流程教程。
一份普通表單,提交後可修改自己的答案forms/responses每位 respondent 每份表單保存一份最新正式回覆。
隊伍報名,再分階段保存草稿及提交作品competitions/entries/submissions一個 entry 連接報名、隊員及各階段的草稿/提交版本。

普通 responses API 沒有草稿、競賽期限或前置階段規則。競賽生成的報名/階段表單必須使用競賽端點;把它送到普通 responses 會得到 409 competition_form_use_entry_api。本頁第 2–7 節示範普通表單,第 8 節說明競賽的接法。

先由管理員建立連接應用,保存伺服器端金鑰及表單 id。forms API 只讀取同一應用擁有的表單;不能把一般管理後台建立的任意表單 id 當作應用自己的表單。取得金鑰及建立定義。

2.由後端取得表單 JSON

表單已建立時,使用它的 id 讀取定義:

bash
curl --fail-with-body "$OPENDASHBOARD_BASE_URL/api/apps/v1/forms/$FORM_ID" \
  -H "Authorization: Bearer $OPENDASHBOARD_TEST_KEY"

回傳是表單物件本身,沒有外層 form 包裝。下方是節錄的合法 JSON 結構;實際 questions 包含該表單所有題目。

json
{
  "id": "evt_demo_form_ui",
  "title": "Synthetic workshop signup",
  "description": "Only synthetic data is used in this example.",
  "test": true,
  "created_at": "2026-01-01T00:00:00.000Z",
  "questions": [
    { "key": "program", "title": "課程", "type": "short_text", "required": true, "personal": false },
    { "key": "session", "title": "場次", "type": "dropdown", "options": ["Demo A", "Demo B"], "required": true, "personal": false },
    { "key": "interest", "title": "興趣程度", "type": "number", "min": 0, "max": 5, "integer": true, "unit": "", "required": true, "personal": false },
    {
      "key": "participants", "title": "參加者", "type": "repeating_group",
      "min": 1, "max": 4, "required": false, "personal": false,
      "fields": [
        { "key": "participant_ref", "label": "匿名參加者編號", "type": "text", "required": true, "personal": false },
        { "key": "category", "label": "類別", "type": "single_choice", "options": ["Demo beginner", "Demo advanced"], "required": true, "personal": false }
      ]
    }
  ]
}

下載完整建立表單 JSON。它是 POST /forms 的正文,不是某個已存在表單的 id。先在測試環境執行,保存回傳 id,再 GET 取得補齊預設值的完整定義:

bash
curl --fail-with-body "$OPENDASHBOARD_BASE_URL/api/apps/v1/forms" \
  -H "Authorization: Bearer $OPENDASHBOARD_TEST_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @custom-ui-form.json

成功回傳 201 { "id": "evt_…" }。首次建立不是冪等操作;如果結果未知,先查核建立結果,不要盲目再次建立。

3.把定義對應到自己的 UI

JSON 設定你的 UI 要怎樣使用
key答案的固定欄位名。用 program 傳課程答案,不用中文標題「課程」。
title/label題目顯示 title;群組子欄位顯示 label,沒有時用 title/key。
required標示必填並提示缺漏。群組 min 大於 0 也要求項目,即使 required 是 false。
type決定輸入控制及送出的 JSON 型別,見下表。
options選項值必須原樣送回;顯示文字相同也不能自行改成數字索引。
min/maxnumber 是數值範圍;repeating_group 是項目數量。兩者不能混用。
integer/unitnumber 的整數要求及顯示單位。群組數值欄位的規則可能在 number 物件,讀 field.number 或 field 本身。
fields群組每個項目/結果物件的子欄位。子欄位仍按自己的 key 收集。
personal資料能否進入報表的分類;不表示瀏覽器可以把欄位公開給所有訪客。
type可自行選擇的 UIanswers 中的值
short_text;群組內 text單行文字字串,最多 500 字。
paragraph多行文字字串,最多 5000 字。
number數值輸入JSON 數字;保留 0,未填留空或省略,不能把空值轉成 0。
date日期輸入YYYY-MM-DD 字串。
single_choice/dropdown單選按鈕/下拉一個 options 字串。
multiple_choice多選核取方塊options 字串陣列,沒有選擇用 [];必填時至少一個。
repeating_group新增/移除項目的區塊或小表格物件陣列,每個物件按 fields 的 key 保存。
group,限群組子欄位一組固定子欄位一個物件,使用子欄位 key。
labels,限群組/app_result 子欄位多個標籤字串陣列;完整限制見 API 參考。
section/text_block標題/說明不傳答案。
hidden由網站後端設定文字;不能讓訪客任意指定身份或來源值。
app_result顯示應用計算的結果fields 定義的物件;成績/計算結果由可信後端產生。
email/phone電郵/電話輸入字串,需 identified mode;永遠屬個人資料。
file_upload你的附件選擇及上傳流程HTTPS URL 字串,需 identified mode;不能直接傳 File、Base64 或 upload id。

這個 JSON 不是標準 JSON Schema,也沒有內建的版面、CSS 或 React 元件。可直接按 questions 生成控制,或設計固定頁面再對應 key。題目順序沿用 questions;unknown key 會被拒絕。表單標題、選項、說明及錯誤使用文字方式渲染,避免把內容直接插入 HTML。

hidden、app_result 及其答案只由可信後端設定或核對;只把訪客需要填寫及查看的定義/答案交給瀏覽器。對含 hidden 子欄位的群組也做相同處理,不要只隱藏最外層題目。

4.回傳答案的 JSON

下載完整提交答案 JSON。此檔供伺服器端測試,含由後端設定的 respondent 和 hidden 值;實際訪客瀏覽器不應自行選擇這兩項。

json
{
  "respondent": "demo-contact-ui",
  "answers": {
    "program": "Synthetic workshop",
    "session": "Demo A",
    "interest": 0,
    "preferred_date": "2026-10-15",
    "topics": ["Demo X", "Demo Y"],
    "participants": [
      { "participant_ref": "demo-person-a", "category": "Demo beginner" },
      { "participant_ref": "demo-person-b", "category": "Demo advanced" }
    ],
    "notes": "Synthetic example only.",
    "portal_source": "demo-portal"
  },
  "submitted_at": "2026-01-01T00:00:00Z",
  "replace": true,
  "expected_version": 0
}
正文字段誰提供/用途
respondent網站後端從已驗證身份或存取憑證取得的穩定匿名編號,4–64 個英文字、數字、_ 或 -。每次更新沿用;不用姓名或電郵。
answers按題目 key 收集的完整答案。array/object 保留結構,不能轉成一個 JSON 文字字串。
submitted_at這次操作固定的 ISO 時間。範例時間是合成值;真實新操作記錄當下時間,重試保留原值。
replacetrue 表示可取代已有回覆。更新送完整答案,省略的舊答案會被移除。
expected_version首次建立用 0;更新用讀回的目前版本。用來拒絕過時修改。
bash
curl --fail-with-body "$OPENDASHBOARD_BASE_URL/api/apps/v1/forms/$FORM_ID/responses" \
  -H "Authorization: Bearer $OPENDASHBOARD_TEST_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @custom-ui-response.json

首次保存及新的取代操作成功回傳 201 { "id": "reg_…", "version": 1 };更新後 version 增加。相同答案、submitted_at 及原 expected_version 的精確重試回傳 200 和原 id/version。

以上合成表單只有欄位型別及範圍驗證。額外業務規則,例如不同欄位之間的條件關係,需由你的可信後端處理或使用對應流程 API,不能只寫在瀏覽器。

5.瀏覽器和網站後端如何分工

使用者填寫 → 你的網站後端確認存取權、加入可信資料 → OpenDashboard 保存 → 你的網站顯示成功及 version。

可以在你自己的網站提供以下兩個介面;這些是網站自行實作的路徑,不是 OpenDashboard 已提供的端點:

自己網站的路徑例子返回/接收甚麼
GET /api/my-form可填寫的定義、目前使用者獲准查看的答案、目前 version;沒有回覆時 version 為 0。
POST /api/my-form接收 answers、expected_version、submitted_at;後端核對身份並加入 respondent、可信 hidden/app_result,轉送 OpenDashboard。

網站後端固定允許使用的 FORM_ID;不得接受訪客傳入任意 form id、respondent、OpenDashboard URL 或金鑰。每次讀寫核對登入/私人存取憑證與該回覆的關係,保留原有 CSRF 防護及請求大小限制。應用金鑰識別網站,不會替你識別訪客。

下方是瀏覽器的提交參考。答案由自己的 UI 收集;csrfToken 由網站提供,後端也必須驗證。沒有把 OpenDashboard 金鑰或 respondent 放入前端。提交期間停用按鈕,保留答案;同一次失敗重試沿用同一 submittedAt 和 version。

javascript
async function sendMyAnswers({ answers, version, submittedAt, csrfToken }) {
  const response = await fetch('/api/my-form', {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify({
      answers,
      expected_version: version,
      submitted_at: submittedAt
    })
  });
  const body = await response.json();
  if (!response.ok) {
    return { saved: false, status: response.status, error: body.error,
      fields: body.fields || {}, message: body.message };
  }
  return { saved: true, id: body.id, version: body.version };
}

自己的後端只有收到 OpenDashboard 保存成功才返回成功。網路錯誤/非 JSON 回應時,前端保留輸入及這次操作的時間/版本,提供查核或重試,不顯示已保存。

UI 可以在送出前檢查必填及格式,OpenDashboard 仍會在伺服器再驗證;讀取表單 JSON 不等於獲得其他人的答案。

6.修改及讀回自己的資料

後端用 GET /forms/$FORM_ID/responses?respondent=$RESPONDENT 讀取已授權的那一隊答案;不存在時 responses 是空陣列。不加 respondent 才回傳全體回覆,目前沒有 cursor 分頁。參數由已核對身份的後端提供,不能接受瀏覽器任意指定另一隊。

javascript
// Only run on the website server after access.respondent has been authorized.
const response = await fetch(
  process.env.OPENDASHBOARD_BASE_URL + '/api/apps/v1/forms/' +
  encodeURIComponent(process.env.OPENDASHBOARD_FORM_ID) +
  '/responses?respondent=' + encodeURIComponent(access.respondent),
  { headers: { Authorization: 'Bearer ' + process.env.OPENDASHBOARD_APP_KEY } }
);
if (!response.ok) throw new Error('Unable to read the current response');
const payload = await response.json();
const own = payload.responses.find(item => item.respondent === access.respondent);
// Filter hidden/application-owned values before returning permitted answers.
const currentVersion = own ? own.version : 0;

access 由你自己的後端登入/存取驗證取得,不從請求正文或網址直接讀 respondent。這段是伺服器參考片段,需接到網站既有的存取檢查及錯誤處理。

修改前載入目前答案和 version。送完整新答案並以該版本作 expected_version;成功後採用返回的新版本。收到 response_changed 時,保存使用者未送出的內容,再讀最新資料讓他核對,不能只提高版本後自動覆寫。

普通表單更新只保存最新回覆。若需保留每次事件原文,可另外使用 append-only records;競賽草稿及作品修訂使用第 8 節的專用端點。

7.驗證錯誤及附件

以下錯誤會拒絕整次寫入:

json
{
  "ok": false,
  "error": "invalid_answers",
  "message": "The response was not saved. Fix these and send it again.",
  "fields": {
    "participants.0.category": "Unknown choice."
  }
}

這個路徑對應第 1 個參加者的 category;序號由 0 開始。保留伺服器回傳的完整路徑,對應到自己的欄位,顯示文字錯誤並把焦點移到需修正的位置。message/fields 的文字可能是英文,網站可依 error code 顯示本地語言提示;不要只依英文句子判斷重試。

狀態/錯誤UI 處理
400 invalid_answers顯示 fields,保留答案,修正後重新提交。
401 invalid_app_key後端處理金鑰/應用設定;不能向訪客要求金鑰。
404 form_not_found核對固定表單 id、所屬應用及測試/正式模式。
409 form_closed/already_submitted顯示目前不能提交的原因;已有回覆需先讀取版本。
409 response_changed核對最新答案與自己的修改,不強行覆寫。
413 request_body_too_large縮減請求;JSON 上限 256 KB,檔案不放 JSON。
429 rate_limited後端按 Retry-After 等待,有限次重試。
5xx/逾時/網路中斷保留操作內容,查核未知結果,再依冪等規則重試。

file_upload 的 HTTPS 字串只保存一個檔案網址,不會自動備份檔案,也不是通用上傳元件。要由 OpenDashboard 保存私人 bytes,使用私人 uploads API,完成檔案驗證後保存固定 upload id 的關聯,讀取時由已授權後端取得短效下載 URL。upload id 目前不能直接當作普通 file_upload 答案;可在受控的 hidden 欄位或私人 records 中保存關聯。

不要把短效下載 URL 當作永久附件識別,也不要把一般管理表單的檔案 metadata 物件直接搬到 Connected Apps API。兩者格式不同。瀏覽器使用簽名上傳 URL 時,不帶應用金鑰。

8.自己的 UI 接競賽及階段提交

競賽仍可使用自己的頁面,但讀寫 entry/submission,不能用普通 responses。所有路徑仍以 /api/apps/v1 開始,由你的網站後端使用應用金鑰呼叫。

操作端點及正文
取得報名欄位、隊員及階段GET /competitions/:id;讀 entry.fields、entry.members.fields 和 stages 的 questions/window/prerequisites。
建立隊伍草稿POST /competitions/:id/entries;送 respondent、division、answers、members。
讀回隊伍及各階段狀態GET /entries/:id。
修改隊伍資料PUT /entries/:id;送 division、完整 answers、完整 members、上次 revision。
正式提交報名POST /entries/:id/submit;送 revision。
讀回某階段草稿/作品GET /entries/:id/submissions/:stage_key。
儲存階段草稿PUT /entries/:id/submissions/:stage_key;送 answers、revision,首次用 0。
正式提交作品POST /entries/:id/submissions/:stage_key/submit;送剛保存後的 revision。
改用 OpenDashboard 私人填寫頁POST /entries/:id/links;取得 url 後私下交給該隊聯絡人,可設定 expires_at。

建立隊伍草稿的正文例子:

json
{
  "respondent": "demo-contact-ui",
  "division": "demo",
  "answers": { "team_name": "Synthetic team" },
  "members": [
    { "alias": "demo-person-a" },
    { "alias": "demo-person-b" }
  ]
}

division、team_name、alias 必須先在該競賽定義宣告,不是可以套在任意競賽的固定欄位。報名 UI 顯示隊伍 fields 和隊員 members.fields,作品 UI 顯示該階段 questions。入口及成員模型與普通 form 的 answers 不同。

保存某階段草稿的正文例子:

json
{
  "answers": { "work": "Synthetic proposal text." },
  "revision": 0
}

work 必須是該階段宣告的 question key。成功保存回傳 submission id 和 revision,再以返回 revision 正式提交。同一 entry 持續更新,網站後端每次核對使用者可管理這支隊伍。

競賽 API 在伺服器檢查報名狀態、開放時間、已提交的前置階段及版本。前置條件未完成時,連保存該階段草稿也會被拒絕。草稿放寬必填答案檢查,但隊伍草稿仍需符合設定隊員人數;提交時檢查完整必填資料。衝突用 entry_changed/submission_changed,而不是普通回覆的 expected_version;需重新讀取核對。私人競賽附件用該 entry 的 uploads API,file 答案保存 upl_ id,不能用普通 file_upload URL 格式。

現有版本同一 respondent 在同一競賽限一隊。可撤銷私人 link 是持有者的存取憑證;自助開始報名、驗證電郵、自動寄出/找回連結仍需另行實作。不要把產生私人連結當作已提供整套登入或自助報名入口。

9.交付前驗收

用測試金鑰驗證:GET 的每個 key 對應正確 UI;必填、數值 0、多選及兩位參加者的物件陣列能保存;錯誤指向正確項目;更新沿用 respondent 和版本;重試不增加重複回覆;陌生訪客不能選另一份回覆;金鑰沒有出現在瀏覽器或下載檔案中。

再檢查只有適當的非個人欄位進報表。資料分類、表單建立重試、回覆讀取限制及私人檔案完整規則,見API 參考及常見問題。每個網站仍需實作自己的身份核對、欄位對應及伺服器接入層;不用為它重新修改 OpenDashboard。

哪些情況需要把資料送回原站?

既有整合若仍由原站確認正式答案,維護方式見原站回寫。新流程即使外站需要評審,也應由 OpenDashboard 保存答案,再用流程資料同步提供副本;不必改用舊式回寫。以 OpenDashboard 為主時,兩個 UI 直接使用同一中央 API,通常不需要回寫;先看接入方式。公開入口只建立新隊伍,再以私人連結修改;既有資料不會按姓名或電郵被認領。需要配置原站回寫程序及通知,單純匯入或建立表單不會自動啟用。

公開入口及管理位置

兩份以上表單需要按步驟串聯時,使用多步流程。在流程管理選擇流程,再到「網站接入」取得公開新申請連結,或由可信後端建立同一 run 的私人連結。原站為正式資料來源時使用原站回寫入口。表單管理保留每張表的回覆查看,關聯表單可直接返回所屬流程。