@miketromba/issy-app 0.1.1 → 0.1.4
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/app.js +304 -0
- package/dist/index.html +13 -0
- package/dist/server.js +1899 -0
- package/{src/index.css → dist/styles.css} +1 -2
- package/package.json +18 -13
- package/bunfig.toml +0 -2
- package/src/App.tsx +0 -346
- package/src/components/Badge.tsx +0 -51
- package/src/components/ConfirmModal.tsx +0 -72
- package/src/components/CreateIssueModal.tsx +0 -201
- package/src/components/EditIssueModal.tsx +0 -215
- package/src/components/FilterBar.tsx +0 -209
- package/src/components/IssueDetail.tsx +0 -184
- package/src/components/IssueList.tsx +0 -73
- package/src/components/QueryHelpModal.tsx +0 -156
- package/src/frontend.tsx +0 -20
- package/src/index.html +0 -12
- package/src/index.ts +0 -163
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import type React from 'react'
|
|
2
|
-
import { useState } from 'react'
|
|
3
|
-
|
|
4
|
-
interface CreateIssueModalProps {
|
|
5
|
-
isOpen: boolean
|
|
6
|
-
onClose: () => void
|
|
7
|
-
onCreated: () => void
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function CreateIssueModal({
|
|
11
|
-
isOpen,
|
|
12
|
-
onClose,
|
|
13
|
-
onCreated,
|
|
14
|
-
}: CreateIssueModalProps) {
|
|
15
|
-
const [title, setTitle] = useState('')
|
|
16
|
-
const [description, setDescription] = useState('')
|
|
17
|
-
const [type, setType] = useState<'bug' | 'improvement'>('improvement')
|
|
18
|
-
const [priority, setPriority] = useState<'high' | 'medium' | 'low'>('medium')
|
|
19
|
-
const [labels, setLabels] = useState('')
|
|
20
|
-
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
21
|
-
const [error, setError] = useState<string | null>(null)
|
|
22
|
-
|
|
23
|
-
if (!isOpen) return null
|
|
24
|
-
|
|
25
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
26
|
-
e.preventDefault()
|
|
27
|
-
|
|
28
|
-
if (!title.trim()) {
|
|
29
|
-
setError('Title is required')
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
setIsSubmitting(true)
|
|
34
|
-
setError(null)
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const response = await fetch('/api/issues/create', {
|
|
38
|
-
method: 'POST',
|
|
39
|
-
headers: { 'Content-Type': 'application/json' },
|
|
40
|
-
body: JSON.stringify({
|
|
41
|
-
title: title.trim(),
|
|
42
|
-
description: description.trim() || title.trim(),
|
|
43
|
-
type,
|
|
44
|
-
priority,
|
|
45
|
-
labels: labels.trim() || undefined,
|
|
46
|
-
}),
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
if (!response.ok) {
|
|
50
|
-
const data = await response.json()
|
|
51
|
-
throw new Error(data.error || 'Failed to create issue')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Reset form and close
|
|
55
|
-
setTitle('')
|
|
56
|
-
setDescription('')
|
|
57
|
-
setType('improvement')
|
|
58
|
-
setPriority('medium')
|
|
59
|
-
setLabels('')
|
|
60
|
-
onCreated()
|
|
61
|
-
onClose()
|
|
62
|
-
} catch (e) {
|
|
63
|
-
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
64
|
-
} finally {
|
|
65
|
-
setIsSubmitting(false)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
70
|
-
if (e.target === e.currentTarget) {
|
|
71
|
-
onClose()
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div
|
|
77
|
-
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
|
|
78
|
-
onClick={handleBackdropClick}
|
|
79
|
-
>
|
|
80
|
-
<div className="bg-surface-elevated border border-border rounded-xl w-full max-w-lg shadow-2xl">
|
|
81
|
-
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
|
82
|
-
<h2 className="text-lg font-semibold text-text-primary">New Issue</h2>
|
|
83
|
-
<button
|
|
84
|
-
onClick={onClose}
|
|
85
|
-
className="text-text-muted hover:text-text-primary transition-colors"
|
|
86
|
-
>
|
|
87
|
-
<svg
|
|
88
|
-
width="20"
|
|
89
|
-
height="20"
|
|
90
|
-
viewBox="0 0 24 24"
|
|
91
|
-
fill="none"
|
|
92
|
-
stroke="currentColor"
|
|
93
|
-
strokeWidth="2"
|
|
94
|
-
>
|
|
95
|
-
<path d="M18 6L6 18M6 6l12 12" />
|
|
96
|
-
</svg>
|
|
97
|
-
</button>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
<form onSubmit={handleSubmit} className="p-5 space-y-4">
|
|
101
|
-
{error && (
|
|
102
|
-
<div className="px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
|
103
|
-
{error}
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
<div>
|
|
108
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
109
|
-
Title *
|
|
110
|
-
</label>
|
|
111
|
-
<input
|
|
112
|
-
type="text"
|
|
113
|
-
value={title}
|
|
114
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
115
|
-
placeholder="Brief description of the issue"
|
|
116
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
|
|
117
|
-
/>
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<div>
|
|
121
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
122
|
-
Description
|
|
123
|
-
</label>
|
|
124
|
-
<input
|
|
125
|
-
type="text"
|
|
126
|
-
value={description}
|
|
127
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
128
|
-
placeholder="One-line summary (defaults to title)"
|
|
129
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
|
|
130
|
-
/>
|
|
131
|
-
</div>
|
|
132
|
-
|
|
133
|
-
<div className="grid grid-cols-2 gap-4">
|
|
134
|
-
<div>
|
|
135
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
136
|
-
Type
|
|
137
|
-
</label>
|
|
138
|
-
<select
|
|
139
|
-
value={type}
|
|
140
|
-
onChange={(e) =>
|
|
141
|
-
setType(e.target.value as 'bug' | 'improvement')
|
|
142
|
-
}
|
|
143
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm focus:outline-none focus:border-accent"
|
|
144
|
-
>
|
|
145
|
-
<option value="improvement">Improvement</option>
|
|
146
|
-
<option value="bug">Bug</option>
|
|
147
|
-
</select>
|
|
148
|
-
</div>
|
|
149
|
-
|
|
150
|
-
<div>
|
|
151
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
152
|
-
Priority
|
|
153
|
-
</label>
|
|
154
|
-
<select
|
|
155
|
-
value={priority}
|
|
156
|
-
onChange={(e) =>
|
|
157
|
-
setPriority(e.target.value as 'high' | 'medium' | 'low')
|
|
158
|
-
}
|
|
159
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm focus:outline-none focus:border-accent"
|
|
160
|
-
>
|
|
161
|
-
<option value="high">High</option>
|
|
162
|
-
<option value="medium">Medium</option>
|
|
163
|
-
<option value="low">Low</option>
|
|
164
|
-
</select>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
<div>
|
|
169
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
170
|
-
Labels
|
|
171
|
-
</label>
|
|
172
|
-
<input
|
|
173
|
-
type="text"
|
|
174
|
-
value={labels}
|
|
175
|
-
onChange={(e) => setLabels(e.target.value)}
|
|
176
|
-
placeholder="Comma-separated: ui, backend, api"
|
|
177
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
|
|
178
|
-
/>
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
<div className="flex justify-end gap-3 pt-2">
|
|
182
|
-
<button
|
|
183
|
-
type="button"
|
|
184
|
-
onClick={onClose}
|
|
185
|
-
className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary transition-colors"
|
|
186
|
-
>
|
|
187
|
-
Cancel
|
|
188
|
-
</button>
|
|
189
|
-
<button
|
|
190
|
-
type="submit"
|
|
191
|
-
disabled={isSubmitting}
|
|
192
|
-
className="px-4 py-2 bg-accent hover:bg-accent-hover text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
|
193
|
-
>
|
|
194
|
-
{isSubmitting ? 'Creating...' : 'Create Issue'}
|
|
195
|
-
</button>
|
|
196
|
-
</div>
|
|
197
|
-
</form>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
)
|
|
201
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import type React from 'react'
|
|
2
|
-
import { useEffect, useState } from 'react'
|
|
3
|
-
import type { Issue } from '../App'
|
|
4
|
-
|
|
5
|
-
interface EditIssueModalProps {
|
|
6
|
-
issue: Issue | null
|
|
7
|
-
isOpen: boolean
|
|
8
|
-
onClose: () => void
|
|
9
|
-
onUpdated: () => void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function EditIssueModal({
|
|
13
|
-
issue,
|
|
14
|
-
isOpen,
|
|
15
|
-
onClose,
|
|
16
|
-
onUpdated,
|
|
17
|
-
}: EditIssueModalProps) {
|
|
18
|
-
const [title, setTitle] = useState('')
|
|
19
|
-
const [description, setDescription] = useState('')
|
|
20
|
-
const [type, setType] = useState<'bug' | 'improvement'>('improvement')
|
|
21
|
-
const [priority, setPriority] = useState<'high' | 'medium' | 'low'>('medium')
|
|
22
|
-
const [labels, setLabels] = useState('')
|
|
23
|
-
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
24
|
-
const [error, setError] = useState<string | null>(null)
|
|
25
|
-
|
|
26
|
-
// Populate form when issue changes
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
if (issue) {
|
|
29
|
-
setTitle(issue.frontmatter.title || '')
|
|
30
|
-
setDescription(issue.frontmatter.description || '')
|
|
31
|
-
setType(
|
|
32
|
-
(issue.frontmatter.type as 'bug' | 'improvement') || 'improvement',
|
|
33
|
-
)
|
|
34
|
-
setPriority(
|
|
35
|
-
(issue.frontmatter.priority as 'high' | 'medium' | 'low') || 'medium',
|
|
36
|
-
)
|
|
37
|
-
setLabels(issue.frontmatter.labels || '')
|
|
38
|
-
}
|
|
39
|
-
}, [issue])
|
|
40
|
-
|
|
41
|
-
if (!isOpen || !issue) return null
|
|
42
|
-
|
|
43
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
44
|
-
e.preventDefault()
|
|
45
|
-
|
|
46
|
-
if (!title.trim()) {
|
|
47
|
-
setError('Title is required')
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
setIsSubmitting(true)
|
|
52
|
-
setError(null)
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const response = await fetch(`/api/issues/${issue.id}`, {
|
|
56
|
-
method: 'PATCH',
|
|
57
|
-
headers: { 'Content-Type': 'application/json' },
|
|
58
|
-
body: JSON.stringify({
|
|
59
|
-
title: title.trim(),
|
|
60
|
-
description: description.trim() || title.trim(),
|
|
61
|
-
type,
|
|
62
|
-
priority,
|
|
63
|
-
labels: labels.trim() || undefined,
|
|
64
|
-
}),
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
const data = await response.json()
|
|
69
|
-
throw new Error(data.error || 'Failed to update issue')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
onUpdated()
|
|
73
|
-
onClose()
|
|
74
|
-
} catch (e) {
|
|
75
|
-
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
76
|
-
} finally {
|
|
77
|
-
setIsSubmitting(false)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
82
|
-
if (e.target === e.currentTarget) {
|
|
83
|
-
onClose()
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div
|
|
89
|
-
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
|
|
90
|
-
onClick={handleBackdropClick}
|
|
91
|
-
>
|
|
92
|
-
<div className="bg-surface-elevated border border-border rounded-xl w-full max-w-lg shadow-2xl">
|
|
93
|
-
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
|
94
|
-
<h2 className="text-lg font-semibold text-text-primary">
|
|
95
|
-
Edit Issue #{issue.id}
|
|
96
|
-
</h2>
|
|
97
|
-
<button
|
|
98
|
-
onClick={onClose}
|
|
99
|
-
className="text-text-muted hover:text-text-primary transition-colors"
|
|
100
|
-
>
|
|
101
|
-
<svg
|
|
102
|
-
width="20"
|
|
103
|
-
height="20"
|
|
104
|
-
viewBox="0 0 24 24"
|
|
105
|
-
fill="none"
|
|
106
|
-
stroke="currentColor"
|
|
107
|
-
strokeWidth="2"
|
|
108
|
-
>
|
|
109
|
-
<path d="M18 6L6 18M6 6l12 12" />
|
|
110
|
-
</svg>
|
|
111
|
-
</button>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
<form onSubmit={handleSubmit} className="p-5 space-y-4">
|
|
115
|
-
{error && (
|
|
116
|
-
<div className="px-3 py-2 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
|
117
|
-
{error}
|
|
118
|
-
</div>
|
|
119
|
-
)}
|
|
120
|
-
|
|
121
|
-
<div>
|
|
122
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
123
|
-
Title *
|
|
124
|
-
</label>
|
|
125
|
-
<input
|
|
126
|
-
type="text"
|
|
127
|
-
value={title}
|
|
128
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
129
|
-
placeholder="Brief description of the issue"
|
|
130
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
<div>
|
|
135
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
136
|
-
Description
|
|
137
|
-
</label>
|
|
138
|
-
<input
|
|
139
|
-
type="text"
|
|
140
|
-
value={description}
|
|
141
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
142
|
-
placeholder="One-line summary"
|
|
143
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
|
|
144
|
-
/>
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
<div className="grid grid-cols-2 gap-4">
|
|
148
|
-
<div>
|
|
149
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
150
|
-
Type
|
|
151
|
-
</label>
|
|
152
|
-
<select
|
|
153
|
-
value={type}
|
|
154
|
-
onChange={(e) =>
|
|
155
|
-
setType(e.target.value as 'bug' | 'improvement')
|
|
156
|
-
}
|
|
157
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm focus:outline-none focus:border-accent"
|
|
158
|
-
>
|
|
159
|
-
<option value="improvement">Improvement</option>
|
|
160
|
-
<option value="bug">Bug</option>
|
|
161
|
-
</select>
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
<div>
|
|
165
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
166
|
-
Priority
|
|
167
|
-
</label>
|
|
168
|
-
<select
|
|
169
|
-
value={priority}
|
|
170
|
-
onChange={(e) =>
|
|
171
|
-
setPriority(e.target.value as 'high' | 'medium' | 'low')
|
|
172
|
-
}
|
|
173
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm focus:outline-none focus:border-accent"
|
|
174
|
-
>
|
|
175
|
-
<option value="high">High</option>
|
|
176
|
-
<option value="medium">Medium</option>
|
|
177
|
-
<option value="low">Low</option>
|
|
178
|
-
</select>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
<div>
|
|
183
|
-
<label className="block text-sm text-text-secondary mb-1.5">
|
|
184
|
-
Labels
|
|
185
|
-
</label>
|
|
186
|
-
<input
|
|
187
|
-
type="text"
|
|
188
|
-
value={labels}
|
|
189
|
-
onChange={(e) => setLabels(e.target.value)}
|
|
190
|
-
placeholder="Comma-separated: ui, backend, api"
|
|
191
|
-
className="w-full px-3 py-2 bg-surface border border-border rounded-lg text-text-primary text-sm placeholder:text-text-muted focus:outline-none focus:border-accent"
|
|
192
|
-
/>
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
<div className="flex justify-end gap-3 pt-2">
|
|
196
|
-
<button
|
|
197
|
-
type="button"
|
|
198
|
-
onClick={onClose}
|
|
199
|
-
className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary transition-colors"
|
|
200
|
-
>
|
|
201
|
-
Cancel
|
|
202
|
-
</button>
|
|
203
|
-
<button
|
|
204
|
-
type="submit"
|
|
205
|
-
disabled={isSubmitting}
|
|
206
|
-
className="px-4 py-2 bg-accent hover:bg-accent-hover text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
|
207
|
-
>
|
|
208
|
-
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
|
209
|
-
</button>
|
|
210
|
-
</div>
|
|
211
|
-
</form>
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
)
|
|
215
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { parseQuery } from '@miketromba/issy-core'
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
-
import type { Issue } from '../App'
|
|
4
|
-
|
|
5
|
-
interface FilterBarProps {
|
|
6
|
-
query: string
|
|
7
|
-
onQueryChange: (query: string) => void
|
|
8
|
-
issues: Issue[]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface DropdownProps {
|
|
12
|
-
label: string
|
|
13
|
-
value: string | null
|
|
14
|
-
options: { value: string; label: string }[]
|
|
15
|
-
onSelect: (value: string | null) => void
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function Dropdown({ label, value, options, onSelect }: DropdownProps) {
|
|
19
|
-
const [isOpen, setIsOpen] = useState(false)
|
|
20
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
21
|
-
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
function handleClickOutside(e: MouseEvent) {
|
|
24
|
-
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
25
|
-
setIsOpen(false)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
document.addEventListener('mousedown', handleClickOutside)
|
|
29
|
-
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
30
|
-
}, [])
|
|
31
|
-
|
|
32
|
-
const selectedOption = options.find((o) => o.value === value)
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<div ref={ref} className="relative">
|
|
36
|
-
<button
|
|
37
|
-
onClick={() => setIsOpen(!isOpen)}
|
|
38
|
-
className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded border transition-colors ${
|
|
39
|
-
value
|
|
40
|
-
? 'bg-accent/10 border-accent/30 text-accent'
|
|
41
|
-
: 'bg-transparent border-border text-text-muted hover:text-text-secondary hover:border-border'
|
|
42
|
-
}`}
|
|
43
|
-
>
|
|
44
|
-
<span>{selectedOption ? selectedOption.label : label}</span>
|
|
45
|
-
<svg
|
|
46
|
-
width="12"
|
|
47
|
-
height="12"
|
|
48
|
-
viewBox="0 0 24 24"
|
|
49
|
-
fill="none"
|
|
50
|
-
stroke="currentColor"
|
|
51
|
-
strokeWidth="2"
|
|
52
|
-
>
|
|
53
|
-
<path d="M6 9l6 6 6-6" />
|
|
54
|
-
</svg>
|
|
55
|
-
</button>
|
|
56
|
-
|
|
57
|
-
{isOpen && (
|
|
58
|
-
<div className="absolute top-full left-0 mt-1 min-w-[120px] bg-surface-elevated border border-border rounded-lg shadow-lg z-50 py-1">
|
|
59
|
-
{value && (
|
|
60
|
-
<>
|
|
61
|
-
<button
|
|
62
|
-
onClick={() => {
|
|
63
|
-
onSelect(null)
|
|
64
|
-
setIsOpen(false)
|
|
65
|
-
}}
|
|
66
|
-
className="w-full px-3 py-1.5 text-left text-xs text-text-muted hover:bg-surface hover:text-text-primary"
|
|
67
|
-
>
|
|
68
|
-
Clear
|
|
69
|
-
</button>
|
|
70
|
-
<div className="border-t border-border my-1" />
|
|
71
|
-
</>
|
|
72
|
-
)}
|
|
73
|
-
{options.map((option) => (
|
|
74
|
-
<button
|
|
75
|
-
key={option.value}
|
|
76
|
-
onClick={() => {
|
|
77
|
-
onSelect(option.value)
|
|
78
|
-
setIsOpen(false)
|
|
79
|
-
}}
|
|
80
|
-
className={`w-full px-3 py-1.5 text-left text-xs hover:bg-surface ${
|
|
81
|
-
value === option.value
|
|
82
|
-
? 'text-accent'
|
|
83
|
-
: 'text-text-secondary hover:text-text-primary'
|
|
84
|
-
}`}
|
|
85
|
-
>
|
|
86
|
-
{option.label}
|
|
87
|
-
</button>
|
|
88
|
-
))}
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Sort options are fixed since they're not data-dependent
|
|
96
|
-
const SORT_OPTIONS = [
|
|
97
|
-
{ value: 'created', label: 'Newest' },
|
|
98
|
-
{ value: 'created-asc', label: 'Oldest' },
|
|
99
|
-
{ value: 'priority', label: 'Priority' },
|
|
100
|
-
]
|
|
101
|
-
|
|
102
|
-
// Priority sort order for consistent display
|
|
103
|
-
const PRIORITY_ORDER: Record<string, number> = { high: 0, medium: 1, low: 2 }
|
|
104
|
-
|
|
105
|
-
function capitalize(str: string): string {
|
|
106
|
-
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function extractUniqueValues(
|
|
110
|
-
issues: Issue[],
|
|
111
|
-
key: keyof Issue['frontmatter'],
|
|
112
|
-
): { value: string; label: string }[] {
|
|
113
|
-
const values = new Set<string>()
|
|
114
|
-
|
|
115
|
-
for (const issue of issues) {
|
|
116
|
-
const val = issue.frontmatter[key]
|
|
117
|
-
if (val && typeof val === 'string') {
|
|
118
|
-
values.add(val.toLowerCase())
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return Array.from(values)
|
|
123
|
-
.sort((a, b) => {
|
|
124
|
-
// Special sort for priority
|
|
125
|
-
if (key === 'priority') {
|
|
126
|
-
return (PRIORITY_ORDER[a] ?? 99) - (PRIORITY_ORDER[b] ?? 99)
|
|
127
|
-
}
|
|
128
|
-
return a.localeCompare(b)
|
|
129
|
-
})
|
|
130
|
-
.map((v) => ({ value: v, label: capitalize(v) }))
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function FilterBar({ query, onQueryChange, issues }: FilterBarProps) {
|
|
134
|
-
const parsed = parseQuery(query)
|
|
135
|
-
|
|
136
|
-
// Dynamically extract options from actual issue data
|
|
137
|
-
const statusOptions = useMemo(
|
|
138
|
-
() => extractUniqueValues(issues, 'status'),
|
|
139
|
-
[issues],
|
|
140
|
-
)
|
|
141
|
-
const priorityOptions = useMemo(
|
|
142
|
-
() => extractUniqueValues(issues, 'priority'),
|
|
143
|
-
[issues],
|
|
144
|
-
)
|
|
145
|
-
const typeOptions = useMemo(
|
|
146
|
-
() => extractUniqueValues(issues, 'type'),
|
|
147
|
-
[issues],
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
const updateQualifier = (key: string, value: string | null) => {
|
|
151
|
-
const newQualifiers = { ...parsed.qualifiers }
|
|
152
|
-
|
|
153
|
-
if (value === null) {
|
|
154
|
-
delete newQualifiers[key]
|
|
155
|
-
} else {
|
|
156
|
-
newQualifiers[key] = value
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Rebuild query string
|
|
160
|
-
const parts: string[] = []
|
|
161
|
-
|
|
162
|
-
// Add qualifiers in consistent order
|
|
163
|
-
if (newQualifiers.is) parts.push(`is:${newQualifiers.is}`)
|
|
164
|
-
if (newQualifiers.priority) parts.push(`priority:${newQualifiers.priority}`)
|
|
165
|
-
if (newQualifiers.type) parts.push(`type:${newQualifiers.type}`)
|
|
166
|
-
if (newQualifiers.label) parts.push(`label:${newQualifiers.label}`)
|
|
167
|
-
if (newQualifiers.sort) parts.push(`sort:${newQualifiers.sort}`)
|
|
168
|
-
|
|
169
|
-
// Add search text at the end
|
|
170
|
-
if (parsed.searchText) parts.push(parsed.searchText)
|
|
171
|
-
|
|
172
|
-
onQueryChange(parts.join(' '))
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
177
|
-
{statusOptions.length > 0 && (
|
|
178
|
-
<Dropdown
|
|
179
|
-
label="Status"
|
|
180
|
-
value={parsed.qualifiers.is || null}
|
|
181
|
-
options={statusOptions}
|
|
182
|
-
onSelect={(v) => updateQualifier('is', v)}
|
|
183
|
-
/>
|
|
184
|
-
)}
|
|
185
|
-
{priorityOptions.length > 0 && (
|
|
186
|
-
<Dropdown
|
|
187
|
-
label="Priority"
|
|
188
|
-
value={parsed.qualifiers.priority || null}
|
|
189
|
-
options={priorityOptions}
|
|
190
|
-
onSelect={(v) => updateQualifier('priority', v)}
|
|
191
|
-
/>
|
|
192
|
-
)}
|
|
193
|
-
{typeOptions.length > 0 && (
|
|
194
|
-
<Dropdown
|
|
195
|
-
label="Type"
|
|
196
|
-
value={parsed.qualifiers.type || null}
|
|
197
|
-
options={typeOptions}
|
|
198
|
-
onSelect={(v) => updateQualifier('type', v)}
|
|
199
|
-
/>
|
|
200
|
-
)}
|
|
201
|
-
<Dropdown
|
|
202
|
-
label="Sort"
|
|
203
|
-
value={parsed.qualifiers.sort || null}
|
|
204
|
-
options={SORT_OPTIONS}
|
|
205
|
-
onSelect={(v) => updateQualifier('sort', v)}
|
|
206
|
-
/>
|
|
207
|
-
</div>
|
|
208
|
-
)
|
|
209
|
-
}
|