@miketromba/issy-app 0.1.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/README.md +17 -0
- package/bunfig.toml +2 -0
- package/package.json +46 -0
- package/src/App.tsx +332 -0
- package/src/components/Badge.tsx +49 -0
- package/src/components/ConfirmModal.tsx +71 -0
- package/src/components/CreateIssueModal.tsx +176 -0
- package/src/components/EditIssueModal.tsx +183 -0
- package/src/components/FilterBar.tsx +182 -0
- package/src/components/IssueDetail.tsx +148 -0
- package/src/components/IssueList.tsx +75 -0
- package/src/components/QueryHelpModal.tsx +160 -0
- package/src/frontend.tsx +18 -0
- package/src/index.css +99 -0
- package/src/index.html +12 -0
- package/src/index.ts +164 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
interface QueryHelpModalProps {
|
|
2
|
+
isOpen: boolean
|
|
3
|
+
onClose: () => void
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function QueryHelpModal({ isOpen, onClose }: QueryHelpModalProps) {
|
|
7
|
+
if (!isOpen) return null
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className="fixed inset-0 bg-black/70 flex items-center justify-center z-[1000] p-5"
|
|
12
|
+
onClick={onClose}
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
className="bg-surface border border-border rounded-xl max-w-[600px] w-full max-h-[65vh] overflow-y-auto shadow-2xl custom-scrollbar"
|
|
16
|
+
onClick={e => e.stopPropagation()}
|
|
17
|
+
>
|
|
18
|
+
<div className="flex items-center justify-between px-6 py-5 border-b border-border">
|
|
19
|
+
<h2 className="text-lg font-semibold text-text-primary">
|
|
20
|
+
Query Syntax Help
|
|
21
|
+
</h2>
|
|
22
|
+
<button
|
|
23
|
+
onClick={onClose}
|
|
24
|
+
aria-label="Close"
|
|
25
|
+
className="w-8 h-8 flex items-center justify-center bg-transparent border-0 rounded-md text-text-muted text-2xl cursor-pointer transition-all hover:bg-surface-elevated hover:text-text-primary"
|
|
26
|
+
>
|
|
27
|
+
×
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div className="p-6">
|
|
32
|
+
<p className="text-text-secondary mb-6 leading-relaxed">
|
|
33
|
+
Use qualifiers to filter issues, or type freely to
|
|
34
|
+
search by text.
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<section className="mb-6">
|
|
38
|
+
<h3 className="text-[13px] font-semibold text-text-muted uppercase tracking-wide mb-3">
|
|
39
|
+
Qualifiers
|
|
40
|
+
</h3>
|
|
41
|
+
<div className="flex flex-col gap-2">
|
|
42
|
+
<QualifierRow
|
|
43
|
+
qualifier="is:"
|
|
44
|
+
description="Filter by status"
|
|
45
|
+
values="open, closed"
|
|
46
|
+
/>
|
|
47
|
+
<QualifierRow
|
|
48
|
+
qualifier="priority:"
|
|
49
|
+
description="Filter by priority"
|
|
50
|
+
values="high, medium, low"
|
|
51
|
+
/>
|
|
52
|
+
<QualifierRow
|
|
53
|
+
qualifier="type:"
|
|
54
|
+
description="Filter by issue type"
|
|
55
|
+
values="bug, feature, task, etc."
|
|
56
|
+
/>
|
|
57
|
+
<QualifierRow
|
|
58
|
+
qualifier="label:"
|
|
59
|
+
description="Filter by label"
|
|
60
|
+
values="any label name"
|
|
61
|
+
/>
|
|
62
|
+
<QualifierRow
|
|
63
|
+
qualifier="sort:"
|
|
64
|
+
description="Sort results"
|
|
65
|
+
values="created, priority, title"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<section className="mb-6">
|
|
71
|
+
<h3 className="text-[13px] font-semibold text-text-muted uppercase tracking-wide mb-3">
|
|
72
|
+
Examples
|
|
73
|
+
</h3>
|
|
74
|
+
<div className="flex flex-col gap-2">
|
|
75
|
+
<ExampleRow
|
|
76
|
+
code="is:open priority:high"
|
|
77
|
+
description="High priority open issues"
|
|
78
|
+
/>
|
|
79
|
+
<ExampleRow
|
|
80
|
+
code="type:bug dashboard"
|
|
81
|
+
description='Bugs mentioning "dashboard"'
|
|
82
|
+
/>
|
|
83
|
+
<ExampleRow
|
|
84
|
+
code="is:open sort:priority"
|
|
85
|
+
description="Open issues sorted by priority"
|
|
86
|
+
/>
|
|
87
|
+
<ExampleRow
|
|
88
|
+
code="kubernetes cluster"
|
|
89
|
+
description='Issues matching "kubernetes cluster"'
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<section>
|
|
95
|
+
<h3 className="text-[13px] font-semibold text-text-muted uppercase tracking-wide mb-3">
|
|
96
|
+
Tips
|
|
97
|
+
</h3>
|
|
98
|
+
<ul className="list-none p-0 m-0 flex flex-col gap-2">
|
|
99
|
+
<li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
|
|
100
|
+
Combine multiple qualifiers to narrow results
|
|
101
|
+
</li>
|
|
102
|
+
<li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
|
|
103
|
+
Free text searches titles, descriptions, and
|
|
104
|
+
content
|
|
105
|
+
</li>
|
|
106
|
+
<li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
|
|
107
|
+
Use quotes for multi-word searches:{' '}
|
|
108
|
+
<code className="bg-surface-elevated px-1.5 py-0.5 rounded text-xs font-mono">
|
|
109
|
+
"api error"
|
|
110
|
+
</code>
|
|
111
|
+
</li>
|
|
112
|
+
<li className="text-text-secondary text-[13px] pl-4 relative before:content-['•'] before:absolute before:left-0 before:text-text-muted">
|
|
113
|
+
Qualifiers are case-insensitive
|
|
114
|
+
</li>
|
|
115
|
+
</ul>
|
|
116
|
+
</section>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function QualifierRow({
|
|
124
|
+
qualifier,
|
|
125
|
+
description,
|
|
126
|
+
values
|
|
127
|
+
}: {
|
|
128
|
+
qualifier: string
|
|
129
|
+
description: string
|
|
130
|
+
values: string
|
|
131
|
+
}) {
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex gap-3 px-3 py-2.5 bg-surface-elevated rounded-md">
|
|
134
|
+
<code className="font-mono text-[13px] text-accent shrink-0 min-w-[80px]">
|
|
135
|
+
{qualifier}
|
|
136
|
+
</code>
|
|
137
|
+
<span className="text-text-secondary text-[13px] flex flex-col gap-0.5">
|
|
138
|
+
{description}
|
|
139
|
+
<span className="text-text-muted text-xs">{values}</span>
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function ExampleRow({
|
|
146
|
+
code,
|
|
147
|
+
description
|
|
148
|
+
}: {
|
|
149
|
+
code: string
|
|
150
|
+
description: string
|
|
151
|
+
}) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="flex flex-col gap-1 px-3 py-2.5 bg-surface-elevated rounded-md">
|
|
154
|
+
<code className="font-mono text-[13px] text-text-primary">
|
|
155
|
+
{code}
|
|
156
|
+
</code>
|
|
157
|
+
<span className="text-text-muted text-xs">{description}</span>
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
package/src/frontend.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issy Frontend Entry Point
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createRoot } from 'react-dom/client'
|
|
6
|
+
import { App } from './App'
|
|
7
|
+
import './index.css'
|
|
8
|
+
|
|
9
|
+
function start() {
|
|
10
|
+
const root = createRoot(document.getElementById('root')!)
|
|
11
|
+
root.render(<App />)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (document.readyState === 'loading') {
|
|
15
|
+
document.addEventListener('DOMContentLoaded', start)
|
|
16
|
+
} else {
|
|
17
|
+
start()
|
|
18
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@plugin "@tailwindcss/typography";
|
|
3
|
+
|
|
4
|
+
@theme {
|
|
5
|
+
/* Core palette - dark theme */
|
|
6
|
+
--color-background: #0a0b0f;
|
|
7
|
+
--color-surface: #12141a;
|
|
8
|
+
--color-surface-elevated: #1a1c24;
|
|
9
|
+
--color-border: #2a2d38;
|
|
10
|
+
--color-border-subtle: #1e2028;
|
|
11
|
+
|
|
12
|
+
/* Text hierarchy */
|
|
13
|
+
--color-text-primary: #f4f4f5;
|
|
14
|
+
--color-text-secondary: #a1a1aa;
|
|
15
|
+
--color-text-muted: #71717a;
|
|
16
|
+
|
|
17
|
+
/* Accent - warm coral */
|
|
18
|
+
--color-accent: #ff6b5b;
|
|
19
|
+
--color-accent-hover: #ff8577;
|
|
20
|
+
|
|
21
|
+
/* Status colors */
|
|
22
|
+
--color-status-open: #22c55e;
|
|
23
|
+
--color-status-closed: #6b7280;
|
|
24
|
+
|
|
25
|
+
/* Priority colors */
|
|
26
|
+
--color-priority-high: #ef4444;
|
|
27
|
+
--color-priority-medium: #f59e0b;
|
|
28
|
+
--color-priority-low: #22c55e;
|
|
29
|
+
|
|
30
|
+
/* Type colors */
|
|
31
|
+
--color-type-default: #a78bfa;
|
|
32
|
+
--color-type-bug: #f87171;
|
|
33
|
+
--color-type-feature: #60a5fa;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Custom scrollbar */
|
|
37
|
+
.custom-scrollbar::-webkit-scrollbar {
|
|
38
|
+
width: 6px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.custom-scrollbar::-webkit-scrollbar-track {
|
|
42
|
+
background: var(--color-background);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
46
|
+
background: var(--color-border);
|
|
47
|
+
border-radius: 3px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Code block styling - remove borders, unify background, add dark scrollbar */
|
|
51
|
+
.prose pre {
|
|
52
|
+
border: none !important;
|
|
53
|
+
box-shadow: none !important;
|
|
54
|
+
background-color: var(--color-surface-elevated) !important;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.prose pre code.hljs {
|
|
58
|
+
background: transparent !important;
|
|
59
|
+
padding: 0 !important;
|
|
60
|
+
font-size: 13px !important;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.prose pre::-webkit-scrollbar {
|
|
64
|
+
height: 6px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.prose pre::-webkit-scrollbar-track {
|
|
68
|
+
background: transparent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.prose pre::-webkit-scrollbar-thumb {
|
|
72
|
+
background: var(--color-border);
|
|
73
|
+
border-radius: 3px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.prose pre::-webkit-scrollbar-thumb:hover {
|
|
77
|
+
background: var(--color-text-muted);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Firefox scrollbar */
|
|
81
|
+
.prose pre {
|
|
82
|
+
scrollbar-width: thin;
|
|
83
|
+
scrollbar-color: var(--color-border) transparent;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Line clamp utility for title */
|
|
87
|
+
.line-clamp-2 {
|
|
88
|
+
display: -webkit-box;
|
|
89
|
+
-webkit-line-clamp: 2;
|
|
90
|
+
-webkit-box-orient: vertical;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.line-clamp-1 {
|
|
95
|
+
display: -webkit-box;
|
|
96
|
+
-webkit-line-clamp: 1;
|
|
97
|
+
-webkit-box-orient: vertical;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
package/src/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>issy</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issy API Server
|
|
3
|
+
*
|
|
4
|
+
* Provides REST API endpoints for the issue tracking system.
|
|
5
|
+
* Uses the shared library for all issue operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { serve } from "bun";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import index from "./index.html";
|
|
11
|
+
|
|
12
|
+
// Import shared library
|
|
13
|
+
import {
|
|
14
|
+
setIssuesDir,
|
|
15
|
+
getAllIssues,
|
|
16
|
+
getIssue,
|
|
17
|
+
createIssue,
|
|
18
|
+
updateIssue,
|
|
19
|
+
closeIssue,
|
|
20
|
+
reopenIssue,
|
|
21
|
+
deleteIssue,
|
|
22
|
+
filterAndSearchIssues,
|
|
23
|
+
filterByQuery,
|
|
24
|
+
type CreateIssueInput,
|
|
25
|
+
type UpdateIssueInput,
|
|
26
|
+
} from "@miketromba/issy-core";
|
|
27
|
+
|
|
28
|
+
// Initialize issues directory from env or current working directory
|
|
29
|
+
const DEFAULT_ROOT = process.env.ISSUES_ROOT || process.cwd();
|
|
30
|
+
const ISSUES_DIR = process.env.ISSUES_DIR || resolve(DEFAULT_ROOT, ".issues");
|
|
31
|
+
setIssuesDir(ISSUES_DIR);
|
|
32
|
+
|
|
33
|
+
const PORT = Number(process.env.ISSUES_PORT || process.env.PORT || 1554);
|
|
34
|
+
|
|
35
|
+
const server = serve({
|
|
36
|
+
port: PORT,
|
|
37
|
+
routes: {
|
|
38
|
+
// API: List all issues with optional filtering and search
|
|
39
|
+
// Supports both legacy filters (status, priority, type, search) and
|
|
40
|
+
// new query language via 'q' parameter (e.g., q=is:open priority:high)
|
|
41
|
+
"/api/issues": {
|
|
42
|
+
GET: async (req) => {
|
|
43
|
+
const url = new URL(req.url);
|
|
44
|
+
const allIssues = await getAllIssues();
|
|
45
|
+
|
|
46
|
+
// New query language support via 'q' parameter
|
|
47
|
+
const query = url.searchParams.get("q");
|
|
48
|
+
if (query) {
|
|
49
|
+
return Response.json(filterByQuery(allIssues, query));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Legacy filter parameters
|
|
53
|
+
const status = url.searchParams.get("status") || undefined;
|
|
54
|
+
const priority = url.searchParams.get("priority") || undefined;
|
|
55
|
+
const type = url.searchParams.get("type") || undefined;
|
|
56
|
+
const search = url.searchParams.get("search") || undefined;
|
|
57
|
+
|
|
58
|
+
// If any legacy filters are provided, apply them
|
|
59
|
+
if (status || priority || type || search) {
|
|
60
|
+
const filtered = filterAndSearchIssues(allIssues, {
|
|
61
|
+
status,
|
|
62
|
+
priority,
|
|
63
|
+
type,
|
|
64
|
+
search,
|
|
65
|
+
});
|
|
66
|
+
return Response.json(filtered);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Response.json(allIssues);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// API: Get single issue by ID
|
|
74
|
+
"/api/issues/:id": {
|
|
75
|
+
GET: async (req) => {
|
|
76
|
+
const issue = await getIssue(req.params.id);
|
|
77
|
+
if (!issue) {
|
|
78
|
+
return Response.json({ error: "Issue not found" }, { status: 404 });
|
|
79
|
+
}
|
|
80
|
+
return Response.json(issue);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Update an issue
|
|
84
|
+
PATCH: async (req) => {
|
|
85
|
+
try {
|
|
86
|
+
const input: UpdateIssueInput = await req.json();
|
|
87
|
+
const issue = await updateIssue(req.params.id, input);
|
|
88
|
+
return Response.json(issue);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
const message = e instanceof Error ? e.message : "Unknown error";
|
|
91
|
+
return Response.json({ error: message }, { status: 400 });
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// API: Create a new issue
|
|
97
|
+
"/api/issues/create": {
|
|
98
|
+
POST: async (req) => {
|
|
99
|
+
try {
|
|
100
|
+
const input: CreateIssueInput = await req.json();
|
|
101
|
+
const issue = await createIssue(input);
|
|
102
|
+
return Response.json(issue, { status: 201 });
|
|
103
|
+
} catch (e) {
|
|
104
|
+
const message = e instanceof Error ? e.message : "Unknown error";
|
|
105
|
+
return Response.json({ error: message }, { status: 400 });
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// API: Close an issue
|
|
111
|
+
"/api/issues/:id/close": {
|
|
112
|
+
POST: async (req) => {
|
|
113
|
+
try {
|
|
114
|
+
const issue = await closeIssue(req.params.id);
|
|
115
|
+
return Response.json(issue);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
const message = e instanceof Error ? e.message : "Unknown error";
|
|
118
|
+
return Response.json({ error: message }, { status: 400 });
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// API: Reopen an issue
|
|
124
|
+
"/api/issues/:id/reopen": {
|
|
125
|
+
POST: async (req) => {
|
|
126
|
+
try {
|
|
127
|
+
const issue = await reopenIssue(req.params.id);
|
|
128
|
+
return Response.json(issue);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
const message = e instanceof Error ? e.message : "Unknown error";
|
|
131
|
+
return Response.json({ error: message }, { status: 400 });
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// API: Delete an issue
|
|
137
|
+
"/api/issues/:id/delete": {
|
|
138
|
+
DELETE: async (req) => {
|
|
139
|
+
try {
|
|
140
|
+
await deleteIssue(req.params.id);
|
|
141
|
+
return Response.json({ success: true });
|
|
142
|
+
} catch (e) {
|
|
143
|
+
const message = e instanceof Error ? e.message : "Unknown error";
|
|
144
|
+
return Response.json({ error: message }, { status: 400 });
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// API: Health check
|
|
150
|
+
"/api/health": {
|
|
151
|
+
GET: () => Response.json({ status: "ok", service: "issy" }),
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Serve frontend for everything else
|
|
155
|
+
"/*": index,
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
development: process.env.NODE_ENV !== "production" && {
|
|
159
|
+
hmr: true,
|
|
160
|
+
console: true,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
console.log(`📋 issy running at ${server.url}`);
|