@mdxui/issues 6.0.0
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/.turbo/turbo-typecheck.log +5 -0
- package/CHANGELOG.md +80 -0
- package/package.json +35 -0
- package/src/components/epic-list.tsx +210 -0
- package/src/components/index.ts +9 -0
- package/src/components/issue-badges.tsx +135 -0
- package/src/components/issue-board.tsx +211 -0
- package/src/components/issue-card.tsx +57 -0
- package/src/components/issue-detail.tsx +390 -0
- package/src/components/issue-dialog.tsx +184 -0
- package/src/components/issue-filters.tsx +180 -0
- package/src/components/issue-list.tsx +142 -0
- package/src/components/issue-row.tsx +56 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-beads.ts +224 -0
- package/src/hooks/use-issue-mutations.ts +260 -0
- package/src/hooks/use-issues-store.ts +210 -0
- package/src/index.ts +20 -0
- package/src/types/index.ts +194 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import { useIssuesStore } from './use-issues-store'
|
|
3
|
+
import type { Issue, IssueStatus, IssuePriority } from '../types'
|
|
4
|
+
|
|
5
|
+
export interface IssueMutations {
|
|
6
|
+
/** Update issue title */
|
|
7
|
+
updateTitle: (id: string, title: string) => Promise<void>
|
|
8
|
+
/** Update issue status */
|
|
9
|
+
updateStatus: (id: string, status: IssueStatus) => Promise<void>
|
|
10
|
+
/** Update issue priority */
|
|
11
|
+
updatePriority: (id: string, priority: IssuePriority) => Promise<void>
|
|
12
|
+
/** Update issue assignee */
|
|
13
|
+
updateAssignee: (id: string, assignee: string | undefined) => Promise<void>
|
|
14
|
+
/** Add label to issue */
|
|
15
|
+
addLabel: (id: string, label: string) => Promise<void>
|
|
16
|
+
/** Remove label from issue */
|
|
17
|
+
removeLabel: (id: string, label: string) => Promise<void>
|
|
18
|
+
/** Update description */
|
|
19
|
+
updateDescription: (id: string, description: string) => Promise<void>
|
|
20
|
+
/** Update design notes */
|
|
21
|
+
updateDesign: (id: string, design: string) => Promise<void>
|
|
22
|
+
/** Update acceptance criteria */
|
|
23
|
+
updateAcceptance: (id: string, acceptance: string) => Promise<void>
|
|
24
|
+
/** Update notes */
|
|
25
|
+
updateNotes: (id: string, notes: string) => Promise<void>
|
|
26
|
+
/** Add dependency */
|
|
27
|
+
addDependency: (id: string, dependsOn: string) => Promise<void>
|
|
28
|
+
/** Remove dependency */
|
|
29
|
+
removeDependency: (id: string, dependsOn: string) => Promise<void>
|
|
30
|
+
/** Close issue */
|
|
31
|
+
closeIssue: (id: string, reason?: string) => Promise<void>
|
|
32
|
+
/** Reopen issue */
|
|
33
|
+
reopenIssue: (id: string) => Promise<void>
|
|
34
|
+
/** Create issue */
|
|
35
|
+
createIssue: (issue: Omit<Issue, 'id' | 'createdAt' | 'updatedAt'>) => Promise<Issue>
|
|
36
|
+
/** Delete issue */
|
|
37
|
+
deleteIssue: (id: string) => Promise<void>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface UseIssueMutationsOptions {
|
|
41
|
+
/** RPC transport function for beads CLI */
|
|
42
|
+
transport?: (method: string, params: Record<string, unknown>) => Promise<unknown>
|
|
43
|
+
/** On mutation success */
|
|
44
|
+
onSuccess?: (method: string, result: unknown) => void
|
|
45
|
+
/** On mutation error */
|
|
46
|
+
onError?: (method: string, error: Error) => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useIssueMutations(options: UseIssueMutationsOptions = {}): IssueMutations {
|
|
50
|
+
const { transport, onSuccess, onError } = options
|
|
51
|
+
const updateIssue = useIssuesStore((s) => s.updateIssue)
|
|
52
|
+
const addIssue = useIssuesStore((s) => s.addIssue)
|
|
53
|
+
const removeIssue = useIssuesStore((s) => s.removeIssue)
|
|
54
|
+
|
|
55
|
+
const mutate = useCallback(
|
|
56
|
+
async (method: string, params: Record<string, unknown>) => {
|
|
57
|
+
try {
|
|
58
|
+
const result = transport ? await transport(method, params) : undefined
|
|
59
|
+
onSuccess?.(method, result)
|
|
60
|
+
return result
|
|
61
|
+
} catch (error) {
|
|
62
|
+
onError?.(method, error as Error)
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[transport, onSuccess, onError]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const updateTitle = useCallback(
|
|
70
|
+
async (id: string, title: string) => {
|
|
71
|
+
updateIssue(id, { title })
|
|
72
|
+
await mutate('update', { id, title })
|
|
73
|
+
},
|
|
74
|
+
[updateIssue, mutate]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const updateStatus = useCallback(
|
|
78
|
+
async (id: string, status: IssueStatus) => {
|
|
79
|
+
const updates: Partial<Issue> = { status }
|
|
80
|
+
if (status === 'closed') {
|
|
81
|
+
updates.closedAt = new Date().toISOString()
|
|
82
|
+
} else {
|
|
83
|
+
updates.closedAt = undefined
|
|
84
|
+
}
|
|
85
|
+
updateIssue(id, updates)
|
|
86
|
+
await mutate('update', { id, status })
|
|
87
|
+
},
|
|
88
|
+
[updateIssue, mutate]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const updatePriority = useCallback(
|
|
92
|
+
async (id: string, priority: IssuePriority) => {
|
|
93
|
+
updateIssue(id, { priority })
|
|
94
|
+
await mutate('update', { id, priority })
|
|
95
|
+
},
|
|
96
|
+
[updateIssue, mutate]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const updateAssignee = useCallback(
|
|
100
|
+
async (id: string, assignee: string | undefined) => {
|
|
101
|
+
updateIssue(id, { assignee })
|
|
102
|
+
await mutate('update', { id, assignee: assignee ?? null })
|
|
103
|
+
},
|
|
104
|
+
[updateIssue, mutate]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const addLabel = useCallback(
|
|
108
|
+
async (id: string, label: string) => {
|
|
109
|
+
const issue = useIssuesStore.getState().issues.find((i) => i.id === id)
|
|
110
|
+
if (issue && !issue.labels.includes(label)) {
|
|
111
|
+
const labels = [...issue.labels, label]
|
|
112
|
+
updateIssue(id, { labels })
|
|
113
|
+
await mutate('label', { id, add: [label] })
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
[updateIssue, mutate]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const removeLabel = useCallback(
|
|
120
|
+
async (id: string, label: string) => {
|
|
121
|
+
const issue = useIssuesStore.getState().issues.find((i) => i.id === id)
|
|
122
|
+
if (issue) {
|
|
123
|
+
const labels = issue.labels.filter((l: string) => l !== label)
|
|
124
|
+
updateIssue(id, { labels })
|
|
125
|
+
await mutate('label', { id, remove: [label] })
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
[updateIssue, mutate]
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
const updateDescription = useCallback(
|
|
132
|
+
async (id: string, description: string) => {
|
|
133
|
+
updateIssue(id, { description })
|
|
134
|
+
await mutate('update', { id, description })
|
|
135
|
+
},
|
|
136
|
+
[updateIssue, mutate]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const updateDesign = useCallback(
|
|
140
|
+
async (id: string, design: string) => {
|
|
141
|
+
updateIssue(id, { design })
|
|
142
|
+
await mutate('update', { id, design })
|
|
143
|
+
},
|
|
144
|
+
[updateIssue, mutate]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const updateAcceptance = useCallback(
|
|
148
|
+
async (id: string, acceptance: string) => {
|
|
149
|
+
updateIssue(id, { acceptance })
|
|
150
|
+
await mutate('update', { id, acceptance })
|
|
151
|
+
},
|
|
152
|
+
[updateIssue, mutate]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const updateNotes = useCallback(
|
|
156
|
+
async (id: string, notes: string) => {
|
|
157
|
+
updateIssue(id, { notes })
|
|
158
|
+
await mutate('update', { id, notes })
|
|
159
|
+
},
|
|
160
|
+
[updateIssue, mutate]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const addDependency = useCallback(
|
|
164
|
+
async (id: string, dependsOn: string) => {
|
|
165
|
+
const issue = useIssuesStore.getState().issues.find((i) => i.id === id)
|
|
166
|
+
if (issue && !issue.dependencies.includes(dependsOn)) {
|
|
167
|
+
updateIssue(id, { dependencies: [...issue.dependencies, dependsOn] })
|
|
168
|
+
await mutate('dep', { id, add: dependsOn })
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
[updateIssue, mutate]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const removeDependency = useCallback(
|
|
175
|
+
async (id: string, dependsOn: string) => {
|
|
176
|
+
const issue = useIssuesStore.getState().issues.find((i) => i.id === id)
|
|
177
|
+
if (issue) {
|
|
178
|
+
updateIssue(id, { dependencies: issue.dependencies.filter((d: string) => d !== dependsOn) })
|
|
179
|
+
await mutate('dep', { id, remove: dependsOn })
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
[updateIssue, mutate]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
const closeIssue = useCallback(
|
|
186
|
+
async (id: string, reason?: string) => {
|
|
187
|
+
updateIssue(id, {
|
|
188
|
+
status: 'closed',
|
|
189
|
+
closedAt: new Date().toISOString(),
|
|
190
|
+
closeReason: reason,
|
|
191
|
+
})
|
|
192
|
+
await mutate('close', { id, reason })
|
|
193
|
+
},
|
|
194
|
+
[updateIssue, mutate]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const reopenIssue = useCallback(
|
|
198
|
+
async (id: string) => {
|
|
199
|
+
updateIssue(id, {
|
|
200
|
+
status: 'open',
|
|
201
|
+
closedAt: undefined,
|
|
202
|
+
closeReason: undefined,
|
|
203
|
+
})
|
|
204
|
+
await mutate('reopen', { id })
|
|
205
|
+
},
|
|
206
|
+
[updateIssue, mutate]
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
const createIssue = useCallback(
|
|
210
|
+
async (data: Omit<Issue, 'id' | 'createdAt' | 'updatedAt'>) => {
|
|
211
|
+
const now = new Date().toISOString()
|
|
212
|
+
const tempId = `temp-${Date.now()}`
|
|
213
|
+
const issue: Issue = {
|
|
214
|
+
...data,
|
|
215
|
+
id: tempId,
|
|
216
|
+
createdAt: now,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
}
|
|
219
|
+
addIssue(issue)
|
|
220
|
+
|
|
221
|
+
const result = (await mutate('create', data)) as { id: string } | undefined
|
|
222
|
+
if (result?.id && result.id !== tempId) {
|
|
223
|
+
// Update with real ID from server
|
|
224
|
+
removeIssue(tempId)
|
|
225
|
+
const realIssue = { ...issue, id: result.id }
|
|
226
|
+
addIssue(realIssue)
|
|
227
|
+
return realIssue
|
|
228
|
+
}
|
|
229
|
+
return issue
|
|
230
|
+
},
|
|
231
|
+
[addIssue, removeIssue, mutate]
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const deleteIssue = useCallback(
|
|
235
|
+
async (id: string) => {
|
|
236
|
+
removeIssue(id)
|
|
237
|
+
await mutate('delete', { id })
|
|
238
|
+
},
|
|
239
|
+
[removeIssue, mutate]
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
updateTitle,
|
|
244
|
+
updateStatus,
|
|
245
|
+
updatePriority,
|
|
246
|
+
updateAssignee,
|
|
247
|
+
addLabel,
|
|
248
|
+
removeLabel,
|
|
249
|
+
updateDescription,
|
|
250
|
+
updateDesign,
|
|
251
|
+
updateAcceptance,
|
|
252
|
+
updateNotes,
|
|
253
|
+
addDependency,
|
|
254
|
+
removeDependency,
|
|
255
|
+
closeIssue,
|
|
256
|
+
reopenIssue,
|
|
257
|
+
createIssue,
|
|
258
|
+
deleteIssue,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import type { Issue, IssueFilters, IssueSort, ClosedTimeRange } from '../types'
|
|
3
|
+
|
|
4
|
+
interface IssuesState {
|
|
5
|
+
/** All issues */
|
|
6
|
+
issues: Issue[]
|
|
7
|
+
/** Currently selected issue ID */
|
|
8
|
+
selectedId: string | null
|
|
9
|
+
/** Current filters */
|
|
10
|
+
filters: IssueFilters
|
|
11
|
+
/** Current sort */
|
|
12
|
+
sort: IssueSort
|
|
13
|
+
/** Closed column time filter */
|
|
14
|
+
closedTimeRange: ClosedTimeRange
|
|
15
|
+
/** Loading state */
|
|
16
|
+
isLoading: boolean
|
|
17
|
+
/** Error state */
|
|
18
|
+
error: string | null
|
|
19
|
+
|
|
20
|
+
// Actions
|
|
21
|
+
setIssues: (issues: Issue[]) => void
|
|
22
|
+
addIssue: (issue: Issue) => void
|
|
23
|
+
updateIssue: (id: string, updates: Partial<Issue>) => void
|
|
24
|
+
removeIssue: (id: string) => void
|
|
25
|
+
selectIssue: (id: string | null) => void
|
|
26
|
+
setFilters: (filters: IssueFilters) => void
|
|
27
|
+
setSort: (sort: IssueSort) => void
|
|
28
|
+
setClosedTimeRange: (range: ClosedTimeRange) => void
|
|
29
|
+
setLoading: (loading: boolean) => void
|
|
30
|
+
setError: (error: string | null) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const useIssuesStore = create<IssuesState>((set) => ({
|
|
34
|
+
issues: [],
|
|
35
|
+
selectedId: null,
|
|
36
|
+
filters: {},
|
|
37
|
+
sort: { field: 'priority', direction: 'asc' },
|
|
38
|
+
closedTimeRange: 'last7days',
|
|
39
|
+
isLoading: false,
|
|
40
|
+
error: null,
|
|
41
|
+
|
|
42
|
+
setIssues: (issues) => set({ issues }),
|
|
43
|
+
|
|
44
|
+
addIssue: (issue) =>
|
|
45
|
+
set((state) => ({
|
|
46
|
+
issues: [...state.issues, issue],
|
|
47
|
+
})),
|
|
48
|
+
|
|
49
|
+
updateIssue: (id, updates) =>
|
|
50
|
+
set((state) => ({
|
|
51
|
+
issues: state.issues.map((issue) =>
|
|
52
|
+
issue.id === id ? { ...issue, ...updates, updatedAt: new Date().toISOString() } : issue
|
|
53
|
+
),
|
|
54
|
+
})),
|
|
55
|
+
|
|
56
|
+
removeIssue: (id) =>
|
|
57
|
+
set((state) => ({
|
|
58
|
+
issues: state.issues.filter((issue) => issue.id !== id),
|
|
59
|
+
selectedId: state.selectedId === id ? null : state.selectedId,
|
|
60
|
+
})),
|
|
61
|
+
|
|
62
|
+
selectIssue: (id) => set({ selectedId: id }),
|
|
63
|
+
|
|
64
|
+
setFilters: (filters) => set({ filters }),
|
|
65
|
+
|
|
66
|
+
setSort: (sort) => set({ sort }),
|
|
67
|
+
|
|
68
|
+
setClosedTimeRange: (closedTimeRange) => set({ closedTimeRange }),
|
|
69
|
+
|
|
70
|
+
setLoading: (isLoading) => set({ isLoading }),
|
|
71
|
+
|
|
72
|
+
setError: (error) => set({ error }),
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
// Selector helpers
|
|
76
|
+
export const useIssue = (id: string | null) => {
|
|
77
|
+
return useIssuesStore((state) => (id ? state.issues.find((i) => i.id === id) : undefined))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const useSelectedIssue = () => {
|
|
81
|
+
return useIssuesStore((state) =>
|
|
82
|
+
state.selectedId ? state.issues.find((i) => i.id === state.selectedId) : undefined
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const useFilteredIssues = () => {
|
|
87
|
+
return useIssuesStore((state) => {
|
|
88
|
+
let filtered = [...state.issues]
|
|
89
|
+
|
|
90
|
+
const { filters, sort, closedTimeRange } = state
|
|
91
|
+
|
|
92
|
+
// Apply status filter
|
|
93
|
+
if (filters.status?.length) {
|
|
94
|
+
filtered = filtered.filter((i) => filters.status!.includes(i.status))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Apply type filter
|
|
98
|
+
if (filters.type?.length) {
|
|
99
|
+
filtered = filtered.filter((i) => filters.type!.includes(i.type))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Apply priority filter
|
|
103
|
+
if (filters.priority?.length) {
|
|
104
|
+
filtered = filtered.filter((i) => filters.priority!.includes(i.priority))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Apply assignee filter
|
|
108
|
+
if (filters.assignee) {
|
|
109
|
+
filtered = filtered.filter((i) => i.assignee === filters.assignee)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Apply labels filter
|
|
113
|
+
if (filters.labels?.length) {
|
|
114
|
+
filtered = filtered.filter((i) => filters.labels!.some((l: string) => i.labels.includes(l)))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Apply epic filter
|
|
118
|
+
if (filters.epic) {
|
|
119
|
+
filtered = filtered.filter((i) => i.epic === filters.epic)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Apply search filter
|
|
123
|
+
if (filters.search) {
|
|
124
|
+
const search = filters.search.toLowerCase()
|
|
125
|
+
filtered = filtered.filter(
|
|
126
|
+
(i) => i.id.toLowerCase().includes(search) || i.title.toLowerCase().includes(search)
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Apply blocked filter
|
|
131
|
+
if (filters.blocked) {
|
|
132
|
+
filtered = filtered.filter((i) => i.status === 'blocked')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Apply ready filter (no dependencies in non-closed status)
|
|
136
|
+
if (filters.ready) {
|
|
137
|
+
filtered = filtered.filter(
|
|
138
|
+
(i) =>
|
|
139
|
+
i.status === 'open' &&
|
|
140
|
+
!i.dependencies.some((depId: string) => {
|
|
141
|
+
const dep = state.issues.find((d) => d.id === depId)
|
|
142
|
+
return dep && dep.status !== 'closed'
|
|
143
|
+
})
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Apply closed time range filter for closed issues
|
|
148
|
+
if (closedTimeRange !== 'all') {
|
|
149
|
+
const now = new Date()
|
|
150
|
+
const cutoff = new Date()
|
|
151
|
+
|
|
152
|
+
if (closedTimeRange === 'today') {
|
|
153
|
+
cutoff.setDate(now.getDate() - 1)
|
|
154
|
+
} else if (closedTimeRange === 'last3days') {
|
|
155
|
+
cutoff.setDate(now.getDate() - 3)
|
|
156
|
+
} else if (closedTimeRange === 'last7days') {
|
|
157
|
+
cutoff.setDate(now.getDate() - 7)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
filtered = filtered.filter((i) => {
|
|
161
|
+
if (i.status === 'closed' && i.closedAt) {
|
|
162
|
+
return new Date(i.closedAt) >= cutoff
|
|
163
|
+
}
|
|
164
|
+
return true
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Apply sort
|
|
169
|
+
filtered.sort((a, b) => {
|
|
170
|
+
let aVal: string | number
|
|
171
|
+
let bVal: string | number
|
|
172
|
+
|
|
173
|
+
switch (sort.field) {
|
|
174
|
+
case 'priority':
|
|
175
|
+
aVal = a.priority
|
|
176
|
+
bVal = b.priority
|
|
177
|
+
break
|
|
178
|
+
case 'createdAt':
|
|
179
|
+
aVal = a.createdAt
|
|
180
|
+
bVal = b.createdAt
|
|
181
|
+
break
|
|
182
|
+
case 'updatedAt':
|
|
183
|
+
aVal = a.updatedAt
|
|
184
|
+
bVal = b.updatedAt
|
|
185
|
+
break
|
|
186
|
+
case 'closedAt':
|
|
187
|
+
aVal = a.closedAt ?? ''
|
|
188
|
+
bVal = b.closedAt ?? ''
|
|
189
|
+
break
|
|
190
|
+
case 'title':
|
|
191
|
+
aVal = a.title.toLowerCase()
|
|
192
|
+
bVal = b.title.toLowerCase()
|
|
193
|
+
break
|
|
194
|
+
case 'status':
|
|
195
|
+
aVal = a.status
|
|
196
|
+
bVal = b.status
|
|
197
|
+
break
|
|
198
|
+
default:
|
|
199
|
+
aVal = a.createdAt
|
|
200
|
+
bVal = b.createdAt
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (aVal < bVal) return sort.direction === 'asc' ? -1 : 1
|
|
204
|
+
if (aVal > bVal) return sort.direction === 'asc' ? 1 : -1
|
|
205
|
+
return 0
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
return filtered
|
|
209
|
+
})
|
|
210
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
Issue,
|
|
4
|
+
IssueStatus,
|
|
5
|
+
IssueType,
|
|
6
|
+
IssuePriority,
|
|
7
|
+
IssueComment,
|
|
8
|
+
Epic,
|
|
9
|
+
IssueFilters,
|
|
10
|
+
IssueSort,
|
|
11
|
+
BoardColumn,
|
|
12
|
+
ClosedTimeRange,
|
|
13
|
+
} from './types'
|
|
14
|
+
export { defaultBoardColumns } from './types'
|
|
15
|
+
|
|
16
|
+
// Hooks
|
|
17
|
+
export * from './hooks'
|
|
18
|
+
|
|
19
|
+
// Components
|
|
20
|
+
export * from './components'
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Issue Status
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
export const IssueStatusSchema = z.enum([
|
|
8
|
+
'open',
|
|
9
|
+
'in_progress',
|
|
10
|
+
'blocked',
|
|
11
|
+
'closed',
|
|
12
|
+
])
|
|
13
|
+
export type IssueStatus = z.infer<typeof IssueStatusSchema>
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Issue Type
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
export const IssueTypeSchema = z.enum([
|
|
20
|
+
'task',
|
|
21
|
+
'bug',
|
|
22
|
+
'feature',
|
|
23
|
+
'epic',
|
|
24
|
+
'story',
|
|
25
|
+
'chore',
|
|
26
|
+
])
|
|
27
|
+
export type IssueType = z.infer<typeof IssueTypeSchema>
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Issue Priority (0-4, 0=critical, 4=backlog)
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
export const IssuePrioritySchema = z.number().min(0).max(4)
|
|
34
|
+
export type IssuePriority = z.infer<typeof IssuePrioritySchema>
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// Issue - Core data type
|
|
38
|
+
// =============================================================================
|
|
39
|
+
|
|
40
|
+
export const IssueSchema = z.object({
|
|
41
|
+
/** Unique issue ID (e.g., "beads-001") */
|
|
42
|
+
id: z.string(),
|
|
43
|
+
/** Issue title */
|
|
44
|
+
title: z.string(),
|
|
45
|
+
/** Detailed description (markdown) */
|
|
46
|
+
description: z.string().optional(),
|
|
47
|
+
/** Issue type */
|
|
48
|
+
type: IssueTypeSchema,
|
|
49
|
+
/** Issue status */
|
|
50
|
+
status: IssueStatusSchema,
|
|
51
|
+
/** Priority (0=critical, 4=backlog) */
|
|
52
|
+
priority: IssuePrioritySchema,
|
|
53
|
+
/** Assignee username */
|
|
54
|
+
assignee: z.string().optional(),
|
|
55
|
+
/** Labels */
|
|
56
|
+
labels: z.array(z.string()).default([]),
|
|
57
|
+
/** Parent epic ID */
|
|
58
|
+
epic: z.string().optional(),
|
|
59
|
+
/** IDs this issue depends on */
|
|
60
|
+
dependencies: z.array(z.string()).default([]),
|
|
61
|
+
/** IDs that depend on this issue */
|
|
62
|
+
dependents: z.array(z.string()).default([]),
|
|
63
|
+
/** Design notes (markdown) */
|
|
64
|
+
design: z.string().optional(),
|
|
65
|
+
/** Acceptance criteria (markdown) */
|
|
66
|
+
acceptance: z.string().optional(),
|
|
67
|
+
/** General notes (markdown) */
|
|
68
|
+
notes: z.string().optional(),
|
|
69
|
+
/** Created timestamp */
|
|
70
|
+
createdAt: z.string(),
|
|
71
|
+
/** Updated timestamp */
|
|
72
|
+
updatedAt: z.string(),
|
|
73
|
+
/** Closed timestamp */
|
|
74
|
+
closedAt: z.string().optional(),
|
|
75
|
+
/** Close reason */
|
|
76
|
+
closeReason: z.string().optional(),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
export type Issue = z.infer<typeof IssueSchema>
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// Issue Comment
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
export const IssueCommentSchema = z.object({
|
|
86
|
+
/** Comment ID */
|
|
87
|
+
id: z.string(),
|
|
88
|
+
/** Comment author */
|
|
89
|
+
author: z.string(),
|
|
90
|
+
/** Comment content (markdown) */
|
|
91
|
+
content: z.string(),
|
|
92
|
+
/** Created timestamp */
|
|
93
|
+
createdAt: z.string(),
|
|
94
|
+
/** Updated timestamp */
|
|
95
|
+
updatedAt: z.string().optional(),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export type IssueComment = z.infer<typeof IssueCommentSchema>
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Epic - Grouping of issues
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
export const EpicSchema = z.object({
|
|
105
|
+
/** Epic ID */
|
|
106
|
+
id: z.string(),
|
|
107
|
+
/** Epic title */
|
|
108
|
+
title: z.string(),
|
|
109
|
+
/** Epic description */
|
|
110
|
+
description: z.string().optional(),
|
|
111
|
+
/** Child issue IDs */
|
|
112
|
+
issues: z.array(z.string()).default([]),
|
|
113
|
+
/** Progress (0-100) */
|
|
114
|
+
progress: z.number().min(0).max(100).default(0),
|
|
115
|
+
/** Created timestamp */
|
|
116
|
+
createdAt: z.string(),
|
|
117
|
+
/** Updated timestamp */
|
|
118
|
+
updatedAt: z.string(),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
export type Epic = z.infer<typeof EpicSchema>
|
|
122
|
+
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// Issue Filters
|
|
125
|
+
// =============================================================================
|
|
126
|
+
|
|
127
|
+
export const IssueFiltersSchema = z.object({
|
|
128
|
+
/** Status filter */
|
|
129
|
+
status: z.array(IssueStatusSchema).optional(),
|
|
130
|
+
/** Type filter */
|
|
131
|
+
type: z.array(IssueTypeSchema).optional(),
|
|
132
|
+
/** Priority filter */
|
|
133
|
+
priority: z.array(IssuePrioritySchema).optional(),
|
|
134
|
+
/** Assignee filter */
|
|
135
|
+
assignee: z.string().optional(),
|
|
136
|
+
/** Label filter */
|
|
137
|
+
labels: z.array(z.string()).optional(),
|
|
138
|
+
/** Epic filter */
|
|
139
|
+
epic: z.string().optional(),
|
|
140
|
+
/** Search text */
|
|
141
|
+
search: z.string().optional(),
|
|
142
|
+
/** Only show blocked issues */
|
|
143
|
+
blocked: z.boolean().optional(),
|
|
144
|
+
/** Only show ready issues (no blockers) */
|
|
145
|
+
ready: z.boolean().optional(),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
export type IssueFilters = z.infer<typeof IssueFiltersSchema>
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// Issue Sort
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
export const IssueSortSchema = z.object({
|
|
155
|
+
field: z.enum(['priority', 'createdAt', 'updatedAt', 'closedAt', 'title', 'status']),
|
|
156
|
+
direction: z.enum(['asc', 'desc']),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
export type IssueSort = z.infer<typeof IssueSortSchema>
|
|
160
|
+
|
|
161
|
+
// =============================================================================
|
|
162
|
+
// Board Column Configuration
|
|
163
|
+
// =============================================================================
|
|
164
|
+
|
|
165
|
+
export const BoardColumnSchema = z.object({
|
|
166
|
+
/** Column ID */
|
|
167
|
+
id: z.string(),
|
|
168
|
+
/** Column title */
|
|
169
|
+
title: z.string(),
|
|
170
|
+
/** Status values for this column */
|
|
171
|
+
statuses: z.array(IssueStatusSchema),
|
|
172
|
+
/** Column color */
|
|
173
|
+
color: z.string().optional(),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
export type BoardColumn = z.infer<typeof BoardColumnSchema>
|
|
177
|
+
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// Default board columns (matches beads-ui)
|
|
180
|
+
// =============================================================================
|
|
181
|
+
|
|
182
|
+
export const defaultBoardColumns: BoardColumn[] = [
|
|
183
|
+
{ id: 'blocked', title: 'Blocked', statuses: ['blocked'], color: 'red' },
|
|
184
|
+
{ id: 'ready', title: 'Ready', statuses: ['open'], color: 'blue' },
|
|
185
|
+
{ id: 'in_progress', title: 'In Progress', statuses: ['in_progress'], color: 'yellow' },
|
|
186
|
+
{ id: 'closed', title: 'Closed', statuses: ['closed'], color: 'green' },
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// Closed Time Range Filter
|
|
191
|
+
// =============================================================================
|
|
192
|
+
|
|
193
|
+
export const ClosedTimeRangeSchema = z.enum(['today', 'last3days', 'last7days', 'all'])
|
|
194
|
+
export type ClosedTimeRange = z.infer<typeof ClosedTimeRangeSchema>
|