@kernhq/module-tracker 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/LICENSE +662 -0
- package/README.md +265 -0
- package/dist/contract/events.d.ts +140 -0
- package/dist/contract/events.d.ts.map +1 -0
- package/dist/contract/events.js +51 -0
- package/dist/contract/events.js.map +1 -0
- package/dist/contract/index.d.ts +6 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +6 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/models.d.ts +2547 -0
- package/dist/contract/models.d.ts.map +1 -0
- package/dist/contract/models.js +1214 -0
- package/dist/contract/models.js.map +1 -0
- package/dist/contract/notifications.d.ts +4 -0
- package/dist/contract/notifications.d.ts.map +1 -0
- package/dist/contract/notifications.js +46 -0
- package/dist/contract/notifications.js.map +1 -0
- package/dist/contract/permissions.d.ts +161 -0
- package/dist/contract/permissions.d.ts.map +1 -0
- package/dist/contract/permissions.js +192 -0
- package/dist/contract/permissions.js.map +1 -0
- package/dist/contract/router.d.ts +9659 -0
- package/dist/contract/router.d.ts.map +1 -0
- package/dist/contract/router.js +732 -0
- package/dist/contract/router.js.map +1 -0
- package/dist/kql/ast.d.ts +90 -0
- package/dist/kql/ast.d.ts.map +1 -0
- package/dist/kql/ast.js +80 -0
- package/dist/kql/ast.js.map +1 -0
- package/dist/kql/dates.d.ts +16 -0
- package/dist/kql/dates.d.ts.map +1 -0
- package/dist/kql/dates.js +54 -0
- package/dist/kql/dates.js.map +1 -0
- package/dist/kql/fields.d.ts +61 -0
- package/dist/kql/fields.d.ts.map +1 -0
- package/dist/kql/fields.js +106 -0
- package/dist/kql/fields.js.map +1 -0
- package/dist/kql/index.d.ts +16 -0
- package/dist/kql/index.d.ts.map +1 -0
- package/dist/kql/index.js +16 -0
- package/dist/kql/index.js.map +1 -0
- package/dist/kql/lexer.d.ts +28 -0
- package/dist/kql/lexer.d.ts.map +1 -0
- package/dist/kql/lexer.js +127 -0
- package/dist/kql/lexer.js.map +1 -0
- package/dist/kql/parser.d.ts +14 -0
- package/dist/kql/parser.d.ts.map +1 -0
- package/dist/kql/parser.js +290 -0
- package/dist/kql/parser.js.map +1 -0
- package/dist/kql/suggest.d.ts +8 -0
- package/dist/kql/suggest.d.ts.map +1 -0
- package/dist/kql/suggest.js +96 -0
- package/dist/kql/suggest.js.map +1 -0
- package/dist/kql/validate.d.ts +10 -0
- package/dist/kql/validate.d.ts.map +1 -0
- package/dist/kql/validate.js +84 -0
- package/dist/kql/validate.js.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +583 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/kql/compile.d.ts +49 -0
- package/dist/server/kql/compile.d.ts.map +1 -0
- package/dist/server/kql/compile.js +372 -0
- package/dist/server/kql/compile.js.map +1 -0
- package/dist/server/rank.d.ts +23 -0
- package/dist/server/rank.d.ts.map +1 -0
- package/dist/server/rank.js +66 -0
- package/dist/server/rank.js.map +1 -0
- package/dist/server/rich.d.ts +30 -0
- package/dist/server/rich.d.ts.map +1 -0
- package/dist/server/rich.js +97 -0
- package/dist/server/rich.js.map +1 -0
- package/dist/server/router.d.ts +10589 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/router.js +431 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/schema.d.ts +6622 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +581 -0
- package/dist/server/schema.js.map +1 -0
- package/dist/server/services/access.d.ts +53 -0
- package/dist/server/services/access.d.ts.map +1 -0
- package/dist/server/services/access.js +146 -0
- package/dist/server/services/access.js.map +1 -0
- package/dist/server/services/comments.d.ts +36 -0
- package/dist/server/services/comments.d.ts.map +1 -0
- package/dist/server/services/comments.js +231 -0
- package/dist/server/services/comments.js.map +1 -0
- package/dist/server/services/config.d.ts +130 -0
- package/dist/server/services/config.d.ts.map +1 -0
- package/dist/server/services/config.js +797 -0
- package/dist/server/services/config.js.map +1 -0
- package/dist/server/services/db.d.ts +93 -0
- package/dist/server/services/db.d.ts.map +1 -0
- package/dist/server/services/db.js +519 -0
- package/dist/server/services/db.js.map +1 -0
- package/dist/server/services/imports.d.ts +37 -0
- package/dist/server/services/imports.d.ts.map +1 -0
- package/dist/server/services/imports.js +324 -0
- package/dist/server/services/imports.js.map +1 -0
- package/dist/server/services/index.d.ts +36 -0
- package/dist/server/services/index.d.ts.map +1 -0
- package/dist/server/services/index.js +57 -0
- package/dist/server/services/index.js.map +1 -0
- package/dist/server/services/intake.d.ts +47 -0
- package/dist/server/services/intake.d.ts.map +1 -0
- package/dist/server/services/intake.js +268 -0
- package/dist/server/services/intake.js.map +1 -0
- package/dist/server/services/issues.d.ts +763 -0
- package/dist/server/services/issues.d.ts.map +1 -0
- package/dist/server/services/issues.js +1381 -0
- package/dist/server/services/issues.js.map +1 -0
- package/dist/server/services/notify.d.ts +51 -0
- package/dist/server/services/notify.d.ts.map +1 -0
- package/dist/server/services/notify.js +90 -0
- package/dist/server/services/notify.js.map +1 -0
- package/dist/server/services/planning.d.ts +62 -0
- package/dist/server/services/planning.d.ts.map +1 -0
- package/dist/server/services/planning.js +614 -0
- package/dist/server/services/planning.js.map +1 -0
- package/dist/server/services/projects.d.ts +43 -0
- package/dist/server/services/projects.d.ts.map +1 -0
- package/dist/server/services/projects.js +375 -0
- package/dist/server/services/projects.js.map +1 -0
- package/dist/server/services/query.d.ts +24 -0
- package/dist/server/services/query.d.ts.map +1 -0
- package/dist/server/services/query.js +312 -0
- package/dist/server/services/query.js.map +1 -0
- package/dist/server/services/reports.d.ts +30 -0
- package/dist/server/services/reports.d.ts.map +1 -0
- package/dist/server/services/reports.js +295 -0
- package/dist/server/services/reports.js.map +1 -0
- package/dist/server/services/time.d.ts +53 -0
- package/dist/server/services/time.d.ts.map +1 -0
- package/dist/server/services/time.js +192 -0
- package/dist/server/services/time.js.map +1 -0
- package/dist/server/services/transitions.d.ts +61 -0
- package/dist/server/services/transitions.d.ts.map +1 -0
- package/dist/server/services/transitions.js +493 -0
- package/dist/server/services/transitions.js.map +1 -0
- package/dist/server/services/views.d.ts +33 -0
- package/dist/server/services/views.d.ts.map +1 -0
- package/dist/server/services/views.js +195 -0
- package/dist/server/services/views.js.map +1 -0
- package/migrations/0000_init.sql +529 -0
- package/migrations/0001_rls.sql +259 -0
- package/migrations/meta/0000_snapshot.json +4140 -0
- package/migrations/meta/_journal.json +20 -0
- package/package.json +82 -0
- package/src/client/api.ts +15 -0
- package/src/client/format.ts +164 -0
- package/src/client/group.ts +150 -0
- package/src/client/index.ts +60 -0
- package/src/client/kql.ts +492 -0
- package/src/client/rank.ts +87 -0
- package/src/client/types.ts +57 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "7",
|
|
3
|
+
"dialect": "postgresql",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"idx": 0,
|
|
7
|
+
"version": "7",
|
|
8
|
+
"when": 1787397066442,
|
|
9
|
+
"tag": "0000_init",
|
|
10
|
+
"breakpoints": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idx": 1,
|
|
14
|
+
"version": "7",
|
|
15
|
+
"when": 1787397066443,
|
|
16
|
+
"tag": "0001_rls",
|
|
17
|
+
"breakpoints": true
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kernhq/module-tracker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kern tracker module: projects, work item types, custom fields, workflows, issues, KQL, cycles, views, reports, intake, time tracking (server + client skeleton)",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/KernAIO/modules",
|
|
10
|
+
"directory": "packages/tracker"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"migrations",
|
|
18
|
+
"src/client",
|
|
19
|
+
"docs"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
"./contract": {
|
|
23
|
+
"types": "./dist/contract/index.d.ts",
|
|
24
|
+
"import": "./dist/contract/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./kql": {
|
|
27
|
+
"types": "./dist/kql/index.d.ts",
|
|
28
|
+
"import": "./dist/kql/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./server": {
|
|
31
|
+
"types": "./dist/server/index.d.ts",
|
|
32
|
+
"import": "./dist/server/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./client": {
|
|
35
|
+
"types": "./src/client/index.ts",
|
|
36
|
+
"svelte": "./src/client/index.ts",
|
|
37
|
+
"default": "./src/client/index.ts"
|
|
38
|
+
},
|
|
39
|
+
"./client/*": {
|
|
40
|
+
"types": "./src/client/*",
|
|
41
|
+
"svelte": "./src/client/*",
|
|
42
|
+
"default": "./src/client/*"
|
|
43
|
+
},
|
|
44
|
+
"./migrations": "./migrations"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@kernhq/contracts": "^0.1.0",
|
|
48
|
+
"@kernhq/kernel": "^0.1.0",
|
|
49
|
+
"@kernhq/sdk": "^0.1.0",
|
|
50
|
+
"@kernhq/workflow": "^0.1.0",
|
|
51
|
+
"@orpc/contract": "^1.15.0",
|
|
52
|
+
"@orpc/server": "^1.15.0",
|
|
53
|
+
"drizzle-orm": "^0.45.0",
|
|
54
|
+
"zod": "^4.1.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@kernhq/tsconfig": "^0.1.0",
|
|
58
|
+
"@types/node": "^24.0.0",
|
|
59
|
+
"@types/pg": "^8.15.0",
|
|
60
|
+
"drizzle-kit": "^0.31.0",
|
|
61
|
+
"pg": "^8.16.0",
|
|
62
|
+
"typescript": "~5.9.3",
|
|
63
|
+
"vitest": "^4.0.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"svelte": "^5.0.0"
|
|
67
|
+
},
|
|
68
|
+
"peerDependenciesMeta": {
|
|
69
|
+
"svelte": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "tsc -p tsconfig.json",
|
|
75
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
76
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
77
|
+
"lint": "biome check .",
|
|
78
|
+
"test": "vitest run",
|
|
79
|
+
"test:unit": "vitest run --exclude 'src/**/*.int.test.ts'",
|
|
80
|
+
"db:generate": "drizzle-kit generate"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createModuleClient, type KernClientOptions } from '@kernhq/sdk'
|
|
2
|
+
import type { ContractRouterClient } from '@orpc/contract'
|
|
3
|
+
import type { TrackerContract } from '../contract/index.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Typed oRPC client for `/api/tracker` (see `../contract`).
|
|
7
|
+
*
|
|
8
|
+
* The shape is generated from the contract, so every procedure, input and output stays in step with
|
|
9
|
+
* the server without a second declaration: if the contract changes, callers stop compiling.
|
|
10
|
+
*/
|
|
11
|
+
export type TrackerApi = ContractRouterClient<TrackerContract>
|
|
12
|
+
|
|
13
|
+
export function createTrackerClient(opts: KernClientOptions): TrackerApi {
|
|
14
|
+
return createModuleClient<TrackerApi>(opts, 'tracker')
|
|
15
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { StatusCategory } from '@kernhq/workflow'
|
|
2
|
+
import type { Priority } from '../contract/models.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Presentation rules shared by every tracker surface (list rows, board cards, detail panel).
|
|
6
|
+
*
|
|
7
|
+
* Everything here is pure and framework-free so the same rules can be unit-tested and reused by any
|
|
8
|
+
* host: the Svelte components only turn these values into markup. Colours are design-system tokens
|
|
9
|
+
* (`--kern-*`), never literal hex, so light mode, dark mode and any future theme all work.
|
|
10
|
+
* See `app/DESIGN.md` §3.0 for the drawing spec these values implement.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The five status treatments the design draws (DESIGN.md §3.0). */
|
|
14
|
+
export type StatusVisual = 'triage' | 'todo' | 'in_progress' | 'in_review' | 'done' | 'cancelled'
|
|
15
|
+
|
|
16
|
+
export interface StatusStyle {
|
|
17
|
+
visual: StatusVisual
|
|
18
|
+
/** stroke/fill colour for the status glyph */
|
|
19
|
+
color: string
|
|
20
|
+
/** chip background for the same status */
|
|
21
|
+
tint: string
|
|
22
|
+
/** SVG stroke-dasharray, `null` for a solid ring */
|
|
23
|
+
dash: string | null
|
|
24
|
+
/** inner fill path (16×16 viewBox), `null` for an empty ring */
|
|
25
|
+
fill: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const STYLES: Record<StatusVisual, StatusStyle> = {
|
|
29
|
+
triage: {
|
|
30
|
+
visual: 'triage',
|
|
31
|
+
color: 'var(--kern-ink-280)',
|
|
32
|
+
tint: 'var(--kern-surface-chip)',
|
|
33
|
+
dash: '2.2 2',
|
|
34
|
+
fill: null,
|
|
35
|
+
},
|
|
36
|
+
todo: {
|
|
37
|
+
visual: 'todo',
|
|
38
|
+
color: 'var(--kern-ink-350)',
|
|
39
|
+
tint: 'var(--kern-surface-chip)',
|
|
40
|
+
dash: null,
|
|
41
|
+
fill: null,
|
|
42
|
+
},
|
|
43
|
+
in_progress: {
|
|
44
|
+
visual: 'in_progress',
|
|
45
|
+
color: 'var(--kern-accent)',
|
|
46
|
+
tint: 'var(--kern-accent-tint)',
|
|
47
|
+
dash: null,
|
|
48
|
+
fill: 'M8 5.2h2.8v5.6H8z',
|
|
49
|
+
},
|
|
50
|
+
in_review: {
|
|
51
|
+
visual: 'in_review',
|
|
52
|
+
color: 'var(--kern-purple-status)',
|
|
53
|
+
tint: 'var(--kern-purple-tint-2)',
|
|
54
|
+
dash: null,
|
|
55
|
+
fill: 'M8 5.2h2.8v5.6H5.2V8H8z',
|
|
56
|
+
},
|
|
57
|
+
done: {
|
|
58
|
+
visual: 'done',
|
|
59
|
+
color: 'var(--kern-success)',
|
|
60
|
+
tint: 'var(--kern-success-tint-2)',
|
|
61
|
+
dash: null,
|
|
62
|
+
fill: 'M5.2 5.2h5.6v5.6H5.2z',
|
|
63
|
+
},
|
|
64
|
+
cancelled: {
|
|
65
|
+
visual: 'cancelled',
|
|
66
|
+
color: 'var(--kern-ink-330)',
|
|
67
|
+
tint: 'var(--kern-surface-chip)',
|
|
68
|
+
dash: null,
|
|
69
|
+
fill: 'M5.2 5.2h5.6v5.6H5.2z',
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Statuses whose id or name marks them as a review step get the purple treatment. */
|
|
74
|
+
const REVIEW = /(^|[-_ ])(review|qa|verify|verification)([-_ ]|$)/i
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Pick the drawing for a status. The workflow category decides the shape; a status that reads as a
|
|
78
|
+
* review step inside `in_progress` gets its own colour, which is what the design shows.
|
|
79
|
+
*/
|
|
80
|
+
export function statusStyle(category: StatusCategory, statusId?: string | null, name?: string | null) {
|
|
81
|
+
if (category === 'in_progress' && ((statusId && REVIEW.test(statusId)) || (name && REVIEW.test(name)))) {
|
|
82
|
+
return STYLES.in_review
|
|
83
|
+
}
|
|
84
|
+
switch (category) {
|
|
85
|
+
case 'triage':
|
|
86
|
+
return STYLES.triage
|
|
87
|
+
case 'backlog':
|
|
88
|
+
case 'todo':
|
|
89
|
+
return STYLES.todo
|
|
90
|
+
case 'in_progress':
|
|
91
|
+
return STYLES.in_progress
|
|
92
|
+
case 'done':
|
|
93
|
+
return STYLES.done
|
|
94
|
+
case 'cancelled':
|
|
95
|
+
return STYLES.cancelled
|
|
96
|
+
default:
|
|
97
|
+
return STYLES.todo
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Group ordering for "group by status": triage first, finished work last (DESIGN.md §3.2). */
|
|
102
|
+
export const STATUS_CATEGORY_ORDER: Record<StatusCategory, number> = {
|
|
103
|
+
triage: 0,
|
|
104
|
+
backlog: 1,
|
|
105
|
+
todo: 2,
|
|
106
|
+
in_progress: 3,
|
|
107
|
+
done: 4,
|
|
108
|
+
cancelled: 5,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Highest priority first, matching the design's group order (Urgent, High, Medium, Low). */
|
|
112
|
+
export const PRIORITY_GROUP_ORDER: Priority[] = ['urgent', 'high', 'medium', 'low', 'none']
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The three bars of the priority glyph (DESIGN.md §3.0). `true` = lit.
|
|
116
|
+
* `none` is drawn as an empty box so the column never collapses.
|
|
117
|
+
*/
|
|
118
|
+
export function priorityBars(priority: Priority): [boolean, boolean, boolean] {
|
|
119
|
+
switch (priority) {
|
|
120
|
+
case 'urgent':
|
|
121
|
+
case 'high':
|
|
122
|
+
return [true, true, true]
|
|
123
|
+
case 'medium':
|
|
124
|
+
return [true, true, false]
|
|
125
|
+
case 'low':
|
|
126
|
+
return [true, false, false]
|
|
127
|
+
default:
|
|
128
|
+
return [false, false, false]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Geometry of the three bars, shared by the glyph component and any future renderer. */
|
|
133
|
+
export const PRIORITY_BAR_GEOMETRY = [
|
|
134
|
+
{ x: 1.5, y: 9, height: 5.5 },
|
|
135
|
+
{ x: 6.5, y: 5.5, height: 9 },
|
|
136
|
+
{ x: 11.5, y: 2, height: 12.5 },
|
|
137
|
+
] as const
|
|
138
|
+
|
|
139
|
+
export type DueTone = 'hot' | 'normal'
|
|
140
|
+
|
|
141
|
+
/** Today and tomorrow (and anything overdue) are "hot" and drawn in danger red (DESIGN.md §3.0). */
|
|
142
|
+
export function dueTone(dueDate: string, today = new Date()): DueTone {
|
|
143
|
+
const days = daysUntil(dueDate, today)
|
|
144
|
+
return days <= 1 ? 'hot' : 'normal'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Whole days between today (local) and a `YYYY-MM-DD` date; negative when overdue. */
|
|
148
|
+
export function daysUntil(dueDate: string, today = new Date()): number {
|
|
149
|
+
const [y, m, d] = dueDate.split('-').map(Number)
|
|
150
|
+
if (!y || !m || !d) return Number.POSITIVE_INFINITY
|
|
151
|
+
const due = Date.UTC(y, m - 1, d)
|
|
152
|
+
const start = Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())
|
|
153
|
+
return Math.round((due - start) / 86_400_000)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Seconds of logged time as the board footer shows it ("0h", "3h", "1d 2h"). */
|
|
157
|
+
export function formatDuration(seconds: number): string {
|
|
158
|
+
if (seconds <= 0) return '0h'
|
|
159
|
+
const hours = Math.round(seconds / 3600)
|
|
160
|
+
if (hours < 24) return `${hours}h`
|
|
161
|
+
const days = Math.floor(hours / 24)
|
|
162
|
+
const rest = hours % 24
|
|
163
|
+
return rest ? `${days}d ${rest}h` : `${days}d`
|
|
164
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { StatusCategory } from '@kernhq/workflow'
|
|
2
|
+
import type { GroupBy, Issue, Priority } from '../contract/models.js'
|
|
3
|
+
import { PRIORITY_GROUP_ORDER, STATUS_CATEGORY_ORDER } from './format.js'
|
|
4
|
+
import { byRank } from './rank.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Turning a flat list of issues into the grouped list and board the design draws.
|
|
8
|
+
*
|
|
9
|
+
* Kept separate from the components because the rules have edges worth testing: multi-valued fields
|
|
10
|
+
* (assignees, labels) put an issue in every group it belongs to, unassigned work still needs a
|
|
11
|
+
* group, empty groups are hidden, and the group order is prescribed per grouping (DESIGN.md 3.2).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** `null` is the "no value" group: Unassigned, No label, No cycle and so on. */
|
|
15
|
+
export type GroupKey = string | null
|
|
16
|
+
|
|
17
|
+
export interface IssueGroup<T> {
|
|
18
|
+
key: GroupKey
|
|
19
|
+
items: T[]
|
|
20
|
+
/** sum of estimates, `null` when nothing in the group is estimated */
|
|
21
|
+
estimate: number | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Everything grouping needs to know that does not live on the issue itself. */
|
|
25
|
+
export interface GroupContext {
|
|
26
|
+
/** display order of a status inside the board and list (workflow order) */
|
|
27
|
+
statusOrder(statusId: string): number
|
|
28
|
+
statusCategory(statusId: string): StatusCategory
|
|
29
|
+
/** display order for the remaining reference fields; unknown ids sort last */
|
|
30
|
+
order?(groupBy: GroupBy, key: string): number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Groupable = Pick<
|
|
34
|
+
Issue,
|
|
35
|
+
| 'statusId'
|
|
36
|
+
| 'statusCategory'
|
|
37
|
+
| 'priority'
|
|
38
|
+
| 'assigneeIds'
|
|
39
|
+
| 'labelIds'
|
|
40
|
+
| 'componentIds'
|
|
41
|
+
| 'versionIds'
|
|
42
|
+
| 'typeId'
|
|
43
|
+
| 'cycleId'
|
|
44
|
+
| 'milestoneId'
|
|
45
|
+
| 'projectId'
|
|
46
|
+
| 'parentId'
|
|
47
|
+
| 'dueDate'
|
|
48
|
+
| 'createdAt'
|
|
49
|
+
| 'estimate'
|
|
50
|
+
| 'rank'
|
|
51
|
+
>
|
|
52
|
+
|
|
53
|
+
/** Every group an issue belongs to under `groupBy`. Multi-valued fields yield more than one. */
|
|
54
|
+
export function groupKeysOf(issue: Groupable, groupBy: GroupBy): GroupKey[] {
|
|
55
|
+
switch (groupBy) {
|
|
56
|
+
case 'status':
|
|
57
|
+
return [issue.statusId]
|
|
58
|
+
case 'statusCategory':
|
|
59
|
+
return [issue.statusCategory]
|
|
60
|
+
case 'priority':
|
|
61
|
+
return [issue.priority]
|
|
62
|
+
case 'type':
|
|
63
|
+
return [issue.typeId]
|
|
64
|
+
case 'project':
|
|
65
|
+
return [issue.projectId]
|
|
66
|
+
case 'assignee':
|
|
67
|
+
return issue.assigneeIds.length ? [...issue.assigneeIds] : [null]
|
|
68
|
+
case 'label':
|
|
69
|
+
return issue.labelIds.length ? [...issue.labelIds] : [null]
|
|
70
|
+
case 'component':
|
|
71
|
+
return issue.componentIds.length ? [...issue.componentIds] : [null]
|
|
72
|
+
case 'version':
|
|
73
|
+
return issue.versionIds.length ? [...issue.versionIds] : [null]
|
|
74
|
+
case 'cycle':
|
|
75
|
+
return [issue.cycleId]
|
|
76
|
+
case 'milestone':
|
|
77
|
+
return [issue.milestoneId]
|
|
78
|
+
case 'parent':
|
|
79
|
+
return [issue.parentId]
|
|
80
|
+
case 'dueDate':
|
|
81
|
+
return [issue.dueDate]
|
|
82
|
+
case 'createdAt':
|
|
83
|
+
return [issue.createdAt.slice(0, 10)]
|
|
84
|
+
default:
|
|
85
|
+
return [null]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Sort weight for a group heading. Lower comes first; the "no value" group always goes last. */
|
|
90
|
+
export function groupOrder(key: GroupKey, groupBy: GroupBy, ctx: GroupContext): number {
|
|
91
|
+
if (key === null) return Number.MAX_SAFE_INTEGER
|
|
92
|
+
switch (groupBy) {
|
|
93
|
+
case 'status':
|
|
94
|
+
return STATUS_CATEGORY_ORDER[ctx.statusCategory(key)] * 1000 + ctx.statusOrder(key)
|
|
95
|
+
case 'statusCategory':
|
|
96
|
+
return STATUS_CATEGORY_ORDER[key as StatusCategory] ?? 900
|
|
97
|
+
case 'priority': {
|
|
98
|
+
const i = PRIORITY_GROUP_ORDER.indexOf(key as Priority)
|
|
99
|
+
return i < 0 ? 900 : i
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
return ctx.order?.(groupBy, key) ?? 0
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Group and order issues for the list and the board.
|
|
108
|
+
*
|
|
109
|
+
* Groups with no rows are dropped, because the design never shows an empty heading. The exception is
|
|
110
|
+
* `alwaysShow`, which is how a board keeps an empty column you can still drop a card into.
|
|
111
|
+
*/
|
|
112
|
+
export function groupIssues<T extends Groupable>(
|
|
113
|
+
issues: readonly T[],
|
|
114
|
+
groupBy: GroupBy,
|
|
115
|
+
ctx: GroupContext,
|
|
116
|
+
opts: { alwaysShow?: readonly GroupKey[]; compare?: (a: T, b: T) => number } = {},
|
|
117
|
+
): Array<IssueGroup<T>> {
|
|
118
|
+
const compare = opts.compare ?? byRank
|
|
119
|
+
const buckets = new Map<string, IssueGroup<T>>()
|
|
120
|
+
const keyId = (k: GroupKey) => (k === null ? ' none' : k)
|
|
121
|
+
|
|
122
|
+
for (const key of opts.alwaysShow ?? []) {
|
|
123
|
+
buckets.set(keyId(key), { key, items: [], estimate: null })
|
|
124
|
+
}
|
|
125
|
+
for (const issue of issues) {
|
|
126
|
+
for (const key of groupKeysOf(issue, groupBy)) {
|
|
127
|
+
const id = keyId(key)
|
|
128
|
+
let bucket = buckets.get(id)
|
|
129
|
+
if (!bucket) {
|
|
130
|
+
bucket = { key, items: [], estimate: null }
|
|
131
|
+
buckets.set(id, bucket)
|
|
132
|
+
}
|
|
133
|
+
bucket.items.push(issue)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const always = new Set((opts.alwaysShow ?? []).map(keyId))
|
|
138
|
+
return [...buckets.values()]
|
|
139
|
+
.filter((g) => g.items.length > 0 || always.has(keyId(g.key)))
|
|
140
|
+
.map((g) => {
|
|
141
|
+
g.items.sort(compare)
|
|
142
|
+
const estimates = g.items.map((i) => i.estimate).filter((e): e is number => typeof e === 'number')
|
|
143
|
+
g.estimate = estimates.length ? estimates.reduce((a, b) => a + b, 0) : null
|
|
144
|
+
return g
|
|
145
|
+
})
|
|
146
|
+
.sort((a, b) => {
|
|
147
|
+
const d = groupOrder(a.key, groupBy, ctx) - groupOrder(b.key, groupBy, ctx)
|
|
148
|
+
return d !== 0 ? d : String(a.key ?? '').localeCompare(String(b.key ?? ''))
|
|
149
|
+
})
|
|
150
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracker client entry.
|
|
3
|
+
*
|
|
4
|
+
* The pieces a host application needs to talk to the tracker and to render it the way `DESIGN.md`
|
|
5
|
+
* describes, with no framework in them: a typed API client, the presentation rules for statuses,
|
|
6
|
+
* priorities and due dates, grouping for the list and the board, fractional ranks for drag and drop,
|
|
7
|
+
* and a KQL reader for the query box.
|
|
8
|
+
*
|
|
9
|
+
* The Svelte views themselves live in the host app (`repos/app/src/lib/modules/tracker`) for now:
|
|
10
|
+
* the shell renders module pages as real SvelteKit routes rather than through `ClientModule.routes`,
|
|
11
|
+
* so shipping components from here would have nowhere to be mounted.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { createTrackerClient, type TrackerApi } from './api.js'
|
|
15
|
+
export {
|
|
16
|
+
type DueTone,
|
|
17
|
+
daysUntil,
|
|
18
|
+
dueTone,
|
|
19
|
+
formatDuration,
|
|
20
|
+
PRIORITY_BAR_GEOMETRY,
|
|
21
|
+
PRIORITY_GROUP_ORDER,
|
|
22
|
+
priorityBars,
|
|
23
|
+
STATUS_CATEGORY_ORDER,
|
|
24
|
+
type StatusStyle,
|
|
25
|
+
type StatusVisual,
|
|
26
|
+
statusStyle,
|
|
27
|
+
} from './format.js'
|
|
28
|
+
export {
|
|
29
|
+
type GroupContext,
|
|
30
|
+
type GroupKey,
|
|
31
|
+
groupIssues,
|
|
32
|
+
groupKeysOf,
|
|
33
|
+
groupOrder,
|
|
34
|
+
type IssueGroup,
|
|
35
|
+
} from './group.js'
|
|
36
|
+
export {
|
|
37
|
+
customKqlField,
|
|
38
|
+
type KqlComparison,
|
|
39
|
+
type KqlExpr,
|
|
40
|
+
type KqlField,
|
|
41
|
+
type KqlOp,
|
|
42
|
+
type KqlOrder,
|
|
43
|
+
type KqlQuery,
|
|
44
|
+
type KqlValue,
|
|
45
|
+
operatorsFor,
|
|
46
|
+
type ParsedKql,
|
|
47
|
+
type ParseOptions,
|
|
48
|
+
parseKql,
|
|
49
|
+
type Span,
|
|
50
|
+
SYSTEM_FIELDS,
|
|
51
|
+
} from './kql.js'
|
|
52
|
+
export {
|
|
53
|
+
byRank,
|
|
54
|
+
initialRank,
|
|
55
|
+
isValidRank,
|
|
56
|
+
rankBetween,
|
|
57
|
+
rankForIndex,
|
|
58
|
+
rankSequence,
|
|
59
|
+
} from './rank.js'
|
|
60
|
+
export type * from './types.js'
|