@kernhq/module-tracker 0.10.1 → 0.11.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 +19 -6
- package/src/client/api-instance.ts +34 -0
- package/src/client/components/BoardCard.svelte +182 -0
- package/src/client/components/BoardView.svelte +248 -0
- package/src/client/components/CommentComposer.svelte +219 -0
- package/src/client/components/CommentThread.svelte +389 -0
- package/src/client/components/CustomField.svelte +333 -0
- package/src/client/components/DueDate.svelte +44 -0
- package/src/client/components/FilterMenu.svelte +91 -0
- package/src/client/components/GroupHeader.svelte +137 -0
- package/src/client/components/HomeLinks.svelte +38 -0
- package/src/client/components/IssueApprovals.svelte +217 -0
- package/src/client/components/IssueConnections.svelte +424 -0
- package/src/client/components/IssueDetailPanel.svelte +1337 -0
- package/src/client/components/IssueInline.svelte +75 -0
- package/src/client/components/IssueListView.svelte +153 -0
- package/src/client/components/IssuePicker.svelte +180 -0
- package/src/client/components/IssueRow.svelte +171 -0
- package/src/client/components/IssueTime.svelte +306 -0
- package/src/client/components/KqlInput.svelte +295 -0
- package/src/client/components/NewIssueDialog.svelte +210 -0
- package/src/client/components/NewProjectDialog.svelte +235 -0
- package/src/client/components/PriorityGlyph.svelte +52 -0
- package/src/client/components/SaveViewDialog.svelte +153 -0
- package/src/client/components/SidebarProjects.svelte +263 -0
- package/src/client/components/SidebarViews.svelte +336 -0
- package/src/client/components/StatusIcon.svelte +50 -0
- package/src/client/components/TrackerControls.svelte +109 -0
- package/src/client/components/TrackerSidebar.svelte +96 -0
- package/src/client/context.svelte.ts +105 -0
- package/src/client/core-api.ts +35 -0
- package/src/client/csv.test.ts +50 -0
- package/src/client/csv.ts +60 -0
- package/src/client/filters.ts +94 -0
- package/src/client/i18n.ts +3260 -0
- package/src/client/index.ts +12 -0
- package/src/client/labels.ts +200 -0
- package/src/client/mock.ts +2729 -0
- package/src/client/module.ts +491 -0
- package/src/client/nav.test.ts +59 -0
- package/src/client/nav.ts +84 -0
- package/src/client/pages/IntakePage.svelte +287 -0
- package/src/client/pages/IssuesPage.svelte +701 -0
- package/src/client/pages/ProjectPage.svelte +29 -0
- package/src/client/pages/ReportsPage.svelte +365 -0
- package/src/client/permissions.ts +33 -0
- package/src/client/planning/PlanningSections.svelte +259 -0
- package/src/client/project/ComponentsPage.svelte +423 -0
- package/src/client/project/CyclesPage.svelte +488 -0
- package/src/client/project/MilestonesPage.svelte +342 -0
- package/src/client/project/PlanCard.svelte +191 -0
- package/src/client/project/ProjectShell.svelte +93 -0
- package/src/client/project/TemplatesPage.svelte +321 -0
- package/src/client/project/context.svelte.ts +58 -0
- package/src/client/query.ts +50 -0
- package/src/client/recurrence.test.ts +37 -0
- package/src/client/recurrence.ts +89 -0
- package/src/client/richtext.ts +155 -0
- package/src/client/settings/CycleList.svelte +422 -0
- package/src/client/settings/FieldEditor.svelte +324 -0
- package/src/client/settings/FieldsSettings.svelte +273 -0
- package/src/client/settings/ImportSettings.svelte +410 -0
- package/src/client/settings/LayoutEditor.svelte +263 -0
- package/src/client/settings/PlanningList.svelte +249 -0
- package/src/client/settings/PlanningSettings.svelte +179 -0
- package/src/client/settings/ProjectsSettings.svelte +462 -0
- package/src/client/settings/RepeatingSettings.svelte +293 -0
- package/src/client/settings/TypeEditor.svelte +214 -0
- package/src/client/settings/TypesSettings.svelte +384 -0
- package/src/client/settings/WorkflowsSettings.svelte +645 -0
- package/src/client/settings/mutations.ts +19 -0
- package/src/client/time.test.ts +39 -0
- package/src/client/time.ts +34 -0
- package/src/client/tracker.test.ts +399 -0
- package/src/client/views.ts +27 -0
- package/src/client/widgets/AssignedCountWidget.svelte +14 -0
- package/src/client/widgets/CountWidget.svelte +56 -0
- package/src/client/widgets/CycleWidget.svelte +85 -0
- package/src/client/widgets/DueSoonCountWidget.svelte +14 -0
- package/src/client/widgets/IssuesWidget.svelte +222 -0
- package/src/client/widgets/ThroughputWidget.svelte +68 -0
- package/src/client/widgets/TimerWidget.svelte +116 -0
- package/src/client/widgets/VelocityWidget.svelte +55 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { defineClientModule } from '@kernhq/ui'
|
|
2
|
+
import { t, trackerMessageBundles } from './i18n.js'
|
|
3
|
+
import { TRACKER_PERMISSIONS } from './permissions.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The tracker as the shell sees it.
|
|
7
|
+
*
|
|
8
|
+
* A module does not reach into the application: it declares what it contributes — navigation, pages,
|
|
9
|
+
* command-palette actions, keyboard shortcuts and how its objects are drawn elsewhere — and the shell
|
|
10
|
+
* composes that with whatever else is enabled for the workspace. Turning the tracker off in workspace
|
|
11
|
+
* settings removes all of it without a single conditional anywhere in the shell.
|
|
12
|
+
*
|
|
13
|
+
* Labels are getters rather than strings because a module is defined once at import time while the
|
|
14
|
+
* interface language can change afterwards; reading them on render keeps the rail and the palette in
|
|
15
|
+
* the language the person actually chose.
|
|
16
|
+
*/
|
|
17
|
+
export const trackerClientModule = defineClientModule({
|
|
18
|
+
id: 'tracker',
|
|
19
|
+
name: 'Issues',
|
|
20
|
+
icon: 'square-check-big',
|
|
21
|
+
messages: trackerMessageBundles,
|
|
22
|
+
|
|
23
|
+
nav: [
|
|
24
|
+
{
|
|
25
|
+
id: 'tracker',
|
|
26
|
+
get label() {
|
|
27
|
+
return t('nav')
|
|
28
|
+
},
|
|
29
|
+
icon: 'square-check-big',
|
|
30
|
+
href: '/tracker',
|
|
31
|
+
order: 20,
|
|
32
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
|
|
36
|
+
routes: [
|
|
37
|
+
{
|
|
38
|
+
path: '/tracker/reports',
|
|
39
|
+
component: () => import('./pages/ReportsPage.svelte'),
|
|
40
|
+
get title() {
|
|
41
|
+
return t('reports_title')
|
|
42
|
+
},
|
|
43
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: '/tracker',
|
|
47
|
+
component: () => import('./pages/IssuesPage.svelte'),
|
|
48
|
+
get title() {
|
|
49
|
+
return t('title')
|
|
50
|
+
},
|
|
51
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
widgets: [
|
|
56
|
+
{
|
|
57
|
+
id: 'tracker.issues',
|
|
58
|
+
get title() {
|
|
59
|
+
return t('common.widget_issues_title')
|
|
60
|
+
},
|
|
61
|
+
get description() {
|
|
62
|
+
return t('common.widget_issues_desc')
|
|
63
|
+
},
|
|
64
|
+
icon: 'square-check-big',
|
|
65
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
66
|
+
sizes: ['m', 'l', 'xl'],
|
|
67
|
+
defaultSize: 'l',
|
|
68
|
+
order: 10,
|
|
69
|
+
// One widget instead of six. Every one of these settings resolves into the KQL that
|
|
70
|
+
// `issues.query` already answers, which is why the same card can be placed twice showing two
|
|
71
|
+
// entirely different lists.
|
|
72
|
+
settings: [
|
|
73
|
+
{
|
|
74
|
+
kind: 'select',
|
|
75
|
+
key: 'preset',
|
|
76
|
+
get label() {
|
|
77
|
+
return t('common.setting_show')
|
|
78
|
+
},
|
|
79
|
+
default: 'assigned',
|
|
80
|
+
get options() {
|
|
81
|
+
return [
|
|
82
|
+
{ value: 'assigned', label: t('preset_assigned') },
|
|
83
|
+
{ value: 'active', label: t('preset_active') },
|
|
84
|
+
{ value: 'backlog', label: t('preset_backlog') },
|
|
85
|
+
{ value: 'created', label: t('preset_created') },
|
|
86
|
+
{ value: 'subscribed', label: t('preset_subscribed') },
|
|
87
|
+
{ value: 'triage', label: t('common.widget_preset_triage') },
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
kind: 'select',
|
|
93
|
+
key: 'view',
|
|
94
|
+
get label() {
|
|
95
|
+
return t('common.widget_setting_saved_view')
|
|
96
|
+
},
|
|
97
|
+
default: null,
|
|
98
|
+
nullable: true,
|
|
99
|
+
get nullLabel() {
|
|
100
|
+
return t('common.widget_setting_no_view')
|
|
101
|
+
},
|
|
102
|
+
loadOptions: async (ctx) => {
|
|
103
|
+
const { getTrackerApi } = await import('./api-instance.js')
|
|
104
|
+
const views = await getTrackerApi().views.list({ workspaceId: ctx.workspaceId })
|
|
105
|
+
return views.map((v) => ({ value: v.id, label: v.name }))
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
kind: 'number',
|
|
110
|
+
key: 'limit',
|
|
111
|
+
get label() {
|
|
112
|
+
return t('common.setting_rows')
|
|
113
|
+
},
|
|
114
|
+
default: 8,
|
|
115
|
+
min: 3,
|
|
116
|
+
max: 20,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
component: () => import('./widgets/IssuesWidget.svelte'),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'tracker.stat-assigned',
|
|
123
|
+
get title() {
|
|
124
|
+
return t('common.widget_assigned_title')
|
|
125
|
+
},
|
|
126
|
+
get description() {
|
|
127
|
+
return t('common.widget_assigned_desc')
|
|
128
|
+
},
|
|
129
|
+
icon: 'square-check-big',
|
|
130
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
131
|
+
sizes: ['s'],
|
|
132
|
+
defaultSize: 's',
|
|
133
|
+
compact: true,
|
|
134
|
+
order: 20,
|
|
135
|
+
component: () => import('./widgets/AssignedCountWidget.svelte'),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'tracker.stat-due-soon',
|
|
139
|
+
get title() {
|
|
140
|
+
return t('common.widget_due_soon_title')
|
|
141
|
+
},
|
|
142
|
+
get description() {
|
|
143
|
+
return t('common.widget_due_soon_desc')
|
|
144
|
+
},
|
|
145
|
+
icon: 'clock',
|
|
146
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
147
|
+
sizes: ['s'],
|
|
148
|
+
defaultSize: 's',
|
|
149
|
+
compact: true,
|
|
150
|
+
order: 30,
|
|
151
|
+
component: () => import('./widgets/DueSoonCountWidget.svelte'),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'tracker.cycle-progress',
|
|
155
|
+
get title() {
|
|
156
|
+
return t('common.widget_cycle_title')
|
|
157
|
+
},
|
|
158
|
+
get description() {
|
|
159
|
+
return t('common.widget_cycle_desc')
|
|
160
|
+
},
|
|
161
|
+
icon: 'gauge',
|
|
162
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
163
|
+
sizes: ['m', 'l'],
|
|
164
|
+
defaultSize: 'm',
|
|
165
|
+
order: 40,
|
|
166
|
+
settings: [
|
|
167
|
+
{
|
|
168
|
+
kind: 'select',
|
|
169
|
+
key: 'project',
|
|
170
|
+
get label() {
|
|
171
|
+
return t('common.widget_setting_project')
|
|
172
|
+
},
|
|
173
|
+
default: null,
|
|
174
|
+
nullable: true,
|
|
175
|
+
get nullLabel() {
|
|
176
|
+
return t('common.any')
|
|
177
|
+
},
|
|
178
|
+
loadOptions: async (ctx) => {
|
|
179
|
+
const { getTrackerApi } = await import('./api-instance.js')
|
|
180
|
+
const projects = await getTrackerApi().projects.list({ workspaceId: ctx.workspaceId })
|
|
181
|
+
return projects.map((p) => ({ value: p.id, label: p.name }))
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
component: () => import('./widgets/CycleWidget.svelte'),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'tracker.velocity',
|
|
189
|
+
get title() {
|
|
190
|
+
return t('common.widget_velocity_title')
|
|
191
|
+
},
|
|
192
|
+
get description() {
|
|
193
|
+
return t('common.widget_velocity_desc')
|
|
194
|
+
},
|
|
195
|
+
icon: 'chart-column',
|
|
196
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
197
|
+
// A grouped bar chart is unreadable in a quarter-width card, so `s` is not offered.
|
|
198
|
+
sizes: ['l', 'xl'],
|
|
199
|
+
defaultSize: 'l',
|
|
200
|
+
order: 60,
|
|
201
|
+
settings: [
|
|
202
|
+
{
|
|
203
|
+
kind: 'select',
|
|
204
|
+
key: 'project',
|
|
205
|
+
get label() {
|
|
206
|
+
return t('common.widget_setting_project')
|
|
207
|
+
},
|
|
208
|
+
default: null,
|
|
209
|
+
nullable: true,
|
|
210
|
+
get nullLabel() {
|
|
211
|
+
return t('common.any')
|
|
212
|
+
},
|
|
213
|
+
loadOptions: async (ctx) => {
|
|
214
|
+
const { getTrackerApi } = await import('./api-instance.js')
|
|
215
|
+
const projects = await getTrackerApi().projects.list({ workspaceId: ctx.workspaceId })
|
|
216
|
+
return projects.map((p) => ({ value: p.id, label: p.name }))
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
kind: 'number',
|
|
221
|
+
key: 'lastN',
|
|
222
|
+
get label() {
|
|
223
|
+
return t('common.widget_velocity_cycles')
|
|
224
|
+
},
|
|
225
|
+
default: 6,
|
|
226
|
+
min: 3,
|
|
227
|
+
max: 12,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
component: () => import('./widgets/VelocityWidget.svelte'),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'tracker.created-vs-resolved',
|
|
234
|
+
get title() {
|
|
235
|
+
return t('common.widget_throughput_title')
|
|
236
|
+
},
|
|
237
|
+
get description() {
|
|
238
|
+
return t('common.widget_throughput_desc')
|
|
239
|
+
},
|
|
240
|
+
icon: 'chart-line',
|
|
241
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
242
|
+
sizes: ['l', 'xl'],
|
|
243
|
+
defaultSize: 'xl',
|
|
244
|
+
order: 70,
|
|
245
|
+
settings: [
|
|
246
|
+
{
|
|
247
|
+
kind: 'select',
|
|
248
|
+
key: 'project',
|
|
249
|
+
get label() {
|
|
250
|
+
return t('common.widget_setting_project')
|
|
251
|
+
},
|
|
252
|
+
default: null,
|
|
253
|
+
nullable: true,
|
|
254
|
+
get nullLabel() {
|
|
255
|
+
return t('common.any')
|
|
256
|
+
},
|
|
257
|
+
loadOptions: async (ctx) => {
|
|
258
|
+
const { getTrackerApi } = await import('./api-instance.js')
|
|
259
|
+
const projects = await getTrackerApi().projects.list({ workspaceId: ctx.workspaceId })
|
|
260
|
+
return projects.map((p) => ({ value: p.id, label: p.name }))
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
kind: 'select',
|
|
265
|
+
key: 'range',
|
|
266
|
+
get label() {
|
|
267
|
+
return t('common.widget_setting_range')
|
|
268
|
+
},
|
|
269
|
+
default: '30',
|
|
270
|
+
get options() {
|
|
271
|
+
return [
|
|
272
|
+
{ value: '14', label: t('common.widget_range_14') },
|
|
273
|
+
{ value: '30', label: t('common.widget_range_30') },
|
|
274
|
+
{ value: '90', label: t('common.widget_range_90') },
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
component: () => import('./widgets/ThroughputWidget.svelte'),
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'tracker.timer',
|
|
283
|
+
get title() {
|
|
284
|
+
return t('common.widget_timer_title')
|
|
285
|
+
},
|
|
286
|
+
get description() {
|
|
287
|
+
return t('common.widget_timer_desc')
|
|
288
|
+
},
|
|
289
|
+
icon: 'timer',
|
|
290
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
291
|
+
sizes: ['s', 'm'],
|
|
292
|
+
defaultSize: 's',
|
|
293
|
+
compact: true,
|
|
294
|
+
order: 50,
|
|
295
|
+
component: () => import('./widgets/TimerWidget.svelte'),
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
|
|
299
|
+
commands: [
|
|
300
|
+
{
|
|
301
|
+
id: 'tracker.new-issue',
|
|
302
|
+
get label() {
|
|
303
|
+
return t('new_issue')
|
|
304
|
+
},
|
|
305
|
+
icon: 'plus',
|
|
306
|
+
shortcut: ['c'],
|
|
307
|
+
permission: TRACKER_PERMISSIONS.create,
|
|
308
|
+
run: (ctx) => ctx.navigate('/tracker?new=1'),
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: 'tracker.issues',
|
|
312
|
+
get label() {
|
|
313
|
+
return t('cmd_open_list')
|
|
314
|
+
},
|
|
315
|
+
icon: 'list',
|
|
316
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
317
|
+
run: (ctx) => ctx.navigate('/tracker'),
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: 'tracker.board',
|
|
321
|
+
get label() {
|
|
322
|
+
return t('cmd_open_board')
|
|
323
|
+
},
|
|
324
|
+
icon: 'columns-3',
|
|
325
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
326
|
+
run: (ctx) => ctx.navigate('/tracker?view=board'),
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: 'tracker.my-issues',
|
|
330
|
+
get label() {
|
|
331
|
+
return t('cmd_my_issues')
|
|
332
|
+
},
|
|
333
|
+
icon: 'user',
|
|
334
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
335
|
+
run: (ctx) => ctx.navigate('/tracker?preset=assigned'),
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: 'tracker.triage',
|
|
339
|
+
get label() {
|
|
340
|
+
return t('cmd_triage')
|
|
341
|
+
},
|
|
342
|
+
icon: 'inbox',
|
|
343
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
344
|
+
run: (ctx) => ctx.navigate('/tracker?q=triage%20%3D%20true'),
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: 'tracker.reports',
|
|
348
|
+
get label() {
|
|
349
|
+
return t('reports_title')
|
|
350
|
+
},
|
|
351
|
+
icon: 'activity',
|
|
352
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
353
|
+
run: (ctx) => ctx.navigate('/tracker/reports'),
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: 'tracker.by-project',
|
|
357
|
+
get label() {
|
|
358
|
+
return t('cmd_by_project')
|
|
359
|
+
},
|
|
360
|
+
icon: 'diamond',
|
|
361
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
362
|
+
run: (ctx) => ctx.navigate('/tracker?group=project'),
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
|
|
366
|
+
shortcuts: [
|
|
367
|
+
{
|
|
368
|
+
id: 'tracker.new-issue',
|
|
369
|
+
keys: ['c'],
|
|
370
|
+
get label() {
|
|
371
|
+
return t('new_issue')
|
|
372
|
+
},
|
|
373
|
+
scope: 'tracker',
|
|
374
|
+
run: (ctx) => ctx.navigate('/tracker?new=1'),
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
|
|
378
|
+
sidebar: [
|
|
379
|
+
{
|
|
380
|
+
id: 'tracker',
|
|
381
|
+
match: ['tracker'],
|
|
382
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
383
|
+
controls: () => import('./components/TrackerControls.svelte'),
|
|
384
|
+
component: () => import('./components/TrackerSidebar.svelte'),
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
// The three "my work" rows used to live in the application layout, where a workspace with the
|
|
388
|
+
// tracker switched off still saw them.
|
|
389
|
+
id: 'tracker.home',
|
|
390
|
+
match: [''],
|
|
391
|
+
order: 20,
|
|
392
|
+
permission: TRACKER_PERMISSIONS.view,
|
|
393
|
+
component: () => import('./components/HomeLinks.svelte'),
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Where the tracker is configured. The shell builds the settings nav from these — label, icon and
|
|
399
|
+
* permission — while the route itself is conventional (`/settings/<module>/<id>`), so a module
|
|
400
|
+
* does not have to mount pages dynamically to be configurable.
|
|
401
|
+
*/
|
|
402
|
+
settingsPages: [
|
|
403
|
+
{
|
|
404
|
+
id: 'fields',
|
|
405
|
+
get label() {
|
|
406
|
+
return t('settings_fields')
|
|
407
|
+
},
|
|
408
|
+
icon: 'tag',
|
|
409
|
+
scope: 'workspace',
|
|
410
|
+
permission: TRACKER_PERMISSIONS.fieldManage,
|
|
411
|
+
order: 10,
|
|
412
|
+
component: () => import('./settings/FieldsSettings.svelte'),
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
id: 'workflows',
|
|
416
|
+
get label() {
|
|
417
|
+
return t('settings_workflows')
|
|
418
|
+
},
|
|
419
|
+
icon: 'git-branch',
|
|
420
|
+
scope: 'workspace',
|
|
421
|
+
permission: TRACKER_PERMISSIONS.workflowManage,
|
|
422
|
+
order: 25,
|
|
423
|
+
component: () => import('./settings/WorkflowsSettings.svelte'),
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
id: 'projects',
|
|
427
|
+
get label() {
|
|
428
|
+
return t('settings_projects')
|
|
429
|
+
},
|
|
430
|
+
icon: 'folder',
|
|
431
|
+
scope: 'workspace',
|
|
432
|
+
permission: TRACKER_PERMISSIONS.projectManage,
|
|
433
|
+
order: 5,
|
|
434
|
+
component: () => import('./settings/ProjectsSettings.svelte'),
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
id: 'repeating',
|
|
438
|
+
get label() {
|
|
439
|
+
return t('settings_repeating')
|
|
440
|
+
},
|
|
441
|
+
icon: 'refresh-cw',
|
|
442
|
+
scope: 'workspace',
|
|
443
|
+
permission: TRACKER_PERMISSIONS.projectManage,
|
|
444
|
+
order: 40,
|
|
445
|
+
component: () => import('./settings/RepeatingSettings.svelte'),
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
id: 'import',
|
|
449
|
+
get label() {
|
|
450
|
+
return t('settings_import')
|
|
451
|
+
},
|
|
452
|
+
icon: 'upload',
|
|
453
|
+
scope: 'workspace',
|
|
454
|
+
permission: TRACKER_PERMISSIONS.projectManage,
|
|
455
|
+
order: 50,
|
|
456
|
+
component: () => import('./settings/ImportSettings.svelte'),
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: 'planning',
|
|
460
|
+
get label() {
|
|
461
|
+
return t('settings_planning')
|
|
462
|
+
},
|
|
463
|
+
icon: 'diamond',
|
|
464
|
+
scope: 'workspace',
|
|
465
|
+
permission: TRACKER_PERMISSIONS.projectManage,
|
|
466
|
+
order: 30,
|
|
467
|
+
component: () => import('./settings/PlanningSettings.svelte'),
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
id: 'types',
|
|
471
|
+
get label() {
|
|
472
|
+
return t('settings_types')
|
|
473
|
+
},
|
|
474
|
+
icon: 'layout-grid',
|
|
475
|
+
scope: 'workspace',
|
|
476
|
+
permission: TRACKER_PERMISSIONS.typeManage,
|
|
477
|
+
order: 20,
|
|
478
|
+
component: () => import('./settings/TypesSettings.svelte'),
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
|
|
482
|
+
presenters: [
|
|
483
|
+
{
|
|
484
|
+
type: 'issue',
|
|
485
|
+
inline: () => import('./components/IssueInline.svelte'),
|
|
486
|
+
page: (id, workspaceSlug) => `/${workspaceSlug}/tracker?issue=${encodeURIComponent(id)}`,
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
export default trackerClientModule
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
isProjectSection,
|
|
4
|
+
isTrackerTarget,
|
|
5
|
+
projectKql,
|
|
6
|
+
projectSectionHref,
|
|
7
|
+
projectTargets,
|
|
8
|
+
trackerHref,
|
|
9
|
+
} from './nav.js'
|
|
10
|
+
|
|
11
|
+
const PROJECT = '01920000-0000-7000-8000-0000000000a1'
|
|
12
|
+
|
|
13
|
+
describe('tracker sidebar links', () => {
|
|
14
|
+
it('quotes a project id, because KQL would read a bare uuid as a number', () => {
|
|
15
|
+
expect(projectKql(PROJECT)).toBe(`project = "${PROJECT}"`)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('asks for nothing it does not need', () => {
|
|
19
|
+
expect(trackerHref('northstar')).toBe('/northstar/tracker')
|
|
20
|
+
expect(trackerHref('northstar', { preset: 'assigned' })).toBe('/northstar/tracker?preset=assigned')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('carries the query and the grouping a project view stands for', () => {
|
|
24
|
+
const href = trackerHref('northstar', projectTargets(PROJECT).byComponent)
|
|
25
|
+
const params = new URL(href, 'https://example.test').searchParams
|
|
26
|
+
expect(params.get('q')).toBe(projectKql(PROJECT))
|
|
27
|
+
expect(params.get('group')).toBe('component')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('marks a row current only when the screen shows exactly what it asks for', () => {
|
|
31
|
+
const at = (search: string) => new URL(`https://example.test/x${search}`).searchParams
|
|
32
|
+
const components = projectTargets(PROJECT).byComponent
|
|
33
|
+
|
|
34
|
+
expect(isTrackerTarget(at(''), {})).toBe(true)
|
|
35
|
+
expect(isTrackerTarget(at('?preset=assigned'), { preset: 'assigned' })).toBe(true)
|
|
36
|
+
// the same query grouped differently is a different row
|
|
37
|
+
expect(isTrackerTarget(at(`?q=${encodeURIComponent(components.q!)}`), components)).toBe(false)
|
|
38
|
+
expect(isTrackerTarget(at(`?q=${encodeURIComponent(components.q!)}&group=component`), components)).toBe(
|
|
39
|
+
true,
|
|
40
|
+
)
|
|
41
|
+
// "All issues" must not light up while a filtered list is on screen
|
|
42
|
+
expect(isTrackerTarget(at('?preset=assigned'), {})).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('addresses a project page by the key people know it by', () => {
|
|
46
|
+
expect(projectSectionHref('northstar', 'KRN', 'milestones')).toBe(
|
|
47
|
+
'/northstar/tracker/projects/KRN/milestones',
|
|
48
|
+
)
|
|
49
|
+
// an unknown section in the URL must not render a blank page
|
|
50
|
+
expect(isProjectSection('components')).toBe(true)
|
|
51
|
+
expect(isProjectSection('nonsense')).toBe(false)
|
|
52
|
+
expect(isProjectSection(undefined)).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('gives no row to a saved view, which has one of its own', () => {
|
|
56
|
+
const params = new URL('https://example.test/x?view_id=v1').searchParams
|
|
57
|
+
expect(isTrackerTarget(params, {})).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Project } from './index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where the tracker's sidebar sends you.
|
|
5
|
+
*
|
|
6
|
+
* Every row in the sidebar is a link to a query, not a mode the page is put into: the issues screen
|
|
7
|
+
* reads what it shows from the URL, so "the components of Marketing" is `q` plus `group` and
|
|
8
|
+
* nothing else — it can be linked to, opened in a second tab and gone back from. Built here rather
|
|
9
|
+
* than inline so the row that produces a URL and the row that decides it is the current one cannot
|
|
10
|
+
* drift apart.
|
|
11
|
+
*
|
|
12
|
+
* Project ids rather than keys: `project` compiles to a uuid column, and KQL reads an unquoted
|
|
13
|
+
* value starting with a digit as a number.
|
|
14
|
+
*/
|
|
15
|
+
export const projectKql = (projectId: string): string => `project = ${JSON.stringify(projectId)}`
|
|
16
|
+
|
|
17
|
+
export interface TrackerTarget {
|
|
18
|
+
/** the KQL the row asks for, or '' for none */
|
|
19
|
+
q?: string
|
|
20
|
+
/** the grouping, or undefined for the default (status) */
|
|
21
|
+
group?: string
|
|
22
|
+
preset?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A tracker link, with only the parameters the target actually needs. */
|
|
26
|
+
export function trackerHref(workspaceSlug: string, target: TrackerTarget = {}): string {
|
|
27
|
+
const params = new URLSearchParams()
|
|
28
|
+
if (target.preset) params.set('preset', target.preset)
|
|
29
|
+
if (target.q) params.set('q', target.q)
|
|
30
|
+
if (target.group) params.set('group', target.group)
|
|
31
|
+
const query = params.toString()
|
|
32
|
+
return `/${workspaceSlug}/tracker${query ? `?${query}` : ''}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whether the screen is showing exactly what this row asks for. */
|
|
36
|
+
export function isTrackerTarget(params: URLSearchParams, target: TrackerTarget = {}): boolean {
|
|
37
|
+
// a saved view is its own row: while one is open no plain row is the current one
|
|
38
|
+
if (params.get('view_id')) return false
|
|
39
|
+
return (
|
|
40
|
+
(params.get('preset') ?? '') === (target.preset ?? '') &&
|
|
41
|
+
(params.get('q') ?? '') === (target.q ?? '') &&
|
|
42
|
+
(params.get('group') ?? '') === (target.group ?? '')
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A project's work, as queries.
|
|
48
|
+
*
|
|
49
|
+
* The groupings stay because the pages link into them — a component is worth opening as "the issues
|
|
50
|
+
* in it" once you are looking at the component itself — but they are not what the sidebar's
|
|
51
|
+
* Components row does any more: that opens the page where components are made.
|
|
52
|
+
*/
|
|
53
|
+
export const projectTargets = (projectId: string) => ({
|
|
54
|
+
issues: { q: projectKql(projectId) } satisfies TrackerTarget,
|
|
55
|
+
byComponent: { q: projectKql(projectId), group: 'component' } satisfies TrackerTarget,
|
|
56
|
+
byMilestone: { q: projectKql(projectId), group: 'milestone' } satisfies TrackerTarget,
|
|
57
|
+
byCycle: { q: projectKql(projectId), group: 'cycle' } satisfies TrackerTarget,
|
|
58
|
+
triage: { q: `${projectKql(projectId)} and triage = true` } satisfies TrackerTarget,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A project's own pages, addressed by key.
|
|
63
|
+
*
|
|
64
|
+
* Components, milestones, cycles and templates are things a team makes, so each is a page rather
|
|
65
|
+
* than a grouping of the issue list: `/tracker/projects/KRN/components` is where you add one.
|
|
66
|
+
*/
|
|
67
|
+
export const PROJECT_SECTIONS = ['components', 'milestones', 'cycles', 'templates'] as const
|
|
68
|
+
export type ProjectSection = (typeof PROJECT_SECTIONS)[number]
|
|
69
|
+
|
|
70
|
+
export const isProjectSection = (value: string | undefined): value is ProjectSection =>
|
|
71
|
+
PROJECT_SECTIONS.includes((value ?? '') as ProjectSection)
|
|
72
|
+
|
|
73
|
+
export const projectSectionHref = (
|
|
74
|
+
workspaceSlug: string,
|
|
75
|
+
projectKey: string,
|
|
76
|
+
section: ProjectSection,
|
|
77
|
+
): string => `/${workspaceSlug}/tracker/projects/${encodeURIComponent(projectKey)}/${section}`
|
|
78
|
+
|
|
79
|
+
/** Settings pages take the project in the URL, so a menu can land on the one you clicked. */
|
|
80
|
+
export const projectSettingsHref = (workspaceSlug: string, page: string, projectId: string): string =>
|
|
81
|
+
`/${workspaceSlug}/settings/tracker/${page}?project=${encodeURIComponent(projectId)}`
|
|
82
|
+
|
|
83
|
+
/** A project's colour square falls back to the neutral the design system uses for "no colour". */
|
|
84
|
+
export const projectColor = (project: Project): string => project.color ?? 'var(--kern-ink-330)'
|