@vintasoftware/pr-review-canvas 0.3.0 → 0.5.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 +79 -31
- package/docs/reference.md +396 -101
- package/package.json +9 -4
- package/pr-review.config.example.yml +44 -4
- package/prompts/chat-seed.md +3 -0
- package/prompts/generation-format.md +123 -26
- package/prompts/generation-strict-incremental.md +53 -0
- package/prompts/generation-strict.md +1 -27
- package/prompts/generation-surfacing-incremental.md +56 -0
- package/prompts/generation-surfacing.md +1 -58
- package/prompts/judging-strict.md +27 -0
- package/prompts/judging-surfacing.md +58 -0
- package/skills/pr-review-canvas/SKILL.md +81 -42
- package/src/acpx/acpx.ts +98 -5
- package/src/acpx/models.ts +43 -0
- package/src/canvas/comment.ts +24 -0
- package/src/canvas/import.ts +30 -9
- package/src/canvas/name.ts +1 -0
- package/src/canvas/zip.ts +21 -1
- package/src/chat/chat-manager.ts +67 -40
- package/src/chat/threads.ts +27 -26
- package/src/cli.ts +70 -8
- package/src/commands.ts +50 -17
- package/src/config.ts +14 -26
- package/src/contract/api.ts +58 -2
- package/src/contract/canvas-manifest.ts +7 -0
- package/src/contract/comments.ts +5 -0
- package/src/contract/discovery.ts +5 -2
- package/src/contract/generation-context.ts +76 -2
- package/src/contract/keys.ts +1 -0
- package/src/contract/pending.ts +49 -0
- package/src/contract/review-artifact.ts +50 -7
- package/src/contract/review-key.ts +51 -0
- package/src/contract/reviews.ts +26 -0
- package/src/contract/settings.ts +7 -0
- package/src/contract/state.ts +53 -22
- package/src/contract/validation.ts +1 -0
- package/src/git/environment.mjs +27 -0
- package/src/git/git.ts +109 -9
- package/src/git/local-target.ts +138 -0
- package/src/git/patch-lines.ts +34 -2
- package/src/git/pr-refs.ts +36 -0
- package/src/github/attachments.ts +9 -257
- package/src/github/canvas-comment.ts +22 -0
- package/src/github/capabilities.ts +3 -41
- package/src/github/comments.ts +3 -24
- package/src/github/post-comment.ts +3 -36
- package/src/github/post-review.ts +64 -23
- package/src/github/pr.ts +6 -87
- package/src/github/threads.ts +2 -2
- package/src/gitlab/attachments.ts +40 -0
- package/src/gitlab/canvas-comment.ts +26 -0
- package/src/gitlab/capabilities.ts +64 -0
- package/src/gitlab/comments.ts +164 -0
- package/src/gitlab/mr.ts +115 -0
- package/src/gitlab/post-comment.ts +111 -0
- package/src/gitlab/post-review.ts +93 -0
- package/src/gitlab/project.ts +13 -0
- package/src/gitlab/publish-drafts.ts +69 -0
- package/src/host/attachments.ts +293 -0
- package/src/host/capabilities.ts +38 -0
- package/src/host/client.ts +246 -0
- package/src/host/host.ts +154 -0
- package/src/host/pr.ts +51 -0
- package/src/host/remote.ts +42 -0
- package/src/project-config.ts +26 -0
- package/src/review/carry-marks.ts +131 -0
- package/src/review/carry-over.ts +79 -0
- package/src/review/doctor.ts +82 -38
- package/src/review/incremental.ts +107 -0
- package/src/review/normalize.ts +14 -4
- package/src/review/prepare.ts +111 -10
- package/src/review/prompt.ts +112 -5
- package/src/review/publish.ts +62 -10
- package/src/{github → review}/review-body.ts +17 -5
- package/src/review/skill-command.ts +5 -3
- package/src/review/test-paths.ts +44 -4
- package/src/review/validate-folds.ts +349 -24
- package/src/review/validate.ts +14 -5
- package/src/server/bundle.ts +324 -111
- package/src/server/context.ts +10 -8
- package/src/server/errors.ts +30 -8
- package/src/server/html.ts +35 -11
- package/src/server/routes/api.ts +63 -26
- package/src/server/routes/chat-routes.ts +91 -44
- package/src/server/routes/pages.ts +26 -9
- package/src/server/routes/review-routes.ts +278 -67
- package/src/store/canvas-store.ts +93 -55
- package/src/store/data-dir.ts +2 -1
- package/src/store/derived-store.ts +41 -27
- package/src/store/pr-store.ts +23 -14
- package/src/store/settings-store.ts +9 -1
- package/src/store/state-store.ts +105 -36
- package/src/upgrade.ts +338 -0
- package/static/js/api.js +87 -25
- package/static/js/app.js +50 -15
- package/static/js/chat-panel.js +32 -9
- package/static/js/chat.js +30 -6
- package/static/js/code-folds.js +171 -44
- package/static/js/composer.js +132 -17
- package/static/js/contract-types.d.ts +7 -0
- package/static/js/diff-decorations.js +67 -1
- package/static/js/diff-renderer.js +1 -1
- package/static/js/download.js +1 -1
- package/static/js/empty-state.js +102 -18
- package/static/js/errors.js +22 -6
- package/static/js/fold-levels.js +176 -0
- package/static/js/header.js +65 -15
- package/static/js/host.js +40 -0
- package/static/js/import-zone.js +1 -1
- package/static/js/interactions.js +274 -46
- package/static/js/keyboard.js +4 -1
- package/static/js/keys.js +12 -0
- package/static/js/layers.js +295 -32
- package/static/js/links.js +3 -3
- package/static/js/markdown.js +28 -1
- package/static/js/nav.js +22 -4
- package/static/js/pending.js +161 -0
- package/static/js/points.js +70 -9
- package/static/js/progress.js +4 -5
- package/static/js/quick-questions.js +15 -2
- package/static/js/reading-level.js +97 -0
- package/static/js/review-session.js +113 -29
- package/static/js/settings.js +55 -24
- package/static/js/signoff.js +79 -10
- package/static/js/skin.js +2 -2
- package/static/styles/chat-panel.css +22 -24
- package/static/styles/chat.css +4 -0
- package/static/styles/commands.css +6 -0
- package/static/styles/header.css +21 -0
- package/static/styles/panels.css +4 -0
- package/static/styles/pending.css +102 -0
- package/static/styles/review-actions.css +1 -0
- package/static/styles/review.css +4 -0
- package/static/styles/skin-github.css +7 -1
- package/static/styles.css +1 -0
- package/src/github/gh.ts +0 -211
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// How aggressively the canvas hides code. One implementation for the server (re-exported by
|
|
3
|
+
// src/contract/review-artifact.ts) and the browser, so the validator and the page agree on what
|
|
4
|
+
// a level hides.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The levels, from the least hiding to the most. They nest: a fold hides at its own level and at
|
|
8
|
+
* every level above it, so `light` folds are hidden in all three modes. The page opens at the
|
|
9
|
+
* level saved in the settings file, `light` until the reader picks another, and a change from
|
|
10
|
+
* the control holds for the session only.
|
|
11
|
+
*/
|
|
12
|
+
export const FOLD_LEVELS = /** @type {const} */ (['light', 'moderate', 'aggressive'])
|
|
13
|
+
|
|
14
|
+
/** @typedef {(typeof FOLD_LEVELS)[number]} FoldLevel */
|
|
15
|
+
|
|
16
|
+
/** The level a review opens at until the reader saves another, and the level of an older canvas's folds. */
|
|
17
|
+
export const DEFAULT_FOLD_LEVEL = 'light'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {unknown} value
|
|
21
|
+
* @returns {value is FoldLevel}
|
|
22
|
+
*/
|
|
23
|
+
export function isFoldLevel(value) {
|
|
24
|
+
return FOLD_LEVELS.some(level => level === value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where a level sits in the order, so two levels can be compared.
|
|
29
|
+
* @param {FoldLevel} level
|
|
30
|
+
* @returns {number}
|
|
31
|
+
*/
|
|
32
|
+
export function foldLevelRank(level) {
|
|
33
|
+
return FOLD_LEVELS.indexOf(level)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether code marked with `level` is hidden while the reader is at `current`.
|
|
38
|
+
* @param {FoldLevel} level
|
|
39
|
+
* @param {FoldLevel} current
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function hidesAt(level, current) {
|
|
43
|
+
return foldLevelRank(level) <= foldLevelRank(current)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The level after `level`, back to the first after the last, so one key can step through them.
|
|
48
|
+
* @param {FoldLevel} level
|
|
49
|
+
* @returns {FoldLevel}
|
|
50
|
+
*/
|
|
51
|
+
export function nextFoldLevel(level) {
|
|
52
|
+
return FOLD_LEVELS[(foldLevelRank(level) + 1) % FOLD_LEVELS.length] ?? DEFAULT_FOLD_LEVEL
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {{ side: 'new' | 'old', startLine: number, endLine: number }} Span
|
|
57
|
+
* @typedef {Span & { level: FoldLevel }} Range
|
|
58
|
+
* @typedef {{
|
|
59
|
+
* collapsed?: FoldLevel | undefined,
|
|
60
|
+
* annotations: readonly Span[],
|
|
61
|
+
* folds?: readonly Range[] | undefined,
|
|
62
|
+
* hunks: readonly string[],
|
|
63
|
+
* }} FoldedFile
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether `inner` sits wholly inside `outer` without being the same range. The validator allows
|
|
68
|
+
* this only when the inner fold has the lower level; the page then draws the outer one alone.
|
|
69
|
+
* @param {Span} outer
|
|
70
|
+
* @param {Span} inner
|
|
71
|
+
*/
|
|
72
|
+
export function contains(outer, inner) {
|
|
73
|
+
return (
|
|
74
|
+
outer !== inner &&
|
|
75
|
+
outer.side === inner.side &&
|
|
76
|
+
outer.startLine <= inner.startLine &&
|
|
77
|
+
inner.endLine <= outer.endLine &&
|
|
78
|
+
(outer.startLine < inner.startLine || inner.endLine < outer.endLine)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The folds that hide at `level`, with every fold nested inside another kept fold dropped. Only
|
|
84
|
+
* the outermost fold is drawn, so raising the level replaces a set of small titles with one.
|
|
85
|
+
* @template {Range} T
|
|
86
|
+
* @param {readonly T[]} folds
|
|
87
|
+
* @param {FoldLevel} level
|
|
88
|
+
* @returns {T[]}
|
|
89
|
+
*/
|
|
90
|
+
export function foldsForLevel(folds, level) {
|
|
91
|
+
const applicable = folds.filter(fold => hidesAt(fold.level, level))
|
|
92
|
+
return applicable.filter(fold => !applicable.some(other => contains(other, fold)))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether a file's whole body starts hidden at this level. An annotated file never collapses: a
|
|
97
|
+
* fold over an annotation still shows the annotation's text, a collapsed file shows only its path.
|
|
98
|
+
* @param {FoldedFile} file
|
|
99
|
+
* @param {FoldLevel} level
|
|
100
|
+
* @returns {boolean}
|
|
101
|
+
*/
|
|
102
|
+
export function collapsesAt(file, level) {
|
|
103
|
+
return file.collapsed !== undefined && hidesAt(file.collapsed, level) && file.annotations.length === 0
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Rows a set of ranges covers, each row counted once where ranges nest or touch. The one count
|
|
108
|
+
* the page's counters and the validator's thresholds share.
|
|
109
|
+
* @param {readonly Span[]} ranges
|
|
110
|
+
* @returns {number}
|
|
111
|
+
*/
|
|
112
|
+
export function coveredRows(ranges) {
|
|
113
|
+
let total = 0
|
|
114
|
+
for (const side of /** @type {const} */ (['new', 'old'])) {
|
|
115
|
+
const sorted = ranges
|
|
116
|
+
.filter(range => range.side === side)
|
|
117
|
+
.map(range => ({ start: range.startLine, end: range.endLine }))
|
|
118
|
+
.sort((a, b) => a.start - b.start)
|
|
119
|
+
/** @type {{ start: number, end: number } | null} */
|
|
120
|
+
let current = null
|
|
121
|
+
for (const range of sorted) {
|
|
122
|
+
if (current !== null && range.start <= current.end + 1) {
|
|
123
|
+
current.end = Math.max(current.end, range.end)
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
total += current === null ? 0 : current.end - current.start + 1
|
|
127
|
+
current = { ...range }
|
|
128
|
+
}
|
|
129
|
+
total += current === null ? 0 : current.end - current.start + 1
|
|
130
|
+
}
|
|
131
|
+
return total
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Rows a file's hunks draw, near enough for a counter: context lines are shared by both sides,
|
|
136
|
+
* so the longer side is the row count when nothing else is known about the patch.
|
|
137
|
+
* @param {FoldedFile} file
|
|
138
|
+
* @param {ReadonlyArray<{ id: string, oldLines: number, newLines: number }>} hunks every hunk of the file
|
|
139
|
+
* @returns {number}
|
|
140
|
+
*/
|
|
141
|
+
export function fileRows(file, hunks) {
|
|
142
|
+
return hunks
|
|
143
|
+
.filter(hunk => file.hunks.includes(hunk.id))
|
|
144
|
+
.reduce((sum, hunk) => sum + Math.max(hunk.oldLines, hunk.newLines), 0)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* What the review's discussion keeps open in one file card: `keepsOpen` when a comment or an
|
|
149
|
+
* attention point stops the card collapsing, `discussed` when a thread or a pending draft in its
|
|
150
|
+
* chunks stops every fold. The page decides both once per card, and the card and its counter read
|
|
151
|
+
* the answer.
|
|
152
|
+
* @typedef {{ keepsOpen: boolean, discussed: boolean }} Discussion
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
/** A file nobody has discussed yet, as the validator sees every file of a fresh generation. */
|
|
156
|
+
export const UNDISCUSSED = /** @type {Discussion} */ ({ keepsOpen: false, discussed: false })
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* How many diff lines a file shows, and how many of them `level` hides. Measured from the model,
|
|
160
|
+
* so the counter reads the same before and after a card draws its diff.
|
|
161
|
+
* @param {FoldedFile} file
|
|
162
|
+
* @param {ReadonlyArray<{ id: string, oldLines: number, newLines: number }>} hunks every hunk of the file
|
|
163
|
+
* @param {FoldLevel} level
|
|
164
|
+
* @param {Discussion} discussion
|
|
165
|
+
* @returns {{ total: number, hidden: number }}
|
|
166
|
+
*/
|
|
167
|
+
export function hiddenLines(file, hunks, level, discussion) {
|
|
168
|
+
const total = fileRows(file, hunks)
|
|
169
|
+
if (collapsesAt(file, level) && !discussion.keepsOpen) {
|
|
170
|
+
return { total, hidden: total }
|
|
171
|
+
}
|
|
172
|
+
if (discussion.discussed) {
|
|
173
|
+
return { total, hidden: 0 }
|
|
174
|
+
}
|
|
175
|
+
return { total, hidden: coveredRows(foldsForLevel(file.folds ?? [], level)) }
|
|
176
|
+
}
|
package/static/js/header.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
/** @typedef {import('./contract-types.js').RiskTag} RiskTag */
|
|
4
4
|
import { setDisabledReason } from './composer.js'
|
|
5
5
|
import { esc, timeAgo } from './dom.js'
|
|
6
|
-
import {
|
|
6
|
+
import { authorProfileUrl, currentHost, hostLabel } from './host.js'
|
|
7
|
+
import { canvasHiddenLines, getFoldLevel, refreshRail } from './layers.js'
|
|
8
|
+
import { pendingBarHtml, pendingCount } from './pending.js'
|
|
7
9
|
import { progressSummary } from './progress.js'
|
|
10
|
+
import { foldLevelControlHtml } from './reading-level.js'
|
|
8
11
|
import { approveBlockedReason } from './signoff.js'
|
|
9
12
|
import { skinLabel } from './skin.js'
|
|
10
13
|
import { themeLabel } from './theme.js'
|
|
@@ -41,6 +44,19 @@ export function largePrNoticeHtml(bundle) {
|
|
|
41
44
|
)
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The author, linked to their page on the forge. A local review's author is the name this clone
|
|
49
|
+
* commits as, which is no forge login, so it stays plain text.
|
|
50
|
+
* @param {string} author
|
|
51
|
+
* @param {boolean} local
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function authorHtml(author, local) {
|
|
55
|
+
return local
|
|
56
|
+
? esc(author)
|
|
57
|
+
: `<a href="${esc(authorProfileUrl(author))}" target="_blank" rel="noopener noreferrer">${esc(author)}</a>`
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
/** @param {PrBundle['pr']} pr */
|
|
45
61
|
export function statePill(pr) {
|
|
46
62
|
const state = pr.state === 'open' && pr.draft ? 'draft' : pr.state
|
|
@@ -61,47 +77,81 @@ export function renderHeader(bundle, opts) {
|
|
|
61
77
|
? `<span class="pill agent">${esc(artifact.generator.agent)}${artifact.generator.model ? ` · ${esc(artifact.generator.model)}` : ''} · ${esc(artifact.generator.harness)}</span><span>generated ${esc(timeAgo(artifact.generatedAt, opts.now))}</span>`
|
|
62
78
|
: ''
|
|
63
79
|
const number = pr.number === null ? '' : `<span class="mono num">#${pr.number}</span>`
|
|
64
|
-
|
|
80
|
+
// Work that is not pushed has no page on the forge, and nothing to refresh from it either.
|
|
81
|
+
const local = bundle.local !== undefined
|
|
82
|
+
const forgeLink =
|
|
83
|
+
pr.url === ''
|
|
84
|
+
? ''
|
|
85
|
+
: `<a class="cmd" href="${esc(pr.url)}" target="_blank" rel="noopener noreferrer">${esc(currentHost().kind)}</a>`
|
|
86
|
+
const refreshTitle = local
|
|
87
|
+
? 'Snapshot the working tree again and redraw'
|
|
88
|
+
: `Fetch the latest PR, comments, and shared canvas from ${esc(hostLabel())}`
|
|
89
|
+
const progress = ready ? progressHtml(artifact, bundle.state, pr.headSha) : ''
|
|
90
|
+
const level = getFoldLevel()
|
|
91
|
+
const reading = ready
|
|
92
|
+
? foldLevelControlHtml(
|
|
93
|
+
level,
|
|
94
|
+
canvasHiddenLines(
|
|
95
|
+
{
|
|
96
|
+
artifact,
|
|
97
|
+
files: bundle.files,
|
|
98
|
+
comments: bundle.comments.reviewComments,
|
|
99
|
+
state: bundle.state,
|
|
100
|
+
headSha: pr.headSha,
|
|
101
|
+
},
|
|
102
|
+
level
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
: ''
|
|
65
106
|
const risk = ready ? riskLineHtml(artifact.risk) : ''
|
|
66
107
|
return (
|
|
67
108
|
'<header class="hdr">' +
|
|
68
109
|
`<div class="hdr-bar"><div class="brand"><span class="brand-wordmark"><img class="brand-icon" src="/static/brand.svg" width="32" height="32" alt="">PR review canvas</span><span class="mono muted">${esc(opts.host)}</span></div>` +
|
|
69
110
|
'<div class="hdr-actions" role="group" aria-label="Canvas actions">' +
|
|
70
|
-
`<button class="cmd" type="button" id="regenerate" title="Generate a new canvas for this PR" aria-haspopup="dialog"${hasCanvas ? '' : ' disabled'}>regenerate</button>` +
|
|
71
|
-
`<button class="cmd" type="button" id="export-zip" title="Download this canvas as a zip to share on
|
|
72
|
-
|
|
73
|
-
|
|
111
|
+
`<button class="cmd" type="button" id="regenerate" title="Generate a new canvas for ${local ? 'this local work' : 'this PR'}" aria-haspopup="dialog"${hasCanvas ? '' : ' disabled'}>regenerate</button>` +
|
|
112
|
+
`<button class="cmd" type="button" id="export-zip" title="Download this canvas as a zip to share on ${esc(hostLabel())}"${hasCanvas ? '' : ' disabled'}>export zip</button>` +
|
|
113
|
+
`<button class="cmd" type="button" id="refresh" title="${refreshTitle}">refresh</button>` +
|
|
114
|
+
'<button class="cmd" type="button" id="settings" data-act="settings" aria-haspopup="dialog" title="Configure the default reading level and the AI chat agent, model, and limits">settings</button>' +
|
|
74
115
|
'<button class="cmd" type="button" data-act="help" title="Show keyboard shortcuts and review help" aria-haspopup="dialog">help</button>' +
|
|
75
116
|
`<button class="cmd" type="button" id="skin-toggle" title="Switch between Terminal and GitHub styling">${esc(skinLabel(opts.skin))}</button>` +
|
|
76
117
|
`<button class="cmd" type="button" id="theme-toggle" title="Switch between Light, Dark, and Auto themes">${esc(themeLabel(opts.theme))}</button>` +
|
|
77
118
|
'</div></div>' +
|
|
78
119
|
'<div class="stripe" aria-hidden="true"></div>' +
|
|
79
120
|
'<div class="hdr-title">' +
|
|
80
|
-
`<div class="title"><h1>${number}${esc(pr.title)}</h1
|
|
81
|
-
`<p class="meta"><span>by
|
|
121
|
+
`<div class="title"><h1>${number}${esc(pr.title)}</h1>${forgeLink}</div>` +
|
|
122
|
+
`<p class="meta"><span>by ${authorHtml(pr.author, local)}</span>` +
|
|
82
123
|
`<span class="mono">${esc(pr.headRef)} → ${esc(pr.baseRef)}</span>${statePill(pr)}` +
|
|
83
124
|
`<span class="diffstat"><span class="ok">+${pr.additions}</span> <span class="bad">−${pr.deletions}</span></span>${agent}</p>` +
|
|
84
|
-
`${largePrNoticeHtml(bundle)}${risk}${progress}</div></header>`
|
|
125
|
+
`${largePrNoticeHtml(bundle)}${risk}${reading}${progress}</div></header>`
|
|
85
126
|
)
|
|
86
127
|
}
|
|
87
128
|
|
|
88
129
|
/**
|
|
89
|
-
* The thin line, its text, and the
|
|
90
|
-
* reason until every layer that is not Other has been marked reviewed
|
|
130
|
+
* The thin line, its text, the pending-review bar, and the three sign-off commands. Approve stays
|
|
131
|
+
* disabled with the reason until every layer that is not Other has been marked reviewed; a
|
|
132
|
+
* comment-only review and a request for changes are always allowed, since neither claims the
|
|
133
|
+
* change set was read in full.
|
|
91
134
|
* @param {import('./contract-types.js').ReviewArtifact} artifact
|
|
92
135
|
* @param {import('./contract-types.js').PrState} state
|
|
93
136
|
*/
|
|
94
|
-
export function progressHtml(artifact, state) {
|
|
137
|
+
export function progressHtml(artifact, state, headSha = artifact.pr.headSha) {
|
|
95
138
|
const p = progressSummary(artifact, state)
|
|
96
139
|
const blocked = approveBlockedReason(artifact, state)
|
|
140
|
+
const approveTitle = `Write and preview an approving review on ${esc(hostLabel())}`
|
|
97
141
|
const approve =
|
|
98
|
-
`<button class="cmd" type="button" id="approve" data-tooltip="
|
|
99
|
-
`${blocked === null ?
|
|
142
|
+
`<button class="cmd" type="button" id="approve" data-tooltip="${approveTitle}" data-act="signoff" data-event="APPROVE" data-needs-post` +
|
|
143
|
+
`${blocked === null ? ` title="${approveTitle}"` : ` disabled data-disabled-reason="${esc(blocked)}" title="${esc(blocked)}"`}>approve on ${esc(currentHost().kind)}</button>`
|
|
144
|
+
const commentTitle = `Write and preview a review with no verdict on ${esc(hostLabel())}`
|
|
100
145
|
return (
|
|
101
146
|
`<div class="progress"><div class="pline" role="progressbar" aria-valuenow="${p.done}" aria-valuemin="0" aria-valuemax="${p.total}" aria-label="Layers reviewed"><span style="width:${p.percent}%"></span></div>` +
|
|
102
147
|
`<span class="ptext">${p.done} of ${p.total} layers reviewed</span></div>` +
|
|
148
|
+
`<div class="pending-bar-host${pendingCount(state) > 0 ? ' has-pending' : ''}">${pendingBarHtml(
|
|
149
|
+
pendingCount(state),
|
|
150
|
+
state.pending.filter(draft => draft.headSha !== headSha)
|
|
151
|
+
)}</div>` +
|
|
103
152
|
`<div class="signoff">${approve}` +
|
|
104
|
-
|
|
153
|
+
`<button class="cmd" type="button" id="request-changes" data-tooltip="Write and preview a review requesting changes on ${esc(hostLabel())}" title="Write and preview a review requesting changes on ${esc(hostLabel())}" data-act="signoff" data-event="REQUEST_CHANGES" data-needs-post>request changes</button>` +
|
|
154
|
+
`<button class="cmd" type="button" id="comment-review" data-tooltip="${commentTitle}" title="${commentTitle}" data-act="signoff" data-event="COMMENT" data-needs-post>comment</button>` +
|
|
105
155
|
'<span class="capability-note" role="status"></span></div>'
|
|
106
156
|
)
|
|
107
157
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// The forge this server talks to, as the page shell's bootstrap names it. One server serves one
|
|
3
|
+
// host, so the words and links that depend on it are read from here rather than threaded through
|
|
4
|
+
// every renderer.
|
|
5
|
+
/** @typedef {import('./contract-types.js').PublicHost} PublicHost */
|
|
6
|
+
|
|
7
|
+
/** @type {PublicHost} */
|
|
8
|
+
const DEFAULT_HOST = { kind: 'github', label: 'GitHub', webBase: 'https://github.com' }
|
|
9
|
+
|
|
10
|
+
/** @type {PublicHost} */
|
|
11
|
+
let current = DEFAULT_HOST
|
|
12
|
+
|
|
13
|
+
/** @param {PublicHost | undefined} host */
|
|
14
|
+
export function setHost(host) {
|
|
15
|
+
current = host ?? DEFAULT_HOST
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function currentHost() {
|
|
19
|
+
return current
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `GitHub` or `GitLab`, for sentences. */
|
|
23
|
+
export function hostLabel() {
|
|
24
|
+
return current.label
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `post to github` / `post to gitlab`, the command label. */
|
|
28
|
+
export function postToLabel() {
|
|
29
|
+
return `post to ${current.kind}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What a command says when this login may not post. */
|
|
33
|
+
export function noPostingTitle() {
|
|
34
|
+
return `this ${current.label} login cannot post on this repository`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @param {string} author */
|
|
38
|
+
export function authorProfileUrl(author) {
|
|
39
|
+
return `${current.webBase}/${author}`
|
|
40
|
+
}
|
package/static/js/import-zone.js
CHANGED
|
@@ -56,7 +56,7 @@ export function wireDropZone(root, opts) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const send = async (/** @type {File} */ file) => {
|
|
59
|
-
const problem = validateCanvasFilename(file.name)
|
|
59
|
+
const problem = validateCanvasFilename(file.name, opts.prNumber)
|
|
60
60
|
if (problem !== null) {
|
|
61
61
|
apply({ type: 'fail', message: problem })
|
|
62
62
|
return
|