前言

完成互動式 HTML 區塊後,KnockersTaskBlocks 的下一個需求是在 Kanboard 任務中建立跨職能流程圖。

這裡的「流程泳道」不是 Kanboard 看板原本用來水平分組任務的工作泳道,而是一張獨立圖表:每條流程泳道代表角色或部門,裡面包含流程節點,節點之間可以用有方向或無方向的關聯連接。

前端決定使用 React Flow,但真正困難的問題不是把節點畫出來,而是哪些資料應該長期保存。若直接把 React Flow 的 nodes、edges、viewport 與 selection 全部存進 Task Document,外掛就會把文件格式綁在某一版 UI library 上。

需求和 Kanboard 原生泳道不同

原生工作泳道服務的是看板分組;新的跨職能流程圖則是一份任務內容,必須支援:

  • 自訂流程泳道名稱、顏色與順序。
  • 在泳道內建立及拖曳流程節點。
  • 將節點移動到其他泳道。
  • 從節點拉線建立關聯。
  • 區分流程先後、必要依賴與一般關聯。
  • 編輯、刪除關聯與節點。
  • 自動排列、縮放、小地圖與全螢幕。
  • 編輯、摘要、唯讀與公開頁呈現。

這些功能和看板任務沒有一對一同步關係。流程節點不是 Kanboard Task,流程泳道也不是 Kanboard Swimlane。先把兩個領域分開,可以避免後續權限、刪除和同步語意混在一起。

不直接保存 React Flow 狀態

React Flow 需要 parent node、handle、edge route、viewport、selection、量測尺寸與 minimap 狀態,但這些大多只是目前 renderer 的工作資料。

最後保存的是外掛自己的 swimlane_diagram v1:

{
  "lanes": [
    {
      "id": "lane-product",
      "title": "產品",
      "color": "#F8D56B",
      "nodes": [
        {
          "id": "node-confirm",
          "title": "確認需求",
          "position": { "x": 20, "y": 72 }
        }
      ]
    }
  ],
  "relations": [
    {
      "id": "relation-confirm-design",
      "sourceNodeId": "node-confirm",
      "targetNodeId": "node-design",
      "type": "sequence"
    }
  ]
}

文件只保存:

  • 有順序的流程泳道。
  • 每條泳道內有順序的節點。
  • 節點相對於所屬泳道的座標。
  • 以穩定 ID 連接節點的關聯。

React Flow nodes 和 edges 每次顯示時由這份 Domain JSON 投影產生。未來即使更換 renderer,已保存的文件仍然只描述業務資料,不需要理解舊版 UI library 的內部狀態。

泳道只能當容器,不能拿來連線

流程泳道代表角色或責任範圍,本身不是流程步驟。因此只有 Flow Node 可以作為 relation endpoint。

關聯分成三種:

  • sequence:來源先於目標,有方向箭頭。
  • dependency:目標依賴來源,有方向箭頭。
  • association:一般關聯,語意上無方向。

Self relation、找不到端點的 dangling relation,以及相同語意的重複關聯都會被拒絕。循環則允許存在,因為真實流程可能回到前一步,但編輯器會提出警告,所有 traversal 也必須使用 visited set,避免圖形處理進入無限迴圈。

跨泳道移動不能換掉節點 ID

使用者把節點拖到另一條泳道時,不能刪掉舊節點再建立新節點,否則現有關聯會全部失效。

正確流程是:

  1. 保留 Flow Node ID。
  2. 將座標轉換成目標泳道的相對座標。
  3. 更新節點所屬容器與陣列位置。
  4. 保留所有仍然有效的 relation。

刪除節點則相反:所有指向它的 relation 必須在同一次 draft update 中移除,不能留下懸空端點。刪除非空泳道前也要顯示影響範圍,確認後一次刪除泳道、其中節點與相關 relation。

這些規則放在 Domain transformation,而不是散落於拖曳 callback 和 React component 中,才能由單元測試直接驗證。

自動排列必須可重現

第一版沒有引入大型 graph layout engine,而是建立確定性的 lane-stack-v1

  • 流程泳道依文件順序由左到右。
  • 每條泳道內的節點依陣列順序排列。
  • 圖遍歷使用 stable ID 與 visited set。
  • 相同輸入必須產生相同座標。

自動排列不會在每次載入或版本更新時偷偷執行。只有作者按下按鈕後,結果才會寫回正式座標。這保留手動調整,也避免新版演算法在使用者不知情時重排舊圖。

三種原型最後選擇三欄工作台

正式實作前先製作三種可拋棄原型:

A:畫布優先

畫布最大,設定面板浮在右上方。拖曳空間充足,但面板容易遮住右側泳道。

B:三欄工作台

左側顯示泳道與節點結構,中間是 React Flow 畫布,右側固定顯示目前選取項目的設定。資訊密度最高,但大型流程也比較容易定位。

C:文件式編輯

以泳道分頁切換,設定和 JSON 放在畫布下方。它最接近一般內容區塊,卻需要頻繁垂直捲動。

最後選擇 B 作為編輯模式。摘要、唯讀與公開頁不需要三欄工具,只保留乾淨畫布、縮放、符合畫面、小地圖與全螢幕。

原型只用來確認互動方向。正式版仍然重新接回 Task Document Runtime、Revision、validator 和共用 viewer,沒有把 prototype code 直接部署。

大圖不能只靠瀏覽器硬撐

流程圖加入後,效能風險不只來自 JavaScript 檔案大小,也來自節點、關聯與 React re-render。

這個專案為文件設定明確上限:

  • 最多 20 條流程泳道。
  • 最多 300 個流程節點。
  • 最多 600 條節點關聯。
  • 單一流程圖 JSON 最多 500,000 bytes。

接近上限前先顯示 warning,server hard limit 才是正式邊界。節點與關聯增加後會停用非必要動畫與昂貴樣式,並評估只 render 可見元素。

流程圖 bundle 也和一般任務頁分開。Task Document 沒有 swimlane_diagram 時,不應下載 React Flow assets;只有區塊接近 viewport 時才載入 viewer。這避免所有普通任務為了少數流程圖支付初始成本。

測試不只驗證畫面上有節點

這個區塊的測試包括:

  • Lane、Node、Relation ID 唯一性。
  • 跨泳道拖曳後座標和關聯保留。
  • 三種關聯的建立、編輯與刪除。
  • Self、duplicate 與 dangling relation 拒絕。
  • 循環關聯可保存,但 traversal 不會卡住。
  • 自動排列結果可重現。
  • 刪除節點和泳道時同步清除 relation。
  • 編輯、摘要、唯讀與公開模式的控制項差異。
  • 大型 fixture 的 mount、drag 與 connect 效能。
  • JavaScript 失效時仍能看到泳道與節點文字投影。

這次最大的收穫是:使用 React Flow 不代表資料模型也要長得像 React Flow。UI library 負責互動與繪圖,長期文件則應保存產品領域真正需要的資料。

下一篇會把 Task Document 開放給外部自動化:Kanboard AI Agent API 開發實錄:用 expected_revision 避免內容互相覆蓋