Skip to main content

FS Garage Kanban (FE) Architecture

System Overview​

The board groups a flat list of tasks into six fixed columns by status, renders them with drag-and-drop, and on every drop recomputes just two fields on the moved task - status and a fractional order_index - rather than renumbering an entire column.

Architecture Diagram​

graph TB
subgraph FE["Frontend: base-fs-garage-fe"]
StatusConfig["configs/tasks/general/status.js<br/>STATUS_CONFIG (6 columns)"]
KanbanBoard["KanbanBoard.jsx<br/>groupTasksByColumn + drag state"]
KanbanColumn["KanbanColumn.jsx"]
KanbanTaskCard["KanbanTaskCard.jsx"]
Store["store/index.js<br/>updateTask, updateTaskInStore"]
end
PocketBase["PocketBase: tasks collection"]

StatusConfig -->|"defines columns"| KanbanBoard
KanbanBoard --> KanbanColumn
KanbanColumn --> KanbanTaskCard
KanbanBoard -->|"on drop: status + order_index"| Store
Store -->|"PATCH"| PocketBase

How to read this diagram​

Arrows mean flow / dependency / "talks to", not button clicks.

Core Components​

Column configuration (configs/tasks/general/status.js)​

STATUS_CONFIG is the single source of truth for the board's six columns (To Do, In Progress, In Review, In Testing, In Acceptance, Done), each listing the raw task statuses it groups (e.g. "In Progress" covers In Progress, In Dev Testing, Needs Rework) plus a display color, icon, and statusOrder.

KanbanBoard.jsx​

Owns local optimistic task state, builds columns via groupTasksByColumn, and handles onDragEnd/onDragUpdate from @hello-pangea/dnd's DragDropContext.

KanbanColumn.jsx / KanbanTaskCard.jsx​

Render one column's task list and one task card respectively; presentation only, no calculation logic of their own.

Ordering & Column Calculations​

Grouping tasks into columns​

groupTasksByColumn(tasks) =
for each column in STATUS_CONFIG:
column.tasks = tasks.filter(task => column.statuses.includes(task.status))
.sort((a, b) => a.order_index - b.order_index)

A task's column is determined entirely by which STATUS_CONFIG group its raw status value belongs to - there's no separate "column" field on the task itself.

Sub-order within a shared column​

Several raw statuses can share one column (e.g. "In Review" = Ready for Review, In Review, Ready for Staging, In Staging). getStatusSubOrder derives a finer-grained order value so that detail isn't lost when they visually collapse into one column:

subOrder = column.statusOrder + (indexOfStatusWithinColumn + 1) / 10

Example: "In Review" has statusOrder = 3; its four statuses get 3.1, 3.2, 3.3, 3.4 in listed order.

Fractional order-index on drag-and-drop​

Dropping a card never renumbers the whole column - it computes one new value that sits between its new neighbors:

calculateOrderIndex(columnTasks, destinationIndex):
if destinationIndex == 0: neighbor.order_index - 0.5 (dropped at top)
if destinationIndex >= length: lastTask.order_index + 1 (dropped at bottom)
otherwise: (prevTask.order_index + nextTask.order_index) / 2 (dropped between two cards)

Dropping within the same column, the dragged task is excluded from columnTasks before this runs, to avoid an off-by-one against its own old position.

Column-to-status mapping on cross-column drop​

Moving a card to a different column rewrites its status to a fixed representative value for that column (not just "whatever status the column happens to display"):

ColumnStatus written on drop
TodoTo Do
In ProgressIn Progress
In ReviewReady for Review
In TestingReady for Testing
In AcceptanceReady for Acceptance
DoneReady for Production

Dropping within the same column never changes status - only order_index moves.

Optimistic update with rollback​

1. Compute {status, order_index} for the dropped task
2. Update local board state immediately (optimistic)
3. Update the in-memory store silently (no extra rerender)
4. PATCH the task via store/index.js's updateTask
5. If the PATCH fails or throws: revert local state AND the store to the
pre-drag {status, order_index} - the board never shows a position the
server didn't actually accept

Data Flow​

sequenceDiagram
participant User
participant Board as KanbanBoard.jsx
participant Store as store/index.js
participant PocketBase

User->>Board: Drag task card to a new column/position
Board->>Board: calculateOrderIndex(destinationTasks, destinationIndex)
Board->>Board: resolve status via COLUMN_STATUS_MAP (if cross-column)
Board-->>User: Optimistic re-render (immediate)
Board->>Store: updateTask(taskId, {status, order_index})
Store->>PocketBase: PATCH tasks/:id
alt save fails
PocketBase-->>Store: error
Store-->>Board: failure
Board->>Board: revert to pre-drag state
else save succeeds
PocketBase-->>Store: updated task
end

Design Decisions​

Fractional indexing instead of renumbering​

Context: Reordering within a column by renumbering every task's order_index would mean writing every affected row on each drag, not just the one that moved.

Decision: Compute a single midpoint value between the dropped task's new neighbors ((prev + next) / 2) - only the dragged task is written. This is the same fractional-indexing approach used for ordering in many drag-and-drop lists, trading unbounded float precision for far fewer writes.

A fixed column-to-status map, not "whatever the column shows"​

Context: A column groups several raw statuses (e.g. In Review covers four); dropping a card into that column needs to pick one concrete status to write.

Decision: COLUMN_STATUS_MAP pins one representative status per column, so cross-column drops are deterministic instead of guessing from whichever status happened to sort first.

Optimistic UI with an explicit rollback path​

Context: Waiting for the PocketBase round-trip before showing a moved card would make drag-and-drop feel laggy.

Decision: Update local state immediately, then reconcile: on failure, restore the exact pre-drag task list and store values rather than leaving an inconsistent, unsaved-looking board.

Technology Stack​

LayerTechnology
FrameworkNext.js App Router
LanguageJavaScript
Drag-and-drop@hello-pangea/dnd
BackendPocketBase tasks collection (shared, no Kanban-specific schema)
Default branchdev

Next Steps​