kanban-lite 1.0.9 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1015 -407
- package/dist/extension.js +897 -347
- package/dist/mcp-server.js +550 -189
- package/dist/sdk/index.cjs +1223 -0
- package/dist/sdk/index.mjs +1168 -0
- package/dist/sdk/sdk/KanbanSDK.d.ts +39 -21
- package/dist/sdk/sdk/index.d.ts +4 -3
- package/dist/sdk/sdk/migration.d.ts +6 -0
- package/dist/sdk/sdk/types.d.ts +3 -5
- package/dist/sdk/shared/config.d.ts +23 -10
- package/dist/sdk/shared/types.d.ts +18 -4
- package/dist/standalone-webview/index.js +27 -27
- package/dist/standalone-webview/index.js.map +1 -1
- package/dist/standalone-webview/style.css +1 -1
- package/dist/standalone.js +831 -328
- package/dist/webview/index.js +45 -45
- package/dist/webview/index.js.map +1 -1
- package/dist/webview/style.css +1 -1
- package/package.json +4 -3
- package/src/cli/index.ts +217 -95
- package/src/extension/KanbanPanel.ts +49 -22
- package/src/mcp-server/index.ts +138 -62
- package/src/sdk/KanbanSDK.ts +283 -77
- package/src/sdk/__tests__/KanbanSDK.test.ts +5 -5
- package/src/sdk/__tests__/migration.test.ts +269 -0
- package/src/sdk/__tests__/multi-board.test.ts +449 -0
- package/src/sdk/index.ts +4 -3
- package/src/sdk/migration.ts +52 -0
- package/src/sdk/types.ts +3 -6
- package/src/shared/config.ts +144 -22
- package/src/shared/types.ts +14 -5
- package/src/standalone/__tests__/server.integration.test.ts +38 -37
- package/src/standalone/server.ts +239 -21
- package/src/webview/App.tsx +17 -7
- package/src/webview/components/Toolbar.tsx +99 -3
- package/src/webview/store/index.ts +11 -3
- package/.kanban/backlog/1-test1.md +0 -30
- package/.kanban.json +0 -42
package/src/standalone/server.ts
CHANGED
|
@@ -3,13 +3,14 @@ import * as fs from 'fs'
|
|
|
3
3
|
import * as path from 'path'
|
|
4
4
|
import { WebSocketServer, WebSocket } from 'ws'
|
|
5
5
|
import chokidar from 'chokidar'
|
|
6
|
-
import type
|
|
6
|
+
import { generateSlug, type Comment, type Feature, type Priority, type KanbanColumn, type FeatureFrontmatter, type CardDisplaySettings } from '../shared/types'
|
|
7
7
|
import { KanbanSDK } from '../sdk/KanbanSDK'
|
|
8
8
|
import { serializeFeature } from '../sdk/parser'
|
|
9
|
+
import { readConfig } from '../shared/config'
|
|
9
10
|
import { fireWebhooks, loadWebhooks, createWebhook, deleteWebhook } from './webhooks'
|
|
10
11
|
|
|
11
12
|
interface CreateFeatureData {
|
|
12
|
-
status:
|
|
13
|
+
status: string
|
|
13
14
|
priority: Priority
|
|
14
15
|
content: string
|
|
15
16
|
assignee: string | null
|
|
@@ -35,6 +36,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
35
36
|
let migrating = false
|
|
36
37
|
let currentEditingFeatureId: string | null = null
|
|
37
38
|
let lastWrittenContent = ''
|
|
39
|
+
let currentBoardId: string | undefined
|
|
38
40
|
|
|
39
41
|
// Resolve webview static files directory
|
|
40
42
|
const resolvedWebviewDir = webviewDir || path.join(__dirname, 'standalone-webview')
|
|
@@ -121,20 +123,23 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
121
123
|
// --- Feature loading ---
|
|
122
124
|
|
|
123
125
|
async function loadFeatures(): Promise<void> {
|
|
124
|
-
features = await sdk.listCards(sdk.listColumns().map(c => c.id))
|
|
126
|
+
features = await sdk.listCards(sdk.listColumns(currentBoardId).map(c => c.id), currentBoardId)
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
// --- Message building & broadcast ---
|
|
128
130
|
|
|
129
131
|
function buildInitMessage(): unknown {
|
|
132
|
+
const config = readConfig(workspaceRoot)
|
|
130
133
|
const settings = sdk.getSettings()
|
|
131
134
|
settings.showBuildWithAI = false
|
|
132
135
|
settings.markdownEditorMode = false
|
|
133
136
|
return {
|
|
134
137
|
type: 'init',
|
|
135
138
|
features,
|
|
136
|
-
columns: sdk.listColumns(),
|
|
137
|
-
settings
|
|
139
|
+
columns: sdk.listColumns(currentBoardId),
|
|
140
|
+
settings,
|
|
141
|
+
boards: sdk.listBoards(),
|
|
142
|
+
currentBoard: currentBoardId || config.defaultBoard
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
|
|
@@ -160,6 +165,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
160
165
|
assignee: data.assignee,
|
|
161
166
|
dueDate: data.dueDate,
|
|
162
167
|
labels: data.labels,
|
|
168
|
+
boardId: currentBoardId,
|
|
163
169
|
})
|
|
164
170
|
await loadFeatures()
|
|
165
171
|
broadcast(buildInitMessage())
|
|
@@ -177,7 +183,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
177
183
|
|
|
178
184
|
migrating = true
|
|
179
185
|
try {
|
|
180
|
-
const updated = await sdk.moveCard(featureId, newStatus
|
|
186
|
+
const updated = await sdk.moveCard(featureId, newStatus, newOrder, currentBoardId)
|
|
181
187
|
await loadFeatures()
|
|
182
188
|
broadcast(buildInitMessage())
|
|
183
189
|
fireWebhooks(workspaceRoot, 'task.moved', {
|
|
@@ -196,7 +202,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
196
202
|
|
|
197
203
|
migrating = true
|
|
198
204
|
try {
|
|
199
|
-
const updated = await sdk.updateCard(featureId, updates)
|
|
205
|
+
const updated = await sdk.updateCard(featureId, updates, currentBoardId)
|
|
200
206
|
lastWrittenContent = serializeFeature(updated)
|
|
201
207
|
await loadFeatures()
|
|
202
208
|
broadcast(buildInitMessage())
|
|
@@ -213,7 +219,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
213
219
|
|
|
214
220
|
try {
|
|
215
221
|
const deleted = sanitizeFeature(feature)
|
|
216
|
-
await sdk.deleteCard(featureId)
|
|
222
|
+
await sdk.deleteCard(featureId, currentBoardId)
|
|
217
223
|
await loadFeatures()
|
|
218
224
|
broadcast(buildInitMessage())
|
|
219
225
|
fireWebhooks(workspaceRoot, 'task.deleted', deleted)
|
|
@@ -225,7 +231,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
function doAddColumn(name: string, color: string): KanbanColumn {
|
|
228
|
-
const existingColumns = sdk.listColumns()
|
|
234
|
+
const existingColumns = sdk.listColumns(currentBoardId)
|
|
229
235
|
const id = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
230
236
|
let uniqueId = id
|
|
231
237
|
let counter = 1
|
|
@@ -233,7 +239,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
233
239
|
uniqueId = `${id}-${counter++}`
|
|
234
240
|
}
|
|
235
241
|
const column: KanbanColumn = { id: uniqueId, name, color }
|
|
236
|
-
sdk.addColumn(column)
|
|
242
|
+
sdk.addColumn(column, currentBoardId)
|
|
237
243
|
broadcast(buildInitMessage())
|
|
238
244
|
fireWebhooks(workspaceRoot, 'column.created', column)
|
|
239
245
|
return column
|
|
@@ -241,7 +247,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
241
247
|
|
|
242
248
|
function doEditColumn(columnId: string, updates: { name: string; color: string }): KanbanColumn | null {
|
|
243
249
|
try {
|
|
244
|
-
const columns = sdk.updateColumn(columnId, { name: updates.name, color: updates.color })
|
|
250
|
+
const columns = sdk.updateColumn(columnId, { name: updates.name, color: updates.color }, currentBoardId)
|
|
245
251
|
const updated = columns.find(c => c.id === columnId) ?? null
|
|
246
252
|
broadcast(buildInitMessage())
|
|
247
253
|
if (updated) fireWebhooks(workspaceRoot, 'column.updated', updated)
|
|
@@ -253,11 +259,11 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
253
259
|
|
|
254
260
|
async function doRemoveColumn(columnId: string): Promise<{ removed: boolean; error?: string }> {
|
|
255
261
|
try {
|
|
256
|
-
const columns = sdk.listColumns()
|
|
262
|
+
const columns = sdk.listColumns(currentBoardId)
|
|
257
263
|
if (columns.length <= 1) return { removed: false, error: 'Cannot remove last column' }
|
|
258
264
|
const col = columns.find(c => c.id === columnId)
|
|
259
265
|
if (!col) return { removed: false, error: 'Column not found' }
|
|
260
|
-
await sdk.removeColumn(columnId)
|
|
266
|
+
await sdk.removeColumn(columnId, currentBoardId)
|
|
261
267
|
broadcast(buildInitMessage())
|
|
262
268
|
fireWebhooks(workspaceRoot, 'column.deleted', col)
|
|
263
269
|
return { removed: true }
|
|
@@ -282,7 +288,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
282
288
|
// Register attachment via SDK (skips copy since file is already in place)
|
|
283
289
|
migrating = true
|
|
284
290
|
try {
|
|
285
|
-
const updated = await sdk.addAttachment(featureId, path.join(featureDir, filename))
|
|
291
|
+
const updated = await sdk.addAttachment(featureId, path.join(featureDir, filename), currentBoardId)
|
|
286
292
|
lastWrittenContent = serializeFeature(updated)
|
|
287
293
|
await loadFeatures()
|
|
288
294
|
} finally {
|
|
@@ -297,7 +303,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
297
303
|
|
|
298
304
|
migrating = true
|
|
299
305
|
try {
|
|
300
|
-
const updated = await sdk.removeAttachment(featureId, attachment)
|
|
306
|
+
const updated = await sdk.removeAttachment(featureId, attachment, currentBoardId)
|
|
301
307
|
lastWrittenContent = serializeFeature(updated)
|
|
302
308
|
await loadFeatures()
|
|
303
309
|
broadcast(buildInitMessage())
|
|
@@ -312,7 +318,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
312
318
|
async function doAddComment(featureId: string, author: string, content: string): Promise<Comment | null> {
|
|
313
319
|
migrating = true
|
|
314
320
|
try {
|
|
315
|
-
const updated = await sdk.addComment(featureId, author, content)
|
|
321
|
+
const updated = await sdk.addComment(featureId, author, content, currentBoardId)
|
|
316
322
|
lastWrittenContent = serializeFeature(updated)
|
|
317
323
|
const comment = updated.comments[updated.comments.length - 1]
|
|
318
324
|
await loadFeatures()
|
|
@@ -329,7 +335,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
329
335
|
async function doUpdateComment(featureId: string, commentId: string, content: string): Promise<Comment | null> {
|
|
330
336
|
migrating = true
|
|
331
337
|
try {
|
|
332
|
-
const updated = await sdk.updateComment(featureId, commentId, content)
|
|
338
|
+
const updated = await sdk.updateComment(featureId, commentId, content, currentBoardId)
|
|
333
339
|
lastWrittenContent = serializeFeature(updated)
|
|
334
340
|
const comment = (updated.comments || []).find(c => c.id === commentId)
|
|
335
341
|
await loadFeatures()
|
|
@@ -351,7 +357,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
351
357
|
|
|
352
358
|
migrating = true
|
|
353
359
|
try {
|
|
354
|
-
const updated = await sdk.deleteComment(featureId, commentId)
|
|
360
|
+
const updated = await sdk.deleteComment(featureId, commentId, currentBoardId)
|
|
355
361
|
lastWrittenContent = serializeFeature(updated)
|
|
356
362
|
await loadFeatures()
|
|
357
363
|
broadcast(buildInitMessage())
|
|
@@ -519,6 +525,36 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
519
525
|
break
|
|
520
526
|
}
|
|
521
527
|
|
|
528
|
+
case 'switchBoard':
|
|
529
|
+
currentBoardId = msg.boardId as string
|
|
530
|
+
migrating = true
|
|
531
|
+
try {
|
|
532
|
+
await loadFeatures()
|
|
533
|
+
broadcast(buildInitMessage())
|
|
534
|
+
} finally {
|
|
535
|
+
migrating = false
|
|
536
|
+
}
|
|
537
|
+
break
|
|
538
|
+
|
|
539
|
+
case 'createBoard': {
|
|
540
|
+
const boardName = msg.name as string
|
|
541
|
+
const boardId = generateSlug(boardName) || 'board'
|
|
542
|
+
try {
|
|
543
|
+
sdk.createBoard(boardId, boardName)
|
|
544
|
+
currentBoardId = boardId
|
|
545
|
+
migrating = true
|
|
546
|
+
try {
|
|
547
|
+
await loadFeatures()
|
|
548
|
+
broadcast(buildInitMessage())
|
|
549
|
+
} finally {
|
|
550
|
+
migrating = false
|
|
551
|
+
}
|
|
552
|
+
} catch (err) {
|
|
553
|
+
console.error('Failed to create board:', err)
|
|
554
|
+
}
|
|
555
|
+
break
|
|
556
|
+
}
|
|
557
|
+
|
|
522
558
|
// VSCode-specific actions — no-ops in standalone
|
|
523
559
|
case 'openFile':
|
|
524
560
|
case 'focusMenuBar':
|
|
@@ -552,9 +588,191 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
552
588
|
const route = (expectedMethod: string, pattern: string): Record<string, string> | null =>
|
|
553
589
|
matchRoute(expectedMethod, method, pathname, pattern)
|
|
554
590
|
|
|
591
|
+
// ==================== BOARDS API ====================
|
|
592
|
+
|
|
593
|
+
let params = route('GET', '/api/boards')
|
|
594
|
+
if (params) {
|
|
595
|
+
return jsonOk(res, sdk.listBoards())
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
params = route('POST', '/api/boards')
|
|
599
|
+
if (params) {
|
|
600
|
+
try {
|
|
601
|
+
const body = await readBody(req)
|
|
602
|
+
const id = body.id as string
|
|
603
|
+
const name = body.name as string
|
|
604
|
+
if (!id) return jsonError(res, 400, 'id is required')
|
|
605
|
+
if (!name) return jsonError(res, 400, 'name is required')
|
|
606
|
+
const board = sdk.createBoard(id, name, {
|
|
607
|
+
description: body.description as string | undefined,
|
|
608
|
+
columns: body.columns as KanbanColumn[] | undefined,
|
|
609
|
+
})
|
|
610
|
+
return jsonOk(res, board, 201)
|
|
611
|
+
} catch (err) {
|
|
612
|
+
return jsonError(res, 400, String(err))
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
params = route('GET', '/api/boards/:boardId')
|
|
617
|
+
if (params) {
|
|
618
|
+
try {
|
|
619
|
+
const { boardId } = params
|
|
620
|
+
const board = sdk.getBoard(boardId)
|
|
621
|
+
return jsonOk(res, board)
|
|
622
|
+
} catch (err) {
|
|
623
|
+
return jsonError(res, 404, String(err))
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
params = route('PUT', '/api/boards/:boardId')
|
|
628
|
+
if (params) {
|
|
629
|
+
try {
|
|
630
|
+
const { boardId } = params
|
|
631
|
+
const body = await readBody(req)
|
|
632
|
+
const board = sdk.updateBoard(boardId, body as Record<string, unknown>)
|
|
633
|
+
return jsonOk(res, board)
|
|
634
|
+
} catch (err) {
|
|
635
|
+
return jsonError(res, 400, String(err))
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
params = route('DELETE', '/api/boards/:boardId')
|
|
640
|
+
if (params) {
|
|
641
|
+
try {
|
|
642
|
+
const { boardId } = params
|
|
643
|
+
await sdk.deleteBoard(boardId)
|
|
644
|
+
return jsonOk(res, { deleted: true })
|
|
645
|
+
} catch (err) {
|
|
646
|
+
return jsonError(res, 400, String(err))
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Transfer a task between boards
|
|
651
|
+
params = route('POST', '/api/boards/:boardId/tasks/:id/transfer')
|
|
652
|
+
if (params) {
|
|
653
|
+
try {
|
|
654
|
+
const { boardId, id } = params
|
|
655
|
+
const body = await readBody(req)
|
|
656
|
+
const config = readConfig(workspaceRoot)
|
|
657
|
+
const fromBoard = currentBoardId || config.defaultBoard
|
|
658
|
+
const targetStatus = body.targetStatus as string | undefined
|
|
659
|
+
const card = await sdk.transferCard(id, fromBoard, boardId, targetStatus)
|
|
660
|
+
return jsonOk(res, sanitizeFeature(card))
|
|
661
|
+
} catch (err) {
|
|
662
|
+
return jsonError(res, 400, String(err))
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ==================== BOARD-SCOPED TASKS API ====================
|
|
667
|
+
|
|
668
|
+
params = route('GET', '/api/boards/:boardId/tasks')
|
|
669
|
+
if (params) {
|
|
670
|
+
try {
|
|
671
|
+
const { boardId } = params
|
|
672
|
+
const boardColumns = sdk.listColumns(boardId)
|
|
673
|
+
const boardTasks = await sdk.listCards(boardColumns.map(c => c.id), boardId)
|
|
674
|
+
let result = boardTasks.map(sanitizeFeature)
|
|
675
|
+
const status = url.searchParams.get('status')
|
|
676
|
+
if (status) result = result.filter(f => f.status === status)
|
|
677
|
+
const priority = url.searchParams.get('priority')
|
|
678
|
+
if (priority) result = result.filter(f => f.priority === priority)
|
|
679
|
+
const assignee = url.searchParams.get('assignee')
|
|
680
|
+
if (assignee) result = result.filter(f => f.assignee === assignee)
|
|
681
|
+
const label = url.searchParams.get('label')
|
|
682
|
+
if (label) result = result.filter(f => f.labels.includes(label))
|
|
683
|
+
return jsonOk(res, result)
|
|
684
|
+
} catch (err) {
|
|
685
|
+
return jsonError(res, 400, String(err))
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
params = route('POST', '/api/boards/:boardId/tasks')
|
|
690
|
+
if (params) {
|
|
691
|
+
try {
|
|
692
|
+
const { boardId } = params
|
|
693
|
+
const body = await readBody(req)
|
|
694
|
+
const content = (body.content as string) || ''
|
|
695
|
+
if (!content) return jsonError(res, 400, 'content is required')
|
|
696
|
+
const feature = await sdk.createCard({
|
|
697
|
+
content,
|
|
698
|
+
status: (body.status as string) || 'backlog',
|
|
699
|
+
priority: (body.priority as Priority) || 'medium',
|
|
700
|
+
assignee: (body.assignee as string) || null,
|
|
701
|
+
dueDate: (body.dueDate as string) || null,
|
|
702
|
+
labels: (body.labels as string[]) || [],
|
|
703
|
+
boardId,
|
|
704
|
+
})
|
|
705
|
+
return jsonOk(res, sanitizeFeature(feature), 201)
|
|
706
|
+
} catch (err) {
|
|
707
|
+
return jsonError(res, 400, String(err))
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
params = route('GET', '/api/boards/:boardId/tasks/:id')
|
|
712
|
+
if (params) {
|
|
713
|
+
try {
|
|
714
|
+
const { boardId, id } = params
|
|
715
|
+
const card = await sdk.getCard(id, boardId)
|
|
716
|
+
if (!card) return jsonError(res, 404, 'Task not found')
|
|
717
|
+
return jsonOk(res, sanitizeFeature(card))
|
|
718
|
+
} catch (err) {
|
|
719
|
+
return jsonError(res, 400, String(err))
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
params = route('PUT', '/api/boards/:boardId/tasks/:id')
|
|
724
|
+
if (params) {
|
|
725
|
+
try {
|
|
726
|
+
const { boardId, id } = params
|
|
727
|
+
const body = await readBody(req)
|
|
728
|
+
const feature = await sdk.updateCard(id, body as Partial<Feature>, boardId)
|
|
729
|
+
return jsonOk(res, sanitizeFeature(feature))
|
|
730
|
+
} catch (err) {
|
|
731
|
+
return jsonError(res, 400, String(err))
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
params = route('PATCH', '/api/boards/:boardId/tasks/:id/move')
|
|
736
|
+
if (params) {
|
|
737
|
+
try {
|
|
738
|
+
const { boardId, id } = params
|
|
739
|
+
const body = await readBody(req)
|
|
740
|
+
const newStatus = body.status as string
|
|
741
|
+
const position = body.position as number ?? 0
|
|
742
|
+
if (!newStatus) return jsonError(res, 400, 'status is required')
|
|
743
|
+
const feature = await sdk.moveCard(id, newStatus, position, boardId)
|
|
744
|
+
return jsonOk(res, sanitizeFeature(feature))
|
|
745
|
+
} catch (err) {
|
|
746
|
+
return jsonError(res, 400, String(err))
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
params = route('DELETE', '/api/boards/:boardId/tasks/:id')
|
|
751
|
+
if (params) {
|
|
752
|
+
try {
|
|
753
|
+
const { boardId, id } = params
|
|
754
|
+
await sdk.deleteCard(id, boardId)
|
|
755
|
+
return jsonOk(res, { deleted: true })
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return jsonError(res, 400, String(err))
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ==================== BOARD-SCOPED COLUMNS API ====================
|
|
762
|
+
|
|
763
|
+
params = route('GET', '/api/boards/:boardId/columns')
|
|
764
|
+
if (params) {
|
|
765
|
+
try {
|
|
766
|
+
const { boardId } = params
|
|
767
|
+
return jsonOk(res, sdk.listColumns(boardId))
|
|
768
|
+
} catch (err) {
|
|
769
|
+
return jsonError(res, 400, String(err))
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
555
773
|
// ==================== TASKS API ====================
|
|
556
774
|
|
|
557
|
-
|
|
775
|
+
params = route('GET', '/api/tasks')
|
|
558
776
|
if (params) {
|
|
559
777
|
await loadFeatures()
|
|
560
778
|
let result = features.map(sanitizeFeature)
|
|
@@ -575,7 +793,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
575
793
|
const body = await readBody(req)
|
|
576
794
|
const data: CreateFeatureData = {
|
|
577
795
|
content: (body.content as string) || '',
|
|
578
|
-
status: (body.status as
|
|
796
|
+
status: (body.status as string) || 'backlog',
|
|
579
797
|
priority: (body.priority as Priority) || 'medium',
|
|
580
798
|
assignee: (body.assignee as string) || null,
|
|
581
799
|
dueDate: (body.dueDate as string) || null,
|
|
@@ -744,7 +962,7 @@ export function startServer(featuresDir: string, port: number, webviewDir?: stri
|
|
|
744
962
|
|
|
745
963
|
params = route('GET', '/api/columns')
|
|
746
964
|
if (params) {
|
|
747
|
-
return jsonOk(res, sdk.listColumns())
|
|
965
|
+
return jsonOk(res, sdk.listColumns(currentBoardId))
|
|
748
966
|
}
|
|
749
967
|
|
|
750
968
|
params = route('POST', '/api/columns')
|
package/src/webview/App.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { Toolbar } from './components/Toolbar'
|
|
|
8
8
|
import { UndoToast } from './components/UndoToast'
|
|
9
9
|
import { SettingsPanel } from './components/SettingsPanel'
|
|
10
10
|
import { ColumnDialog } from './components/ColumnDialog'
|
|
11
|
-
import type { Comment, Feature,
|
|
11
|
+
import type { Comment, Feature, KanbanColumn, Priority, ExtensionMessage, FeatureFrontmatter, CardDisplaySettings } from '../shared/types'
|
|
12
12
|
import { getTitleFromContent } from '../shared/types'
|
|
13
13
|
|
|
14
14
|
// Declare vscode API type
|
|
@@ -28,13 +28,15 @@ function App(): React.JSX.Element {
|
|
|
28
28
|
settingsOpen,
|
|
29
29
|
setFeatures,
|
|
30
30
|
setColumns,
|
|
31
|
+
setBoards,
|
|
32
|
+
setCurrentBoard,
|
|
31
33
|
setIsDarkMode,
|
|
32
34
|
setCardSettings,
|
|
33
35
|
setSettingsOpen
|
|
34
36
|
} = useStore()
|
|
35
37
|
|
|
36
38
|
const [createFeatureOpen, setCreateFeatureOpen] = useState(false)
|
|
37
|
-
const [createFeatureStatus, setCreateFeatureStatus] = useState<
|
|
39
|
+
const [createFeatureStatus, setCreateFeatureStatus] = useState<string>('backlog')
|
|
38
40
|
|
|
39
41
|
// Column dialog state
|
|
40
42
|
const [columnDialogOpen, setColumnDialogOpen] = useState(false)
|
|
@@ -206,6 +208,8 @@ function App(): React.JSX.Element {
|
|
|
206
208
|
case 'init':
|
|
207
209
|
setFeatures(message.features)
|
|
208
210
|
setColumns(message.columns)
|
|
211
|
+
if (message.boards) setBoards(message.boards)
|
|
212
|
+
if (message.currentBoard) setCurrentBoard(message.currentBoard)
|
|
209
213
|
if (message.settings) {
|
|
210
214
|
if (message.settings.markdownEditorMode && editingFeature) {
|
|
211
215
|
setEditingFeature(null)
|
|
@@ -246,7 +250,7 @@ function App(): React.JSX.Element {
|
|
|
246
250
|
vscode.postMessage({ type: 'ready' })
|
|
247
251
|
|
|
248
252
|
return () => window.removeEventListener('message', handleMessage)
|
|
249
|
-
}, [setFeatures, setColumns, setCardSettings, setSettingsOpen])
|
|
253
|
+
}, [setFeatures, setColumns, setBoards, setCurrentBoard, setCardSettings, setSettingsOpen])
|
|
250
254
|
|
|
251
255
|
const handleFeatureClick = (feature: Feature): void => {
|
|
252
256
|
// Request feature content for inline editing
|
|
@@ -354,12 +358,12 @@ function App(): React.JSX.Element {
|
|
|
354
358
|
}
|
|
355
359
|
|
|
356
360
|
const handleAddFeatureInColumn = (status: string): void => {
|
|
357
|
-
setCreateFeatureStatus(status
|
|
361
|
+
setCreateFeatureStatus(status)
|
|
358
362
|
setCreateFeatureOpen(true)
|
|
359
363
|
}
|
|
360
364
|
|
|
361
365
|
const handleCreateFeature = (data: {
|
|
362
|
-
status:
|
|
366
|
+
status: string
|
|
363
367
|
priority: Priority
|
|
364
368
|
content: string
|
|
365
369
|
}): void => {
|
|
@@ -390,7 +394,7 @@ function App(): React.JSX.Element {
|
|
|
390
394
|
|
|
391
395
|
const updated = features.map(f =>
|
|
392
396
|
f.id === featureId
|
|
393
|
-
? { ...f, status: newStatus
|
|
397
|
+
? { ...f, status: newStatus, order: newOrderKey }
|
|
394
398
|
: f
|
|
395
399
|
)
|
|
396
400
|
setFeatures(updated)
|
|
@@ -416,7 +420,13 @@ function App(): React.JSX.Element {
|
|
|
416
420
|
|
|
417
421
|
return (
|
|
418
422
|
<div className="h-full w-full flex flex-col bg-[var(--vscode-editor-background)]">
|
|
419
|
-
<Toolbar
|
|
423
|
+
<Toolbar
|
|
424
|
+
onOpenSettings={() => vscode.postMessage({ type: 'openSettings' })}
|
|
425
|
+
onAddColumn={handleAddColumn}
|
|
426
|
+
onToggleTheme={() => vscode.postMessage({ type: 'toggleTheme' })}
|
|
427
|
+
onSwitchBoard={(boardId) => vscode.postMessage({ type: 'switchBoard', boardId })}
|
|
428
|
+
onCreateBoard={(name) => vscode.postMessage({ type: 'createBoard', name })}
|
|
429
|
+
/>
|
|
420
430
|
<div className="flex-1 flex overflow-hidden">
|
|
421
431
|
<div className={editingFeature ? 'w-1/2' : 'w-full'}>
|
|
422
432
|
<KanbanBoard
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { Search, X, Columns, Rows, Settings, Plus, Moon, Sun, ChevronDown, Check } from 'lucide-react'
|
|
2
3
|
import { useStore, type DueDateFilter } from '../store'
|
|
3
4
|
import type { Priority } from '../../shared/types'
|
|
4
5
|
|
|
@@ -21,7 +22,7 @@ const dueDateOptions: { value: DueDateFilter; label: string }[] = [
|
|
|
21
22
|
const selectClassName =
|
|
22
23
|
'text-sm bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500 text-zinc-900 dark:text-zinc-100'
|
|
23
24
|
|
|
24
|
-
export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme }: { onOpenSettings: () => void; onAddColumn: () => void; onToggleTheme: () => void }) {
|
|
25
|
+
export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme, onSwitchBoard, onCreateBoard }: { onOpenSettings: () => void; onAddColumn: () => void; onToggleTheme: () => void; onSwitchBoard: (boardId: string) => void; onCreateBoard: (name: string) => void }) {
|
|
25
26
|
const {
|
|
26
27
|
searchQuery,
|
|
27
28
|
setSearchQuery,
|
|
@@ -40,15 +41,110 @@ export function Toolbar({ onOpenSettings, onAddColumn, onToggleTheme }: { onOpen
|
|
|
40
41
|
layout,
|
|
41
42
|
toggleLayout,
|
|
42
43
|
isDarkMode,
|
|
43
|
-
cardSettings
|
|
44
|
+
cardSettings,
|
|
45
|
+
boards,
|
|
46
|
+
currentBoard
|
|
44
47
|
} = useStore()
|
|
45
48
|
|
|
46
49
|
const assignees = getUniqueAssignees()
|
|
47
50
|
const labels = getUniqueLabels()
|
|
48
51
|
const filtersActive = hasActiveFilters()
|
|
49
52
|
|
|
53
|
+
const [boardDropdownOpen, setBoardDropdownOpen] = useState(false)
|
|
54
|
+
const [creatingBoard, setCreatingBoard] = useState(false)
|
|
55
|
+
const [newBoardName, setNewBoardName] = useState('')
|
|
56
|
+
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
57
|
+
const newBoardInputRef = useRef<HTMLInputElement>(null)
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
61
|
+
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
62
|
+
setBoardDropdownOpen(false)
|
|
63
|
+
setCreatingBoard(false)
|
|
64
|
+
setNewBoardName('')
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (boardDropdownOpen) {
|
|
68
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
69
|
+
}
|
|
70
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
71
|
+
}, [boardDropdownOpen])
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (creatingBoard && newBoardInputRef.current) {
|
|
75
|
+
newBoardInputRef.current.focus()
|
|
76
|
+
}
|
|
77
|
+
}, [creatingBoard])
|
|
78
|
+
|
|
79
|
+
const currentBoardName = boards.find(b => b.id === currentBoard)?.name || currentBoard
|
|
80
|
+
|
|
81
|
+
const handleCreateBoard = () => {
|
|
82
|
+
const name = newBoardName.trim()
|
|
83
|
+
if (!name) return
|
|
84
|
+
onCreateBoard(name)
|
|
85
|
+
setNewBoardName('')
|
|
86
|
+
setCreatingBoard(false)
|
|
87
|
+
setBoardDropdownOpen(false)
|
|
88
|
+
}
|
|
89
|
+
|
|
50
90
|
return (
|
|
51
91
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900/50 flex-wrap">
|
|
92
|
+
{/* Board Selector */}
|
|
93
|
+
<div className="relative" ref={dropdownRef}>
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={() => setBoardDropdownOpen(!boardDropdownOpen)}
|
|
97
|
+
className="flex items-center gap-1.5 px-2 py-1.5 text-sm bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md hover:bg-zinc-50 dark:hover:bg-zinc-700 text-zinc-900 dark:text-zinc-100 transition-colors"
|
|
98
|
+
>
|
|
99
|
+
<span className="max-w-[120px] truncate">{currentBoardName}</span>
|
|
100
|
+
<ChevronDown size={14} className={`text-zinc-400 transition-transform ${boardDropdownOpen ? 'rotate-180' : ''}`} />
|
|
101
|
+
</button>
|
|
102
|
+
{boardDropdownOpen && (
|
|
103
|
+
<div className="absolute top-full left-0 mt-1 min-w-[180px] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 rounded-md shadow-lg z-50 py-1">
|
|
104
|
+
{boards.map((b) => (
|
|
105
|
+
<button
|
|
106
|
+
key={b.id}
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={() => {
|
|
109
|
+
onSwitchBoard(b.id)
|
|
110
|
+
setBoardDropdownOpen(false)
|
|
111
|
+
}}
|
|
112
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left hover:bg-zinc-100 dark:hover:bg-zinc-700 text-zinc-900 dark:text-zinc-100 transition-colors"
|
|
113
|
+
>
|
|
114
|
+
<Check size={14} className={b.id === currentBoard ? 'text-blue-500' : 'invisible'} />
|
|
115
|
+
<span className="truncate">{b.name}</span>
|
|
116
|
+
</button>
|
|
117
|
+
))}
|
|
118
|
+
<div className="border-t border-zinc-200 dark:border-zinc-600 my-1" />
|
|
119
|
+
{creatingBoard ? (
|
|
120
|
+
<div className="px-3 py-1.5">
|
|
121
|
+
<input
|
|
122
|
+
ref={newBoardInputRef}
|
|
123
|
+
type="text"
|
|
124
|
+
value={newBoardName}
|
|
125
|
+
onChange={(e) => setNewBoardName(e.target.value)}
|
|
126
|
+
onKeyDown={(e) => {
|
|
127
|
+
if (e.key === 'Enter') handleCreateBoard()
|
|
128
|
+
if (e.key === 'Escape') { setCreatingBoard(false); setNewBoardName('') }
|
|
129
|
+
}}
|
|
130
|
+
placeholder="Board name..."
|
|
131
|
+
className="w-full px-2 py-1 text-sm bg-zinc-50 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 text-zinc-900 dark:text-zinc-100 placeholder-zinc-400"
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
) : (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={() => setCreatingBoard(true)}
|
|
138
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left hover:bg-zinc-100 dark:hover:bg-zinc-700 text-blue-600 dark:text-blue-400 transition-colors"
|
|
139
|
+
>
|
|
140
|
+
<Plus size={14} />
|
|
141
|
+
<span>New Board</span>
|
|
142
|
+
</button>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
52
148
|
{/* Search */}
|
|
53
149
|
<div className="relative flex-1 min-w-[180px] max-w-xs">
|
|
54
150
|
<Search
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { create } from 'zustand'
|
|
2
|
-
import type { Feature,
|
|
2
|
+
import type { Feature, KanbanColumn, Priority, CardDisplaySettings, BoardInfo } from '../../shared/types'
|
|
3
3
|
|
|
4
4
|
export type DueDateFilter = 'all' | 'overdue' | 'today' | 'this-week' | 'no-date'
|
|
5
5
|
export type LayoutMode = 'horizontal' | 'vertical'
|
|
@@ -7,6 +7,8 @@ export type LayoutMode = 'horizontal' | 'vertical'
|
|
|
7
7
|
interface KanbanState {
|
|
8
8
|
features: Feature[]
|
|
9
9
|
columns: KanbanColumn[]
|
|
10
|
+
boards: BoardInfo[]
|
|
11
|
+
currentBoard: string
|
|
10
12
|
isDarkMode: boolean
|
|
11
13
|
searchQuery: string
|
|
12
14
|
priorityFilter: Priority | 'all'
|
|
@@ -19,6 +21,8 @@ interface KanbanState {
|
|
|
19
21
|
|
|
20
22
|
setFeatures: (features: Feature[]) => void
|
|
21
23
|
setColumns: (columns: KanbanColumn[]) => void
|
|
24
|
+
setBoards: (boards: BoardInfo[]) => void
|
|
25
|
+
setCurrentBoard: (boardId: string) => void
|
|
22
26
|
setIsDarkMode: (dark: boolean) => void
|
|
23
27
|
setCardSettings: (settings: CardDisplaySettings) => void
|
|
24
28
|
setSettingsOpen: (open: boolean) => void
|
|
@@ -34,8 +38,8 @@ interface KanbanState {
|
|
|
34
38
|
addFeature: (feature: Feature) => void
|
|
35
39
|
updateFeature: (id: string, updates: Partial<Feature>) => void
|
|
36
40
|
removeFeature: (id: string) => void
|
|
37
|
-
getFeaturesByStatus: (status:
|
|
38
|
-
getFilteredFeaturesByStatus: (status:
|
|
41
|
+
getFeaturesByStatus: (status: string) => Feature[]
|
|
42
|
+
getFilteredFeaturesByStatus: (status: string) => Feature[]
|
|
39
43
|
getUniqueAssignees: () => string[]
|
|
40
44
|
getUniqueLabels: () => string[]
|
|
41
45
|
hasActiveFilters: () => boolean
|
|
@@ -80,6 +84,8 @@ const isOverdue = (date: Date): boolean => {
|
|
|
80
84
|
export const useStore = create<KanbanState>((set, get) => ({
|
|
81
85
|
features: [],
|
|
82
86
|
columns: [],
|
|
87
|
+
boards: [],
|
|
88
|
+
currentBoard: 'default',
|
|
83
89
|
isDarkMode: getInitialDarkMode(),
|
|
84
90
|
searchQuery: '',
|
|
85
91
|
priorityFilter: 'all',
|
|
@@ -103,6 +109,8 @@ export const useStore = create<KanbanState>((set, get) => ({
|
|
|
103
109
|
|
|
104
110
|
setFeatures: (features) => set({ features }),
|
|
105
111
|
setColumns: (columns) => set({ columns }),
|
|
112
|
+
setBoards: (boards) => set({ boards }),
|
|
113
|
+
setCurrentBoard: (boardId) => set({ currentBoard: boardId }),
|
|
106
114
|
setIsDarkMode: (dark) => set({ isDarkMode: dark }),
|
|
107
115
|
setCardSettings: (settings) => set({ cardSettings: settings }),
|
|
108
116
|
setSettingsOpen: (open) => set({ settingsOpen: open }),
|