@miketromba/issy-core 0.1.1 → 0.1.3
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/lib/autocomplete.ts +153 -157
- package/src/lib/browser.ts +23 -0
package/package.json
CHANGED
package/src/lib/autocomplete.ts
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* A suggestion for autocomplete
|
|
10
10
|
*/
|
|
11
11
|
export interface Suggestion {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
/** The text to insert when selected */
|
|
13
|
+
text: string
|
|
14
|
+
/** The text to display in the dropdown */
|
|
15
|
+
displayText: string
|
|
16
|
+
/** Optional helper text describing the suggestion */
|
|
17
|
+
description?: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -26,11 +26,11 @@ const QUALIFIER_KEYS = ['is', 'priority', 'type', 'label', 'sort'] as const
|
|
|
26
26
|
* Valid values for each qualifier
|
|
27
27
|
*/
|
|
28
28
|
const QUALIFIER_VALUES: Record<string, readonly string[]> = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
is: ['open', 'closed'] as const,
|
|
30
|
+
priority: ['high', 'medium', 'low'] as const,
|
|
31
|
+
type: ['bug', 'improvement'] as const,
|
|
32
|
+
sort: ['priority', 'created', 'updated', 'id'] as const,
|
|
33
|
+
// label values are dynamic and provided via existingLabels parameter
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -50,185 +50,181 @@ const QUALIFIER_VALUES: Record<string, readonly string[]> = {
|
|
|
50
50
|
* // [{ text: "priority:", displayText: "priority:", description: "Filter by priority" }, ...]
|
|
51
51
|
*/
|
|
52
52
|
export function getQuerySuggestions(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
query: string,
|
|
54
|
+
cursorPosition?: number,
|
|
55
|
+
existingLabels?: string[],
|
|
56
56
|
): Suggestion[] {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return []
|
|
57
|
+
// Default cursor position to end of string
|
|
58
|
+
const cursor = cursorPosition ?? query.length
|
|
59
|
+
|
|
60
|
+
// Get the text up to the cursor
|
|
61
|
+
const textBeforeCursor = query.substring(0, cursor)
|
|
62
|
+
|
|
63
|
+
// Find the current token being typed
|
|
64
|
+
const { currentToken } = findCurrentToken(textBeforeCursor)
|
|
65
|
+
|
|
66
|
+
// If we're in the middle of a qualifier (key:value)
|
|
67
|
+
if (currentToken.includes(':')) {
|
|
68
|
+
const colonIndex = currentToken.indexOf(':')
|
|
69
|
+
const qualifierKey = currentToken.substring(0, colonIndex)
|
|
70
|
+
const partialValue = currentToken.substring(colonIndex + 1)
|
|
71
|
+
|
|
72
|
+
// Check if it's a supported qualifier
|
|
73
|
+
if ((QUALIFIER_KEYS as readonly string[]).includes(qualifierKey)) {
|
|
74
|
+
return getValueSuggestions(qualifierKey, partialValue, existingLabels)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if we're typing a qualifier key (with or without colon)
|
|
79
|
+
const partialQualifier = currentToken
|
|
80
|
+
if (partialQualifier && !partialQualifier.includes(':')) {
|
|
81
|
+
const matchingKeys = QUALIFIER_KEYS.filter((key) =>
|
|
82
|
+
key.startsWith(partialQualifier.toLowerCase()),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if (matchingKeys.length > 0) {
|
|
86
|
+
return matchingKeys.map((key) => ({
|
|
87
|
+
text: `${key}:`,
|
|
88
|
+
displayText: `${key}:`,
|
|
89
|
+
description: getQualifierDescription(key),
|
|
90
|
+
}))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If we're at the start of a new token (space or start of string)
|
|
95
|
+
// and the previous token doesn't end with a colon, suggest qualifier keys
|
|
96
|
+
if (currentToken === '' || currentToken.trim() === '') {
|
|
97
|
+
const previousToken = getPreviousToken(textBeforeCursor)
|
|
98
|
+
if (!previousToken || !previousToken.endsWith(':')) {
|
|
99
|
+
return QUALIFIER_KEYS.map((key) => ({
|
|
100
|
+
text: `${key}:`,
|
|
101
|
+
displayText: `${key}:`,
|
|
102
|
+
description: getQualifierDescription(key),
|
|
103
|
+
}))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return []
|
|
112
108
|
}
|
|
113
109
|
|
|
114
110
|
/**
|
|
115
111
|
* Get suggestions for a qualifier value
|
|
116
112
|
*/
|
|
117
113
|
function getValueSuggestions(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
qualifierKey: string,
|
|
115
|
+
partialValue: string,
|
|
116
|
+
existingLabels?: string[],
|
|
121
117
|
): Suggestion[] {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
118
|
+
const _suggestions: Suggestion[] = []
|
|
119
|
+
|
|
120
|
+
if (qualifierKey === 'label') {
|
|
121
|
+
// For labels, use existing labels if provided
|
|
122
|
+
if (existingLabels && existingLabels.length > 0) {
|
|
123
|
+
const matchingLabels = existingLabels
|
|
124
|
+
.filter((label) =>
|
|
125
|
+
label.toLowerCase().includes(partialValue.toLowerCase()),
|
|
126
|
+
)
|
|
127
|
+
.slice(0, 10) // Limit to 10 suggestions
|
|
128
|
+
|
|
129
|
+
return matchingLabels.map((label) => ({
|
|
130
|
+
text: label,
|
|
131
|
+
displayText: label,
|
|
132
|
+
description: 'Label',
|
|
133
|
+
}))
|
|
134
|
+
}
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// For other qualifiers, use predefined values
|
|
139
|
+
const validValues = QUALIFIER_VALUES[qualifierKey]
|
|
140
|
+
if (!validValues) {
|
|
141
|
+
return []
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const matchingValues = validValues.filter((value) =>
|
|
145
|
+
value.toLowerCase().startsWith(partialValue.toLowerCase()),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return matchingValues.map((value) => ({
|
|
149
|
+
text: value,
|
|
150
|
+
displayText: value,
|
|
151
|
+
description: getValueDescription(qualifierKey, value),
|
|
152
|
+
}))
|
|
157
153
|
}
|
|
158
154
|
|
|
159
155
|
/**
|
|
160
156
|
* Find the current token being typed at the cursor position
|
|
161
157
|
*/
|
|
162
158
|
function findCurrentToken(text: string): {
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
currentToken: string
|
|
160
|
+
tokenStart: number
|
|
165
161
|
} {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
162
|
+
if (!text) {
|
|
163
|
+
return { currentToken: '', tokenStart: 0 }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Find the start of the current token (last space or start of string)
|
|
167
|
+
let tokenStart = text.length
|
|
168
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
169
|
+
if (text[i] === ' ') {
|
|
170
|
+
tokenStart = i + 1
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
if (i === 0) {
|
|
174
|
+
tokenStart = 0
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const currentToken = text.substring(tokenStart)
|
|
179
|
+
return { currentToken, tokenStart }
|
|
184
180
|
}
|
|
185
181
|
|
|
186
182
|
/**
|
|
187
183
|
* Get the previous token before the cursor
|
|
188
184
|
*/
|
|
189
185
|
function getPreviousToken(text: string): string | null {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
186
|
+
if (!text || text.trim() === '') {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
193
189
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
const tokens = text.trim().split(/\s+/)
|
|
191
|
+
if (tokens.length < 2) {
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
198
194
|
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
// Get the second-to-last token
|
|
196
|
+
return tokens[tokens.length - 2]
|
|
201
197
|
}
|
|
202
198
|
|
|
203
199
|
/**
|
|
204
200
|
* Get a human-readable description for a qualifier key
|
|
205
201
|
*/
|
|
206
202
|
function getQualifierDescription(key: string): string {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
203
|
+
const descriptions: Record<string, string> = {
|
|
204
|
+
is: 'Filter by status',
|
|
205
|
+
priority: 'Filter by priority',
|
|
206
|
+
type: 'Filter by type',
|
|
207
|
+
label: 'Filter by label',
|
|
208
|
+
sort: 'Sort results',
|
|
209
|
+
}
|
|
210
|
+
return descriptions[key] || ''
|
|
215
211
|
}
|
|
216
212
|
|
|
217
213
|
/**
|
|
218
214
|
* Get a human-readable description for a qualifier value
|
|
219
215
|
*/
|
|
220
216
|
function getValueDescription(key: string, value: string): string {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
217
|
+
if (key === 'is') {
|
|
218
|
+
return value === 'open' ? 'Open issues' : 'Closed issues'
|
|
219
|
+
}
|
|
220
|
+
if (key === 'priority') {
|
|
221
|
+
return `Priority: ${value}`
|
|
222
|
+
}
|
|
223
|
+
if (key === 'type') {
|
|
224
|
+
return value === 'bug' ? 'Bug report' : 'Improvement'
|
|
225
|
+
}
|
|
226
|
+
if (key === 'sort') {
|
|
227
|
+
return `Sort by ${value}`
|
|
228
|
+
}
|
|
229
|
+
return ''
|
|
234
230
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe exports from issy-core
|
|
3
|
+
* These functions work in both browser and Node.js environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Date formatting helpers
|
|
7
|
+
export { formatDisplayDate, formatFullDate } from './formatDate'
|
|
8
|
+
|
|
9
|
+
// Query parser
|
|
10
|
+
export type { ParsedQuery } from './query-parser'
|
|
11
|
+
export { parseQuery } from './query-parser'
|
|
12
|
+
|
|
13
|
+
// Search functionality (pure functions)
|
|
14
|
+
export { filterByQuery, filterIssues } from './search'
|
|
15
|
+
|
|
16
|
+
// Types
|
|
17
|
+
export type {
|
|
18
|
+
CreateIssueInput,
|
|
19
|
+
Issue,
|
|
20
|
+
IssueFilters,
|
|
21
|
+
IssueFrontmatter,
|
|
22
|
+
UpdateIssueInput,
|
|
23
|
+
} from './types'
|