KanbanDataSource (Strategy)
Panoramica
KanbanDataSource è l'interfaccia Strategy che disaccoppia composables e componenti dal backend effettivo. Tutti i composable (useBoardMetadata, useBoardElementi, useBoardService) delegano l'I/O a un'implementazione di questa interfaccia.
useBoardService / *Connected components
│
▼
KanbanDataSource ◄──────── interfaccia Strategy
▲
│
┌─────┴───────────────┬──────────────┐
│ │ │
PostgRESTDataSource GraphQL DS mock DS
(default) (custom) (test)
Interfaccia
interface KanbanDataSource<
TBoard = unknown,
TColumn = unknown,
TItem = unknown,
TPriority = unknown,
TTag = unknown,
TField = unknown,
> {
/** Carica metadata della board (per ID numerico o codice). null = non trovata. */
loadBoard(boardIdOrCode: BoardId): Promise<TBoard | null>
/** Carica colonne/stati della board, già ordinate. */
loadColumns(boardId: BoardId): Promise<TColumn[]>
/** Carica tutte le schede attive (filtro soft-delete a cura dell'implementazione). */
loadItems(boardId: BoardId): Promise<TItem[]>
/** Carica una singola scheda. undefined = non trovata. */
loadItem(itemId: ItemId): Promise<TItem | undefined>
/** Crea una nuova scheda nella board indicata. */
createItem(boardId: BoardId, data: Partial<TItem>): Promise<TItem | null>
/** Aggiorna una scheda esistente. */
updateItem(itemId: ItemId, data: Partial<TItem>): Promise<TItem | null>
/** Elimina (soft o hard, a discrezione del backend) una scheda. */
deleteItem(itemId: ItemId): Promise<boolean>
/** Sposta una scheda in una colonna diversa con un nuovo ordine. */
moveItem(itemId: ItemId, targetCol: KanbanColumnId, newOrder: number): Promise<MoveItemResult>
// ── Opzionali ────────────────────────────────────────────────────────────
loadPriorities?(boardId: BoardId): Promise<TPriority[]>
loadTags?(boardId: BoardId): Promise<TTag[]>
loadFields?(boardId: BoardId): Promise<TField[]>
updateItemTags?(itemId: ItemId, tagIds: TagId[]): Promise<boolean>
}
interface MoveItemResult {
success: boolean
/** Se valorizzato, indica colonna finale: la scheda è stata chiusa. */
closedAt?: string | null
/** Notifiche accodate come effetto del move (es. email/SMS). */
notificationsQueued?: number
[key: string]: unknown
}
I metodi opzionali (loadPriorities, loadTags, loadFields, updateItemTags) permettono a backend minimali di non implementare funzionalità non utilizzate. I composable verificano if (dataSource.loadTags) prima di chiamarli.
PostgRESTDataSource (default)
L'implementazione di default. Preserva il comportamento storico dello schema fasi. Tutte le scelte sono override-abili via PostgRESTDataSourceOptions.
import { PostgRESTDataSource, usePostgRESTClient } from '@pzeta/vue-kanban'
const dataSource = new PostgRESTDataSource({
client: usePostgRESTClient(),
schema: 'fasi', // default
endpoints: {
board: '/board',
columns: '/fase',
items: '/scheda',
itemsView: '/v_schede_kanban', // null per usare items direttamente
priorities: '/priorita',
tags: '/tag',
fields: '/boardcampo',
itemTags: '/schedatag',
moveRpc: '/rpc/sposta_scheda_fase', // null per PATCH diretto su items
},
primaryKeys: {
board: 'idboard',
column: 'idfase',
item: 'idscheda',
priority: 'idpriorita',
tag: 'idtag',
field: 'idboardcampo',
itemTag: 'idschedatag',
},
boardCodeField: 'codboard', // campo per lookup string ID
getCurrentUserId: () => 0, // provider userId per RPC
softDeleteField: 'dataeliminazione', // null per disabilitare
})
Override per backend con schema diverso
const dataSource = new PostgRESTDataSource({
client: usePostgRESTClient(),
schema: 'public',
endpoints: {
board: '/projects',
columns: '/project_columns',
items: '/project_cards',
itemsView: null, // niente vista, usa /project_cards diretto
moveRpc: null, // niente RPC, fa PATCH diretto
priorities: '/priorities',
tags: '/labels',
fields: '/custom_fields',
itemTags: '/card_labels',
},
primaryKeys: {
board: 'id', column: 'id', item: 'id',
priority: 'id', tag: 'id', field: 'id', itemTag: 'id',
},
boardCodeField: 'slug',
softDeleteField: null,
})
Implementazione custom
Per backend non-PostgREST (REST custom, GraphQL, tRPC, mock), implementa direttamente l'interfaccia:
import type { KanbanDataSource, MoveItemResult } from '@pzeta/vue-kanban'
class MyApiDataSource implements KanbanDataSource<MyBoard, MyColumn, MyCard> {
async loadBoard(id: string | number) {
const res = await fetch(`/api/boards/${id}`)
if (!res.ok) return null
return res.json()
}
async loadColumns(boardId: string | number) {
return fetch(`/api/boards/${boardId}/columns`).then(r => r.json())
}
async loadItems(boardId: string | number) {
return fetch(`/api/boards/${boardId}/cards`).then(r => r.json())
}
async loadItem(itemId: string | number) {
const res = await fetch(`/api/cards/${itemId}`)
return res.ok ? res.json() : undefined
}
async createItem(boardId: string | number, data: Partial<MyCard>) {
const res = await fetch(`/api/boards/${boardId}/cards`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
return res.json()
}
async updateItem(itemId: string | number, data: Partial<MyCard>) {
const res = await fetch(`/api/cards/${itemId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
return res.json()
}
async deleteItem(itemId: string | number) {
const res = await fetch(`/api/cards/${itemId}`, { method: 'DELETE' })
return res.ok
}
async moveItem(itemId, targetCol, newOrder): Promise<MoveItemResult> {
const res = await fetch(`/api/cards/${itemId}/move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ columnId: targetCol, order: newOrder }),
})
if (!res.ok) return { success: false }
return { success: true, ...(await res.json()) }
}
// Opzionali: implementare solo se serve
async loadTags(boardId) { /* ... */ }
async updateItemTags(itemId, tagIds) { /* ... */ }
}
const myDS = new MyApiDataSource()
Iniezione
Via composable
import { useBoardService } from '@pzeta/vue-kanban'
const service = useBoardService('PROJECT-X', { dataSource: myDS })
Via componente smart (EnhancedKanbanBoard)
EnhancedKanbanBoard istanzia useBoardService internamente. Per iniettare un datasource custom, wrappa il composable in un componente padre e passa i dati a KanbanBoard direttamente, oppure usa il pattern di slot scope.
In alternativa, registra un PostgRESTDataSource con endpoint custom in modo da preservare il flusso del componente smart.
Mock per test
import { describe, it, expect, vi } from 'vitest'
import { useBoardService, type KanbanDataSource } from '@pzeta/vue-kanban'
const mockDataSource: KanbanDataSource = {
loadBoard: vi.fn().mockResolvedValue({ idboard: 1, codboard: 'TEST' }),
loadColumns: vi.fn().mockResolvedValue([]),
loadItems: vi.fn().mockResolvedValue([]),
loadItem: vi.fn(),
createItem: vi.fn(),
updateItem: vi.fn(),
deleteItem: vi.fn(),
moveItem: vi.fn().mockResolvedValue({ success: true }),
}
it('carica i dati dal datasource', async () => {
const service = useBoardService('TEST', { dataSource: mockDataSource })
await service.loadBoardData()
expect(mockDataSource.loadBoard).toHaveBeenCalledWith('TEST')
})
TypeScript
Tutti i tipi rilevanti sono esportati dal package root:
import type {
KanbanDataSource,
MoveItemResult,
BoardId,
TagId,
FieldId,
ItemId,
KanbanColumnId,
} from '@pzeta/vue-kanban'
import {
PostgRESTDataSource,
type PostgRESTDataSourceOptions,
type PostgRESTEndpoints,
type PostgRESTPrimaryKeys,
} from '@pzeta/vue-kanban'