@andypai/agent-kanban 0.4.0 → 0.5.1
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/package.json +1 -1
- package/src/__tests__/api.test.ts +7 -1
- package/src/__tests__/board-utils.test.ts +37 -1
- package/src/__tests__/db.test.ts +2 -1
- package/src/__tests__/index.test.ts +5 -0
- package/src/__tests__/jira-provider-mutations.test.ts +2 -0
- package/src/__tests__/jira-provider-read.test.ts +29 -0
- package/src/__tests__/linear-provider-sync.test.ts +33 -6
- package/src/__tests__/postgres-jira-provider.test.ts +92 -3
- package/src/__tests__/postgres-linear-provider.test.ts +46 -1
- package/src/__tests__/postgres-local-provider.test.ts +5 -0
- package/src/api.ts +2 -0
- package/src/db.ts +22 -5
- package/src/index.ts +5 -1
- package/src/labels.ts +34 -0
- package/src/providers/jira-cache.ts +64 -1
- package/src/providers/jira-client.ts +4 -0
- package/src/providers/jira.ts +7 -20
- package/src/providers/linear-client.ts +95 -0
- package/src/providers/linear.ts +3 -1
- package/src/providers/local.ts +2 -1
- package/src/providers/postgres-jira.ts +12 -20
- package/src/providers/postgres-linear.ts +3 -1
- package/src/providers/postgres-local.ts +8 -3
- package/src/providers/types.ts +1 -0
- package/ui/dist/assets/index-CFhtfqCn.js +40 -0
- package/ui/dist/index.html +1 -1
- package/ui/dist/assets/index-qNVJ6clH.js +0 -40
package/src/labels.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function normalizeLabels(input: unknown): string[] {
|
|
2
|
+
const labels: string[] = []
|
|
3
|
+
const seen = new Set<string>()
|
|
4
|
+
|
|
5
|
+
collectLabels(input, labels, seen)
|
|
6
|
+
|
|
7
|
+
return labels
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseStoredLabels(raw: unknown): string[] {
|
|
11
|
+
if (typeof raw !== 'string') return normalizeLabels(raw)
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
return normalizeLabels(JSON.parse(raw))
|
|
15
|
+
} catch {
|
|
16
|
+
return normalizeLabels(raw)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectLabels(input: unknown, labels: string[], seen: Set<string>): void {
|
|
21
|
+
if (Array.isArray(input)) {
|
|
22
|
+
for (const item of input) collectLabels(item, labels, seen)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof input !== 'string') return
|
|
27
|
+
|
|
28
|
+
for (const part of input.split(',')) {
|
|
29
|
+
const label = part.trim()
|
|
30
|
+
if (!label || seen.has(label)) continue
|
|
31
|
+
seen.add(label)
|
|
32
|
+
labels.push(label)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Database } from 'bun:sqlite'
|
|
2
|
+
import { ErrorCode, KanbanError } from '../errors'
|
|
2
3
|
import type { BoardView, ProviderTeamInfo, Task } from '../types'
|
|
3
4
|
|
|
4
5
|
// Column ids are prefixed to avoid collisions across sources:
|
|
5
|
-
// - board-sourced columns: 'board:<boardId>:<columnName>'
|
|
6
|
+
// - board-sourced columns: 'board:<boardId>:<columnName>' with an index suffix
|
|
7
|
+
// only when Jira returns duplicate board column names
|
|
6
8
|
// - status-fallback columns: 'status:<statusId>'
|
|
7
9
|
// The provider (T04) picks ONE source per sync, so mixed-source boards
|
|
8
10
|
// do not occur in practice.
|
|
@@ -30,6 +32,67 @@ export interface JiraCacheConfig {
|
|
|
30
32
|
issueTypes: Array<{ id: string; name: string }>
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export interface JiraBoardColumnInput {
|
|
36
|
+
name: string
|
|
37
|
+
statuses: Array<{ id: string }>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function jiraBoardColumnRows(
|
|
41
|
+
boardId: number,
|
|
42
|
+
columns: JiraBoardColumnInput[],
|
|
43
|
+
): Array<{
|
|
44
|
+
id: string
|
|
45
|
+
name: string
|
|
46
|
+
position: number
|
|
47
|
+
statusIds: string[]
|
|
48
|
+
source: 'board'
|
|
49
|
+
}> {
|
|
50
|
+
const seen = new Set<string>()
|
|
51
|
+
return columns.map((column, index) => {
|
|
52
|
+
const baseId = `board:${boardId}:${column.name}`
|
|
53
|
+
let id = baseId
|
|
54
|
+
if (seen.has(id)) {
|
|
55
|
+
id = `${baseId}:${index}`
|
|
56
|
+
let suffix = 2
|
|
57
|
+
while (seen.has(id)) {
|
|
58
|
+
id = `${baseId}:${index}:${suffix}`
|
|
59
|
+
suffix += 1
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
seen.add(id)
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
name: column.name,
|
|
66
|
+
position: index,
|
|
67
|
+
statusIds: column.statuses.map((status) => status.id),
|
|
68
|
+
source: 'board',
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolves a user-supplied column reference to a cached column id, trying
|
|
74
|
+
// (1) exact id, (2) case-insensitive name, (3) raw status id containment.
|
|
75
|
+
// A name that matches multiple columns (possible when Jira returns duplicate
|
|
76
|
+
// board column names) is rejected as ambiguous so the caller picks an id.
|
|
77
|
+
export function resolveJiraColumnId(columns: JiraColumnRow[], input: string): string {
|
|
78
|
+
const byId = columns.find((column) => column.id === input)
|
|
79
|
+
if (byId) return byId.id
|
|
80
|
+
const lower = input.toLowerCase()
|
|
81
|
+
const byName = columns.filter((column) => column.name.toLowerCase() === lower)
|
|
82
|
+
if (byName.length === 1) return byName[0]!.id
|
|
83
|
+
if (byName.length > 1) {
|
|
84
|
+
throw new KanbanError(
|
|
85
|
+
ErrorCode.COLUMN_NOT_FOUND,
|
|
86
|
+
`Jira column name '${input}' is ambiguous; use one of these column ids: ${byName
|
|
87
|
+
.map((column) => column.id)
|
|
88
|
+
.join(', ')}`,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
const byStatus = columns.find((column) => decodeColumnStatusIds(column).includes(input))
|
|
92
|
+
if (byStatus) return byStatus.id
|
|
93
|
+
throw new KanbanError(ErrorCode.COLUMN_NOT_FOUND, `No Jira column matching '${input}'`)
|
|
94
|
+
}
|
|
95
|
+
|
|
33
96
|
interface JiraIssueRow {
|
|
34
97
|
id: string
|
|
35
98
|
key: string
|
package/src/providers/jira.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { adfToPlainText, plainTextToAdf, type AdfDocument } from './jira-adf'
|
|
21
21
|
import { JIRA_CAPABILITIES } from './capabilities'
|
|
22
22
|
import { providerUpstreamError, unsupportedOperation } from './errors'
|
|
23
|
-
import { JiraClient, type JiraComment, type JiraIssue } from './jira-client'
|
|
23
|
+
import { JiraClient, normalizeJiraLabels, type JiraComment, type JiraIssue } from './jira-client'
|
|
24
24
|
import {
|
|
25
25
|
adjustJiraIssueCommentCount,
|
|
26
26
|
decodeColumnStatusIds,
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
getCachedTask,
|
|
33
33
|
getCachedTasks,
|
|
34
34
|
initJiraCacheSchema,
|
|
35
|
+
jiraBoardColumnRows,
|
|
36
|
+
resolveJiraColumnId,
|
|
35
37
|
loadJiraSyncMeta,
|
|
36
38
|
loadTeamInfo,
|
|
37
39
|
pruneJiraIssuesMissingUpstream,
|
|
@@ -122,13 +124,7 @@ export class JiraProvider implements KanbanProvider {
|
|
|
122
124
|
if (this.config.boardId !== undefined) {
|
|
123
125
|
const boardCfg = await this.client.getBoardColumns(this.config.boardId)
|
|
124
126
|
const boardId = this.config.boardId
|
|
125
|
-
const rows = boardCfg.columnConfig.columns
|
|
126
|
-
id: `board:${boardId}:${col.name}`,
|
|
127
|
-
name: col.name,
|
|
128
|
-
position: i,
|
|
129
|
-
statusIds: col.statuses.map((s) => s.id),
|
|
130
|
-
source: 'board' as const,
|
|
131
|
-
}))
|
|
127
|
+
const rows = jiraBoardColumnRows(boardId, boardCfg.columnConfig.columns)
|
|
132
128
|
replaceJiraColumns(this.db, rows)
|
|
133
129
|
} else {
|
|
134
130
|
const statusCats = await this.client.getProjectStatuses(project.key)
|
|
@@ -275,18 +271,7 @@ export class JiraProvider implements KanbanProvider {
|
|
|
275
271
|
}
|
|
276
272
|
|
|
277
273
|
private resolveColumnId(input: string): string {
|
|
278
|
-
|
|
279
|
-
// Priority 1: exact id.
|
|
280
|
-
const byId = columns.find((c) => c.id === input)
|
|
281
|
-
if (byId) return byId.id
|
|
282
|
-
// Priority 2: case-insensitive name.
|
|
283
|
-
const lower = input.toLowerCase()
|
|
284
|
-
const byName = columns.find((c) => c.name.toLowerCase() === lower)
|
|
285
|
-
if (byName) return byName.id
|
|
286
|
-
// Priority 3: status_ids containment (raw status id).
|
|
287
|
-
const byStatus = columns.find((c) => decodeColumnStatusIds(c).includes(input))
|
|
288
|
-
if (byStatus) return byStatus.id
|
|
289
|
-
throw new KanbanError(ErrorCode.COLUMN_NOT_FOUND, `No Jira column matching '${input}'`)
|
|
274
|
+
return resolveJiraColumnId(getCachedColumns(this.db), input)
|
|
290
275
|
}
|
|
291
276
|
|
|
292
277
|
private async buildBoardConfig(): Promise<BoardConfig> {
|
|
@@ -488,6 +473,8 @@ export class JiraProvider implements KanbanProvider {
|
|
|
488
473
|
accountId: this.resolveAssigneeAccountId(input.assignee),
|
|
489
474
|
}
|
|
490
475
|
}
|
|
476
|
+
const labels = normalizeJiraLabels(input.labels)
|
|
477
|
+
if (labels.length > 0) fields['labels'] = labels
|
|
491
478
|
// Column at create-time is intentionally unsupported in Jira mode: new
|
|
492
479
|
// issues land in the project workflow's default start state. Use
|
|
493
480
|
// `moveTask` after create to change status.
|
|
@@ -43,6 +43,11 @@ export interface LinearComment {
|
|
|
43
43
|
user?: { id: string; name?: string | null; displayName?: string | null } | null
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface LinearIssueLabel {
|
|
47
|
+
id: string
|
|
48
|
+
name: string
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
interface LinearIssueNode {
|
|
47
52
|
id: string
|
|
48
53
|
identifier: string
|
|
@@ -232,6 +237,40 @@ export class LinearClient {
|
|
|
232
237
|
return data.projects.nodes
|
|
233
238
|
}
|
|
234
239
|
|
|
240
|
+
async listIssueLabels(): Promise<LinearIssueLabel[]> {
|
|
241
|
+
let after: string | null = null
|
|
242
|
+
const labels: LinearIssueLabel[] = []
|
|
243
|
+
|
|
244
|
+
do {
|
|
245
|
+
const data: {
|
|
246
|
+
issueLabels: {
|
|
247
|
+
nodes: LinearIssueLabel[]
|
|
248
|
+
pageInfo: PageInfo
|
|
249
|
+
}
|
|
250
|
+
} = await this.query(
|
|
251
|
+
`
|
|
252
|
+
query IssueLabels($after: String) {
|
|
253
|
+
issueLabels(first: 100, after: $after) {
|
|
254
|
+
nodes {
|
|
255
|
+
id
|
|
256
|
+
name
|
|
257
|
+
}
|
|
258
|
+
pageInfo {
|
|
259
|
+
hasNextPage
|
|
260
|
+
endCursor
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
`,
|
|
265
|
+
{ after },
|
|
266
|
+
)
|
|
267
|
+
labels.push(...data.issueLabels.nodes)
|
|
268
|
+
after = data.issueLabels.pageInfo.hasNextPage ? data.issueLabels.pageInfo.endCursor : null
|
|
269
|
+
} while (after)
|
|
270
|
+
|
|
271
|
+
return labels
|
|
272
|
+
}
|
|
273
|
+
|
|
235
274
|
async listIssues(teamId: string, updatedAfter?: string): Promise<LinearIssue[]> {
|
|
236
275
|
let after: string | null = null
|
|
237
276
|
const issues: LinearIssue[] = []
|
|
@@ -311,6 +350,7 @@ export class LinearClient {
|
|
|
311
350
|
priority?: number
|
|
312
351
|
assigneeId?: string
|
|
313
352
|
projectId?: string
|
|
353
|
+
labelIds?: string[]
|
|
314
354
|
}): Promise<{ success: boolean; issue: LinearIssue | null }> {
|
|
315
355
|
const data = await this.query<{
|
|
316
356
|
issueCreate: { success: boolean; issue: LinearIssueNode | null }
|
|
@@ -349,6 +389,7 @@ export class LinearClient {
|
|
|
349
389
|
priority: input.priority,
|
|
350
390
|
assigneeId: input.assigneeId,
|
|
351
391
|
projectId: input.projectId,
|
|
392
|
+
labelIds: input.labelIds,
|
|
352
393
|
},
|
|
353
394
|
},
|
|
354
395
|
)
|
|
@@ -567,3 +608,57 @@ export class LinearClient {
|
|
|
567
608
|
}
|
|
568
609
|
}
|
|
569
610
|
}
|
|
611
|
+
|
|
612
|
+
export async function resolveLabelIdsForCreate(
|
|
613
|
+
client: LinearClient,
|
|
614
|
+
inputLabels: string[] | undefined,
|
|
615
|
+
): Promise<string[] | undefined> {
|
|
616
|
+
if (!inputLabels?.some((label) => label.trim())) return undefined
|
|
617
|
+
return resolveLinearLabelIds(inputLabels, await client.listIssueLabels())
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export function resolveLinearLabelIds(
|
|
621
|
+
inputLabels: string[] | undefined,
|
|
622
|
+
availableLabels: LinearIssueLabel[],
|
|
623
|
+
): string[] | undefined {
|
|
624
|
+
const requested = dedupeLabelQueries(inputLabels)
|
|
625
|
+
if (requested.length === 0) return undefined
|
|
626
|
+
|
|
627
|
+
const byName = new Map(
|
|
628
|
+
availableLabels.map((label) => [label.name.trim().toLowerCase(), label.id]),
|
|
629
|
+
)
|
|
630
|
+
const knownIds = new Set(availableLabels.map((label) => label.id))
|
|
631
|
+
const ids: string[] = []
|
|
632
|
+
const missing: string[] = []
|
|
633
|
+
|
|
634
|
+
for (const label of requested) {
|
|
635
|
+
const id = byName.get(label.toLowerCase()) ?? (knownIds.has(label) ? label : undefined)
|
|
636
|
+
if (!id) {
|
|
637
|
+
missing.push(label)
|
|
638
|
+
continue
|
|
639
|
+
}
|
|
640
|
+
if (!ids.includes(id)) ids.push(id)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (missing.length > 0) {
|
|
644
|
+
providerUpstreamError(
|
|
645
|
+
`Linear label${missing.length === 1 ? '' : 's'} not found: ${missing.join(', ')}`,
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return ids.length > 0 ? ids : undefined
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function dedupeLabelQueries(labels: string[] | undefined): string[] {
|
|
653
|
+
const out: string[] = []
|
|
654
|
+
const seen = new Set<string>()
|
|
655
|
+
for (const label of labels ?? []) {
|
|
656
|
+
const trimmed = label.trim()
|
|
657
|
+
if (!trimmed) continue
|
|
658
|
+
const key = trimmed.toLowerCase()
|
|
659
|
+
if (seen.has(key)) continue
|
|
660
|
+
seen.add(key)
|
|
661
|
+
out.push(trimmed)
|
|
662
|
+
}
|
|
663
|
+
return out
|
|
664
|
+
}
|
package/src/providers/linear.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
upsertUsers,
|
|
32
32
|
type LinearActivityRow,
|
|
33
33
|
} from './linear-cache'
|
|
34
|
-
import { LinearClient, type LinearComment } from './linear-client'
|
|
34
|
+
import { LinearClient, resolveLabelIdsForCreate, type LinearComment } from './linear-client'
|
|
35
35
|
import { unsupportedOperation } from './errors'
|
|
36
36
|
import type {
|
|
37
37
|
CreateTaskInput,
|
|
@@ -349,6 +349,7 @@ export class LinearProvider implements KanbanProvider {
|
|
|
349
349
|
async createTask(input: CreateTaskInput) {
|
|
350
350
|
await this.sync()
|
|
351
351
|
const state = input.column ? this.resolveState(input.column) : undefined
|
|
352
|
+
const labelIds = await resolveLabelIdsForCreate(this.client, input.labels)
|
|
352
353
|
const result = await this.client.createIssue({
|
|
353
354
|
teamId: this.resolvedTeamId(),
|
|
354
355
|
stateId: state?.id,
|
|
@@ -357,6 +358,7 @@ export class LinearProvider implements KanbanProvider {
|
|
|
357
358
|
priority: toLinearPriority(input.priority),
|
|
358
359
|
assigneeId: this.resolveAssigneeId(input.assignee),
|
|
359
360
|
projectId: this.resolveProjectId(input.project),
|
|
361
|
+
labelIds,
|
|
360
362
|
})
|
|
361
363
|
if (!result.success || !result.issue) {
|
|
362
364
|
throw new KanbanError(ErrorCode.PROVIDER_UPSTREAM_ERROR, 'Linear issue creation failed')
|
package/src/providers/local.ts
CHANGED
|
@@ -56,13 +56,14 @@ export class LocalProvider implements KanbanProvider {
|
|
|
56
56
|
private enrichTask(task: Task, commentCount?: number): Task {
|
|
57
57
|
const revision = task.revision ?? 0
|
|
58
58
|
const assignees = task.assignee ? [task.assignee] : []
|
|
59
|
+
const labels = Array.isArray(task.labels) ? task.labels : []
|
|
59
60
|
return {
|
|
60
61
|
...task,
|
|
61
62
|
providerId: task.id,
|
|
62
63
|
externalRef: task.id,
|
|
63
64
|
url: null,
|
|
64
65
|
assignees,
|
|
65
|
-
labels
|
|
66
|
+
labels,
|
|
66
67
|
comment_count: commentCount ?? countComments(this.db, task.id),
|
|
67
68
|
version: String(revision),
|
|
68
69
|
source_updated_at: null,
|
|
@@ -14,9 +14,15 @@ import type {
|
|
|
14
14
|
TaskComment,
|
|
15
15
|
} from '../types'
|
|
16
16
|
import { JIRA_CAPABILITIES } from './capabilities'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
decodeColumnStatusIds,
|
|
19
|
+
jiraBoardColumnRows,
|
|
20
|
+
resolveJiraColumnId,
|
|
21
|
+
type JiraActivityRow,
|
|
22
|
+
type JiraColumnRow,
|
|
23
|
+
} from './jira-cache'
|
|
18
24
|
import { adfToPlainText, plainTextToAdf, type AdfDocument } from './jira-adf'
|
|
19
|
-
import { JiraClient, type JiraComment, type JiraIssue } from './jira-client'
|
|
25
|
+
import { JiraClient, normalizeJiraLabels, type JiraComment, type JiraIssue } from './jira-client'
|
|
20
26
|
import type { JiraProviderConfig } from './jira'
|
|
21
27
|
import { providerUpstreamError, unsupportedOperation } from './errors'
|
|
22
28
|
import type {
|
|
@@ -605,15 +611,7 @@ export class PostgresJiraProvider implements KanbanProvider {
|
|
|
605
611
|
if (this.config.boardId !== undefined) {
|
|
606
612
|
const boardCfg = await this.client.getBoardColumns(this.config.boardId)
|
|
607
613
|
const boardId = this.config.boardId
|
|
608
|
-
await this.replaceColumns(
|
|
609
|
-
boardCfg.columnConfig.columns.map((column, index) => ({
|
|
610
|
-
id: `board:${boardId}:${column.name}`,
|
|
611
|
-
name: column.name,
|
|
612
|
-
position: index,
|
|
613
|
-
statusIds: column.statuses.map((status) => status.id),
|
|
614
|
-
source: 'board' as const,
|
|
615
|
-
})),
|
|
616
|
-
)
|
|
614
|
+
await this.replaceColumns(jiraBoardColumnRows(boardId, boardCfg.columnConfig.columns))
|
|
617
615
|
} else {
|
|
618
616
|
const statusCats = await this.client.getProjectStatuses(project.key)
|
|
619
617
|
const seen = new Set<string>()
|
|
@@ -741,15 +739,7 @@ export class PostgresJiraProvider implements KanbanProvider {
|
|
|
741
739
|
}
|
|
742
740
|
|
|
743
741
|
private async resolveColumnId(input: string): Promise<string> {
|
|
744
|
-
|
|
745
|
-
const byId = columns.find((column) => column.id === input)
|
|
746
|
-
if (byId) return byId.id
|
|
747
|
-
const lower = input.toLowerCase()
|
|
748
|
-
const byName = columns.find((column) => column.name.toLowerCase() === lower)
|
|
749
|
-
if (byName) return byName.id
|
|
750
|
-
const byStatus = columns.find((column) => decodeColumnStatusIds(column).includes(input))
|
|
751
|
-
if (byStatus) return byStatus.id
|
|
752
|
-
throw new KanbanError(ErrorCode.COLUMN_NOT_FOUND, `No Jira column matching '${input}'`)
|
|
742
|
+
return resolveJiraColumnId(await this.getColumns(), input)
|
|
753
743
|
}
|
|
754
744
|
|
|
755
745
|
private async buildBoardConfig(): Promise<BoardConfig> {
|
|
@@ -948,6 +938,8 @@ export class PostgresJiraProvider implements KanbanProvider {
|
|
|
948
938
|
if (input.assignee) {
|
|
949
939
|
fields['assignee'] = { accountId: await this.resolveAssigneeAccountId(input.assignee) }
|
|
950
940
|
}
|
|
941
|
+
const labels = normalizeJiraLabels(input.labels)
|
|
942
|
+
if (labels.length > 0) fields['labels'] = labels
|
|
951
943
|
const created = await this.client.createIssue({ fields })
|
|
952
944
|
await this.sync(true)
|
|
953
945
|
const fresh = await this.getCachedTask(created.key)
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from '../webhook-events'
|
|
23
23
|
import { LINEAR_CAPABILITIES } from './capabilities'
|
|
24
24
|
import { unsupportedOperation } from './errors'
|
|
25
|
-
import { LinearClient, type LinearComment } from './linear-client'
|
|
25
|
+
import { LinearClient, resolveLabelIdsForCreate, type LinearComment } from './linear-client'
|
|
26
26
|
import type {
|
|
27
27
|
CreateTaskInput,
|
|
28
28
|
KanbanProvider,
|
|
@@ -822,6 +822,7 @@ export class PostgresLinearProvider implements KanbanProvider {
|
|
|
822
822
|
async createTask(input: CreateTaskInput): Promise<Task> {
|
|
823
823
|
await this.sync()
|
|
824
824
|
const state = input.column ? await this.resolveState(input.column) : undefined
|
|
825
|
+
const labelIds = await resolveLabelIdsForCreate(this.client, input.labels)
|
|
825
826
|
const result = await this.client.createIssue({
|
|
826
827
|
teamId: await this.resolvedTeamId(),
|
|
827
828
|
stateId: state?.id,
|
|
@@ -830,6 +831,7 @@ export class PostgresLinearProvider implements KanbanProvider {
|
|
|
830
831
|
priority: toLinearPriority(input.priority),
|
|
831
832
|
assigneeId: await this.resolveAssigneeId(input.assignee),
|
|
832
833
|
projectId: await this.resolveProjectId(input.project),
|
|
834
|
+
labelIds,
|
|
833
835
|
})
|
|
834
836
|
if (!result.success || !result.issue) {
|
|
835
837
|
throw new KanbanError(ErrorCode.PROVIDER_UPSTREAM_ERROR, 'Linear issue creation failed')
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
UpdateTaskInput,
|
|
25
25
|
} from './types'
|
|
26
26
|
import type { LocalTrackerConfig } from '../tracker-config'
|
|
27
|
+
import { normalizeLabels, parseStoredLabels } from '../labels'
|
|
27
28
|
|
|
28
29
|
const DEFAULT_COLUMNS = [
|
|
29
30
|
{ name: 'recurring', position: 0 },
|
|
@@ -43,6 +44,7 @@ interface TaskRow {
|
|
|
43
44
|
priority: Priority
|
|
44
45
|
assignee: string
|
|
45
46
|
project: string
|
|
47
|
+
labels: string
|
|
46
48
|
metadata: string
|
|
47
49
|
revision: number
|
|
48
50
|
created_at: string
|
|
@@ -126,6 +128,7 @@ export class PostgresLocalProvider implements KanbanProvider {
|
|
|
126
128
|
priority TEXT NOT NULL DEFAULT 'medium',
|
|
127
129
|
assignee TEXT NOT NULL DEFAULT '',
|
|
128
130
|
project TEXT NOT NULL DEFAULT '',
|
|
131
|
+
labels TEXT NOT NULL DEFAULT '[]',
|
|
129
132
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
130
133
|
revision INTEGER NOT NULL DEFAULT 0,
|
|
131
134
|
created_at TEXT NOT NULL,
|
|
@@ -162,6 +165,7 @@ export class PostgresLocalProvider implements KanbanProvider {
|
|
|
162
165
|
exited_at TEXT
|
|
163
166
|
)
|
|
164
167
|
`
|
|
168
|
+
await this.sql`ALTER TABLE tasks ADD COLUMN IF NOT EXISTS labels TEXT NOT NULL DEFAULT '[]'`
|
|
165
169
|
await this.sql`CREATE INDEX IF NOT EXISTS idx_tasks_column_id ON tasks(column_id)`
|
|
166
170
|
await this.sql`CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority)`
|
|
167
171
|
await this.sql`CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee)`
|
|
@@ -194,7 +198,7 @@ export class PostgresLocalProvider implements KanbanProvider {
|
|
|
194
198
|
externalRef: row.id,
|
|
195
199
|
url: null,
|
|
196
200
|
assignees: row.assignee ? [row.assignee] : [],
|
|
197
|
-
labels:
|
|
201
|
+
labels: parseStoredLabels(row.labels),
|
|
198
202
|
comment_count: commentCount,
|
|
199
203
|
version: String(row.revision ?? 0),
|
|
200
204
|
source_updated_at: null,
|
|
@@ -352,6 +356,7 @@ export class PostgresLocalProvider implements KanbanProvider {
|
|
|
352
356
|
const priority = input.priority ?? 'medium'
|
|
353
357
|
assertPriority(priority)
|
|
354
358
|
const metadata = parseMetadata(input.metadata)
|
|
359
|
+
const labels = normalizeLabels(input.labels)
|
|
355
360
|
const column = input.column
|
|
356
361
|
? await this.resolveColumn(input.column)
|
|
357
362
|
: await this.resolveDefaultTaskColumn()
|
|
@@ -362,12 +367,12 @@ export class PostgresLocalProvider implements KanbanProvider {
|
|
|
362
367
|
const timestamp = nowIso()
|
|
363
368
|
await this.sql`
|
|
364
369
|
INSERT INTO tasks (
|
|
365
|
-
id, title, description, column_id, position, priority, assignee, project, metadata,
|
|
370
|
+
id, title, description, column_id, position, priority, assignee, project, labels, metadata,
|
|
366
371
|
revision, created_at, updated_at
|
|
367
372
|
)
|
|
368
373
|
VALUES (
|
|
369
374
|
${id}, ${input.title}, ${input.description ?? ''}, ${column.id}, ${Number(positionRow?.next ?? 0)},
|
|
370
|
-
${priority}, ${input.assignee ?? ''}, ${input.project ?? ''}, ${metadata},
|
|
375
|
+
${priority}, ${input.assignee ?? ''}, ${input.project ?? ''}, ${JSON.stringify(labels)}, ${metadata},
|
|
371
376
|
0, ${timestamp}, ${timestamp}
|
|
372
377
|
)
|
|
373
378
|
`
|