@storyshelf/app 0.5.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/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/app-types.d.mts +54 -0
- package/dist/asset-manifest.mjs +41 -0
- package/dist/asset-manifest.mjs.map +1 -0
- package/dist/asset-paths.mjs +23 -0
- package/dist/asset-paths.mjs.map +1 -0
- package/dist/assets/htmx.min.js +3304 -0
- package/dist/assets/mark.svg +1 -0
- package/dist/capture-setup.mjs +139 -0
- package/dist/capture-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +20 -0
- package/dist/credential-keys.mjs.map +1 -0
- package/dist/credentials.mjs +56 -0
- package/dist/credentials.mjs.map +1 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +310 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lifecycle.mjs +132 -0
- package/dist/lifecycle.mjs.map +1 -0
- package/dist/middleware/auth-gate.mjs +36 -0
- package/dist/middleware/auth-gate.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +75 -0
- package/dist/middleware/csrf.mjs.map +1 -0
- package/dist/middleware/rate-limit.mjs +68 -0
- package/dist/middleware/rate-limit.mjs.map +1 -0
- package/dist/middleware/request-log.mjs +30 -0
- package/dist/middleware/request-log.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +39 -0
- package/dist/middleware/server-timing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs +37 -0
- package/dist/middleware/setup-gate.mjs.map +1 -0
- package/dist/middleware/store-scope.mjs +33 -0
- package/dist/middleware/store-scope.mjs.map +1 -0
- package/dist/notify.d.mts +17 -0
- package/dist/notify.mjs +141 -0
- package/dist/notify.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +243 -0
- package/dist/pages/admin-system.mjs.map +1 -0
- package/dist/pages/build-detail.mjs +392 -0
- package/dist/pages/build-detail.mjs.map +1 -0
- package/dist/pages/build-diff-actions.mjs +162 -0
- package/dist/pages/build-diff-actions.mjs.map +1 -0
- package/dist/pages/build-diff-comments.mjs +104 -0
- package/dist/pages/build-diff-comments.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +117 -0
- package/dist/pages/build-diff-header.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +48 -0
- package/dist/pages/build-diff-model.mjs.map +1 -0
- package/dist/pages/build-diff-nav.mjs +122 -0
- package/dist/pages/build-diff-nav.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +78 -0
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
- package/dist/pages/build-diff-stale.mjs +70 -0
- package/dist/pages/build-diff-stale.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +70 -0
- package/dist/pages/build-diff-viewer.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +110 -0
- package/dist/pages/build-diff.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +165 -0
- package/dist/pages/compute-jobs.mjs.map +1 -0
- package/dist/pages/invite.mjs +91 -0
- package/dist/pages/invite.mjs.map +1 -0
- package/dist/pages/label-detail.mjs +154 -0
- package/dist/pages/label-detail.mjs.map +1 -0
- package/dist/pages/library.mjs +343 -0
- package/dist/pages/library.mjs.map +1 -0
- package/dist/pages/login.mjs +117 -0
- package/dist/pages/login.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +97 -0
- package/dist/pages/passkey-ceremony.mjs.map +1 -0
- package/dist/pages/profile.mjs +364 -0
- package/dist/pages/profile.mjs.map +1 -0
- package/dist/pages/project-builds-table.mjs +151 -0
- package/dist/pages/project-builds-table.mjs.map +1 -0
- package/dist/pages/project-builds.mjs +143 -0
- package/dist/pages/project-builds.mjs.map +1 -0
- package/dist/pages/project-create.mjs +90 -0
- package/dist/pages/project-create.mjs.map +1 -0
- package/dist/pages/project-settings.mjs +95 -0
- package/dist/pages/project-settings.mjs.map +1 -0
- package/dist/pages/projects-card.mjs +194 -0
- package/dist/pages/projects-card.mjs.map +1 -0
- package/dist/pages/projects.mjs +129 -0
- package/dist/pages/projects.mjs.map +1 -0
- package/dist/pages/root.mjs +98 -0
- package/dist/pages/root.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +138 -0
- package/dist/pages/settings-general.mjs.map +1 -0
- package/dist/pages/settings-labels.mjs +107 -0
- package/dist/pages/settings-labels.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +192 -0
- package/dist/pages/settings-members.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +240 -0
- package/dist/pages/settings-notifications.mjs.map +1 -0
- package/dist/pages/settings-status.mjs +137 -0
- package/dist/pages/settings-status.mjs.map +1 -0
- package/dist/pages/settings-tests.mjs +111 -0
- package/dist/pages/settings-tests.mjs.map +1 -0
- package/dist/pages/settings-tokens.mjs +90 -0
- package/dist/pages/settings-tokens.mjs.map +1 -0
- package/dist/pages/settings-webhooks.mjs +104 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -0
- package/dist/pages/storybook.mjs +116 -0
- package/dist/pages/storybook.mjs.map +1 -0
- package/dist/retention-timer.mjs +65 -0
- package/dist/retention-timer.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +48 -0
- package/dist/routers/admin-pages.mjs.map +1 -0
- package/dist/routers/admin.mjs +113 -0
- package/dist/routers/admin.mjs.map +1 -0
- package/dist/routers/assets.mjs +64 -0
- package/dist/routers/assets.mjs.map +1 -0
- package/dist/routers/attempts.mjs +96 -0
- package/dist/routers/attempts.mjs.map +1 -0
- package/dist/routers/auth-sync.mjs +64 -0
- package/dist/routers/auth-sync.mjs.map +1 -0
- package/dist/routers/auth.mjs +236 -0
- package/dist/routers/auth.mjs.map +1 -0
- package/dist/routers/baseline-guard.mjs +83 -0
- package/dist/routers/baseline-guard.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +217 -0
- package/dist/routers/builds.handlers.mjs.map +1 -0
- package/dist/routers/builds.mjs +381 -0
- package/dist/routers/builds.mjs.map +1 -0
- package/dist/routers/comments.mjs +109 -0
- package/dist/routers/comments.mjs.map +1 -0
- package/dist/routers/flash.mjs +35 -0
- package/dist/routers/flash.mjs.map +1 -0
- package/dist/routers/health-report.mjs +159 -0
- package/dist/routers/health-report.mjs.map +1 -0
- package/dist/routers/health.mjs +46 -0
- package/dist/routers/health.mjs.map +1 -0
- package/dist/routers/helpers.mjs +110 -0
- package/dist/routers/helpers.mjs.map +1 -0
- package/dist/routers/htmx.mjs +24 -0
- package/dist/routers/htmx.mjs.map +1 -0
- package/dist/routers/labels.mjs +118 -0
- package/dist/routers/labels.mjs.map +1 -0
- package/dist/routers/media.mjs +53 -0
- package/dist/routers/media.mjs.map +1 -0
- package/dist/routers/members.mjs +177 -0
- package/dist/routers/members.mjs.map +1 -0
- package/dist/routers/notification-channels.mjs +190 -0
- package/dist/routers/notification-channels.mjs.map +1 -0
- package/dist/routers/notification-subscriptions.mjs +120 -0
- package/dist/routers/notification-subscriptions.mjs.map +1 -0
- package/dist/routers/profile.mjs +276 -0
- package/dist/routers/profile.mjs.map +1 -0
- package/dist/routers/projects.mjs +133 -0
- package/dist/routers/projects.mjs.map +1 -0
- package/dist/routers/review-next.mjs +47 -0
- package/dist/routers/review-next.mjs.map +1 -0
- package/dist/routers/schemas.mjs +337 -0
- package/dist/routers/schemas.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +104 -0
- package/dist/routers/settings-general.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +52 -0
- package/dist/routers/settings-labels.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +58 -0
- package/dist/routers/settings-members.mjs.map +1 -0
- package/dist/routers/settings-notifications.mjs +108 -0
- package/dist/routers/settings-notifications.mjs.map +1 -0
- package/dist/routers/settings-status.mjs +77 -0
- package/dist/routers/settings-status.mjs.map +1 -0
- package/dist/routers/settings-tokens.mjs +40 -0
- package/dist/routers/settings-tokens.mjs.map +1 -0
- package/dist/routers/settings-webhooks.mjs +67 -0
- package/dist/routers/settings-webhooks.mjs.map +1 -0
- package/dist/routers/settings.handlers.mjs +149 -0
- package/dist/routers/settings.handlers.mjs.map +1 -0
- package/dist/routers/settings.mjs +25 -0
- package/dist/routers/settings.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +240 -0
- package/dist/routers/snapshots.mjs.map +1 -0
- package/dist/routers/status-configs.mjs +108 -0
- package/dist/routers/status-configs.mjs.map +1 -0
- package/dist/routers/storybook.mjs +171 -0
- package/dist/routers/storybook.mjs.map +1 -0
- package/dist/routers/tokens.mjs +91 -0
- package/dist/routers/tokens.mjs.map +1 -0
- package/dist/routers/ui.mjs +166 -0
- package/dist/routers/ui.mjs.map +1 -0
- package/dist/routers/webhooks.mjs +97 -0
- package/dist/routers/webhooks.mjs.map +1 -0
- package/dist/routers/well-known.mjs +43 -0
- package/dist/routers/well-known.mjs.map +1 -0
- package/dist/runtime.mjs +52 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/store.mjs +20 -0
- package/dist/store.mjs.map +1 -0
- package/dist/ui/avatar.mjs +76 -0
- package/dist/ui/avatar.mjs.map +1 -0
- package/dist/ui/buttons.mjs +205 -0
- package/dist/ui/buttons.mjs.map +1 -0
- package/dist/ui/code-block.mjs +47 -0
- package/dist/ui/code-block.mjs.map +1 -0
- package/dist/ui/compare-stage.mjs +344 -0
- package/dist/ui/compare-stage.mjs.map +1 -0
- package/dist/ui/csrf-field.mjs +20 -0
- package/dist/ui/csrf-field.mjs.map +1 -0
- package/dist/ui/css.mjs +36 -0
- package/dist/ui/css.mjs.map +1 -0
- package/dist/ui/document.mjs +130 -0
- package/dist/ui/document.mjs.map +1 -0
- package/dist/ui/dropdown.mjs +181 -0
- package/dist/ui/dropdown.mjs.map +1 -0
- package/dist/ui/feedback.mjs +341 -0
- package/dist/ui/feedback.mjs.map +1 -0
- package/dist/ui/filter-input.mjs +58 -0
- package/dist/ui/filter-input.mjs.map +1 -0
- package/dist/ui/forms.mjs +291 -0
- package/dist/ui/forms.mjs.map +1 -0
- package/dist/ui/icons/icon.mjs +53 -0
- package/dist/ui/icons/icon.mjs.map +1 -0
- package/dist/ui/icons/paths.mjs +61 -0
- package/dist/ui/icons/paths.mjs.map +1 -0
- package/dist/ui/icons/sprite.mjs +26 -0
- package/dist/ui/icons/sprite.mjs.map +1 -0
- package/dist/ui/kbd.mjs +33 -0
- package/dist/ui/kbd.mjs.map +1 -0
- package/dist/ui/layout.mjs +173 -0
- package/dist/ui/layout.mjs.map +1 -0
- package/dist/ui/progress.mjs +44 -0
- package/dist/ui/progress.mjs.map +1 -0
- package/dist/ui/segmented.mjs +77 -0
- package/dist/ui/segmented.mjs.map +1 -0
- package/dist/ui/shell/account.mjs +161 -0
- package/dist/ui/shell/account.mjs.map +1 -0
- package/dist/ui/shell/bare.mjs +69 -0
- package/dist/ui/shell/bare.mjs.map +1 -0
- package/dist/ui/shell/boot-script.mjs +46 -0
- package/dist/ui/shell/boot-script.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +259 -0
- package/dist/ui/shell/client-script.mjs.map +1 -0
- package/dist/ui/shell/nav.mjs +111 -0
- package/dist/ui/shell/nav.mjs.map +1 -0
- package/dist/ui/shell/review-script.mjs +195 -0
- package/dist/ui/shell/review-script.mjs.map +1 -0
- package/dist/ui/shell/sidebar.mjs +323 -0
- package/dist/ui/shell/sidebar.mjs.map +1 -0
- package/dist/ui/shell/topbar.mjs +268 -0
- package/dist/ui/shell/topbar.mjs.map +1 -0
- package/dist/ui/stacks.mjs +82 -0
- package/dist/ui/stacks.mjs.map +1 -0
- package/dist/ui/styles/base.mjs +41 -0
- package/dist/ui/styles/base.mjs.map +1 -0
- package/dist/ui/styles/behavior.mjs +43 -0
- package/dist/ui/styles/behavior.mjs.map +1 -0
- package/dist/ui/styles/review-layout.mjs +347 -0
- package/dist/ui/styles/review-layout.mjs.map +1 -0
- package/dist/ui/styles/review.mjs +86 -0
- package/dist/ui/styles/review.mjs.map +1 -0
- package/dist/ui/styles/tokens.mjs +132 -0
- package/dist/ui/styles/tokens.mjs.map +1 -0
- package/dist/ui/styles.mjs +19 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/subnav.mjs +91 -0
- package/dist/ui/subnav.mjs.map +1 -0
- package/dist/ui/table.mjs +73 -0
- package/dist/ui/table.mjs.map +1 -0
- package/dist/ui/theme.mjs +64 -0
- package/dist/ui/theme.mjs.map +1 -0
- package/dist/ui/thumbnail.mjs +51 -0
- package/dist/ui/thumbnail.mjs.map +1 -0
- package/dist/ui/time.mjs +50 -0
- package/dist/ui/time.mjs.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { Button } from "../ui/buttons.mjs";
|
|
6
|
+
import { Badge, EmptyState, Meta, Stat, statusTone } from "../ui/feedback.mjs";
|
|
7
|
+
import { TextareaField } from "../ui/forms.mjs";
|
|
8
|
+
import { Card, PageHeader, SectionTitle } from "../ui/layout.mjs";
|
|
9
|
+
import { HStack, VStack } from "../ui/stacks.mjs";
|
|
10
|
+
import { Thumbnail } from "../ui/thumbnail.mjs";
|
|
11
|
+
import { DocumentLayout } from "../ui/document.mjs";
|
|
12
|
+
import { loadBaselineView } from "../routers/baseline-guard.mjs";
|
|
13
|
+
import { reviewComment, reviewCommentBody, reviewCommentHead, snapshotCard, snapshotCardBody, snapshotCardHead, snapshotCardMeta, snapshotGrid } from "../ui/styles/review.mjs";
|
|
14
|
+
import { StaleBaselineNotice } from "./build-diff-stale.mjs";
|
|
15
|
+
import { BuildModel, CaptureAttemptModel, CaptureLogModel, CommentModel, ProjectModel, SnapshotModel } from "@storyshelf/core/models";
|
|
16
|
+
import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
17
|
+
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
18
|
+
//#region src/pages/build-detail.tsx
|
|
19
|
+
/** Map a capture log level to its badge tone. */
|
|
20
|
+
function logTone(level) {
|
|
21
|
+
if (level === "error") return "danger";
|
|
22
|
+
if (level === "warn") return "warning";
|
|
23
|
+
if (level === "info") return "info";
|
|
24
|
+
return "neutral";
|
|
25
|
+
}
|
|
26
|
+
/** Build overview page: snapshot grid, bulk actions, and build comments. */
|
|
27
|
+
async function renderBuildDetailPage(buildId) {
|
|
28
|
+
const { db } = getStore();
|
|
29
|
+
const build = await new BuildModel(db).get(buildId);
|
|
30
|
+
if (!build) return null;
|
|
31
|
+
const project = await new ProjectModel(db).get(build.projectId);
|
|
32
|
+
if (!project) return null;
|
|
33
|
+
const snapshots = await new SnapshotModel(db).listByBuild(build.id);
|
|
34
|
+
const comments = await new CommentModel(db).listByBuild(build.id);
|
|
35
|
+
const attempts = await new CaptureAttemptModel(db).listByBuild(build.id);
|
|
36
|
+
const logs = new CaptureLogModel(db);
|
|
37
|
+
const attemptLogs = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const attempt of attempts) attemptLogs.set(attempt.id, await logs.listByAttempt(attempt.id));
|
|
39
|
+
const canReview = !getStore().authEnabled || Boolean(getStore().user);
|
|
40
|
+
const { drifted } = await loadBaselineView(project, build, snapshots);
|
|
41
|
+
const staleCount = Object.keys(drifted).length;
|
|
42
|
+
const urls = createUrlBuilder("/", getStore().config.publishedBaseDomain);
|
|
43
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const snap of snapshots) {
|
|
45
|
+
const key = snap.status;
|
|
46
|
+
const list = grouped.get(key) ?? [];
|
|
47
|
+
list.push(snap);
|
|
48
|
+
grouped.set(key, list);
|
|
49
|
+
}
|
|
50
|
+
const inheritedCount = snapshots.filter((snap) => snap.inherited).length;
|
|
51
|
+
const selective = build.affectedOnly && build.affectedImportPaths !== null;
|
|
52
|
+
return /* @__PURE__ */ jsxs(DocumentLayout, {
|
|
53
|
+
title: `Build ${build.gitBranch}`,
|
|
54
|
+
nav: {
|
|
55
|
+
active: "builds",
|
|
56
|
+
projectSlug: project.slug,
|
|
57
|
+
projectName: project.name,
|
|
58
|
+
trail: [{ label: `${build.gitBranch} · ${build.gitSha.slice(0, 7)}` }]
|
|
59
|
+
},
|
|
60
|
+
children: [
|
|
61
|
+
/* @__PURE__ */ jsx(PageHeader, {
|
|
62
|
+
title: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
63
|
+
build.gitBranch,
|
|
64
|
+
" ",
|
|
65
|
+
/* @__PURE__ */ jsxs(Meta, {
|
|
66
|
+
as: "span",
|
|
67
|
+
children: ["· ", build.gitSha.slice(0, 7)]
|
|
68
|
+
}),
|
|
69
|
+
" ",
|
|
70
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
71
|
+
tone: statusTone(build.status),
|
|
72
|
+
children: build.status
|
|
73
|
+
}),
|
|
74
|
+
selective ? /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(Badge, {
|
|
75
|
+
tone: "info",
|
|
76
|
+
children: "affected capture"
|
|
77
|
+
})] }) : null,
|
|
78
|
+
!build.affectedOnly ? /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(Badge, {
|
|
79
|
+
tone: "neutral",
|
|
80
|
+
children: "full capture"
|
|
81
|
+
})] }) : null
|
|
82
|
+
] }),
|
|
83
|
+
description: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
84
|
+
build.message ?? "No message",
|
|
85
|
+
" ",
|
|
86
|
+
build.authorName ? `· ${build.authorName}${build.authorEmail ? ` <${build.authorEmail}>` : ""}` : "",
|
|
87
|
+
" ",
|
|
88
|
+
"· ",
|
|
89
|
+
new Date(build.createdAt).toLocaleString(),
|
|
90
|
+
selective ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
91
|
+
" ",
|
|
92
|
+
"· ",
|
|
93
|
+
snapshots.length - inheritedCount,
|
|
94
|
+
" rendered, ",
|
|
95
|
+
inheritedCount,
|
|
96
|
+
" inherited"
|
|
97
|
+
] }) : null
|
|
98
|
+
] }),
|
|
99
|
+
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
100
|
+
/* @__PURE__ */ jsx(Button, {
|
|
101
|
+
variant: "primary",
|
|
102
|
+
href: urls.buildDiff(project.slug, build.id),
|
|
103
|
+
children: "Review diffs"
|
|
104
|
+
}),
|
|
105
|
+
/* @__PURE__ */ jsx(Button, {
|
|
106
|
+
variant: "ghost",
|
|
107
|
+
href: urls.short(build.id),
|
|
108
|
+
children: "View Storybook"
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ jsx("form", {
|
|
111
|
+
method: "post",
|
|
112
|
+
action: `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
|
|
113
|
+
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/retry`,
|
|
114
|
+
"hx-target": "body",
|
|
115
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
116
|
+
variant: "secondary",
|
|
117
|
+
type: "submit",
|
|
118
|
+
children: "Retry capture"
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
] })
|
|
122
|
+
}),
|
|
123
|
+
staleCount > 0 ? /* @__PURE__ */ jsx(StaleBaselineNotice, {
|
|
124
|
+
project,
|
|
125
|
+
build,
|
|
126
|
+
staleCount,
|
|
127
|
+
canReview
|
|
128
|
+
}) : null,
|
|
129
|
+
/* @__PURE__ */ jsx("div", {
|
|
130
|
+
class: "mb-1",
|
|
131
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
132
|
+
class: "grid grid--3",
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(Stat, {
|
|
135
|
+
label: "Snapshots",
|
|
136
|
+
value: snapshots.length
|
|
137
|
+
}) }),
|
|
138
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(Stat, {
|
|
139
|
+
label: "Changed / new",
|
|
140
|
+
value: build.changedCount,
|
|
141
|
+
tone: "warning"
|
|
142
|
+
}) }),
|
|
143
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(Stat, {
|
|
144
|
+
label: "Approved / unchanged",
|
|
145
|
+
value: build.approvedCount,
|
|
146
|
+
tone: "success"
|
|
147
|
+
}) })
|
|
148
|
+
]
|
|
149
|
+
})
|
|
150
|
+
}),
|
|
151
|
+
attempts.length > 0 ? /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, { children: "Capture attempts" }), attempts.map((attempt, index) => /* @__PURE__ */ jsxs("details", {
|
|
152
|
+
open: index === attempts.length - 1,
|
|
153
|
+
children: [
|
|
154
|
+
/* @__PURE__ */ jsxs("summary", { children: [
|
|
155
|
+
"Attempt ",
|
|
156
|
+
attempt.attemptNo,
|
|
157
|
+
" ",
|
|
158
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
159
|
+
tone: statusTone(attempt.status),
|
|
160
|
+
children: attempt.status
|
|
161
|
+
}),
|
|
162
|
+
" ",
|
|
163
|
+
/* @__PURE__ */ jsxs(Meta, {
|
|
164
|
+
as: "span",
|
|
165
|
+
children: [
|
|
166
|
+
attempt.storyCount,
|
|
167
|
+
" stories",
|
|
168
|
+
attempt.reqId ? ` · ${attempt.reqId}` : ""
|
|
169
|
+
]
|
|
170
|
+
})
|
|
171
|
+
] }),
|
|
172
|
+
attempt.error ? /* @__PURE__ */ jsx(Meta, {
|
|
173
|
+
as: "pre",
|
|
174
|
+
children: attempt.error
|
|
175
|
+
}) : null,
|
|
176
|
+
/* @__PURE__ */ jsx("div", {
|
|
177
|
+
class: "mt-1",
|
|
178
|
+
children: /* @__PURE__ */ jsx(VStack, { children: (attemptLogs.get(attempt.id) ?? []).map((line) => /* @__PURE__ */ jsxs(HStack, {
|
|
179
|
+
align: "baseline",
|
|
180
|
+
wrap: false,
|
|
181
|
+
children: [
|
|
182
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
183
|
+
tone: logTone(line.level),
|
|
184
|
+
children: line.level
|
|
185
|
+
}),
|
|
186
|
+
/* @__PURE__ */ jsx("span", { children: line.message }),
|
|
187
|
+
line.fields ? /* @__PURE__ */ jsx(Meta, {
|
|
188
|
+
as: "code",
|
|
189
|
+
children: line.fields
|
|
190
|
+
}) : null
|
|
191
|
+
]
|
|
192
|
+
}, line.id)) })
|
|
193
|
+
})
|
|
194
|
+
]
|
|
195
|
+
}, attempt.id))] }) : null,
|
|
196
|
+
canReview && (build.status === "reviewing" || build.status === "comparing") ? /* @__PURE__ */ jsx("div", {
|
|
197
|
+
class: "mb-1",
|
|
198
|
+
children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(HStack, { children: [
|
|
199
|
+
/* @__PURE__ */ jsx(Meta, {
|
|
200
|
+
as: "span",
|
|
201
|
+
children: "Bulk actions:"
|
|
202
|
+
}),
|
|
203
|
+
/* @__PURE__ */ jsx("form", {
|
|
204
|
+
method: "post",
|
|
205
|
+
action: `/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`,
|
|
206
|
+
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`,
|
|
207
|
+
"hx-target": "body",
|
|
208
|
+
"hx-confirm": "Approve all remaining changes?",
|
|
209
|
+
"data-toast": "Approved all remaining changes",
|
|
210
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
211
|
+
variant: "primary",
|
|
212
|
+
size: "sm",
|
|
213
|
+
type: "submit",
|
|
214
|
+
children: "Approve all"
|
|
215
|
+
})
|
|
216
|
+
}),
|
|
217
|
+
/* @__PURE__ */ jsx("form", {
|
|
218
|
+
method: "post",
|
|
219
|
+
action: `/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`,
|
|
220
|
+
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`,
|
|
221
|
+
"hx-target": "body",
|
|
222
|
+
"hx-confirm": "Reject all remaining changes?",
|
|
223
|
+
"data-toast": "Rejected all remaining changes",
|
|
224
|
+
"data-toast-tone": "warning",
|
|
225
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
226
|
+
variant: "danger",
|
|
227
|
+
size: "sm",
|
|
228
|
+
type: "submit",
|
|
229
|
+
children: "Reject all"
|
|
230
|
+
})
|
|
231
|
+
}),
|
|
232
|
+
/* @__PURE__ */ jsx(Meta, {
|
|
233
|
+
as: "span",
|
|
234
|
+
children: "Or review individually in the diff view."
|
|
235
|
+
})
|
|
236
|
+
] }) })
|
|
237
|
+
}) : null,
|
|
238
|
+
snapshots.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
239
|
+
title: "No snapshots yet",
|
|
240
|
+
description: "Capture is pending or failed. Try retrying capture."
|
|
241
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
242
|
+
class: snapshotGrid,
|
|
243
|
+
children: snapshots.map((snap) => /* @__PURE__ */ jsxs("div", {
|
|
244
|
+
class: snapshotCard,
|
|
245
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
246
|
+
class: snapshotCardHead,
|
|
247
|
+
children: [
|
|
248
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
249
|
+
tone: statusTone(snap.status),
|
|
250
|
+
children: snap.status
|
|
251
|
+
}),
|
|
252
|
+
snap.inherited ? /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(Badge, {
|
|
253
|
+
tone: "neutral",
|
|
254
|
+
children: "inherited"
|
|
255
|
+
})] }) : null,
|
|
256
|
+
/* @__PURE__ */ jsxs("span", {
|
|
257
|
+
class: snapshotCardMeta,
|
|
258
|
+
children: [
|
|
259
|
+
snap.viewportName,
|
|
260
|
+
" · ",
|
|
261
|
+
snap.viewportWidth,
|
|
262
|
+
"×",
|
|
263
|
+
snap.viewportHeight
|
|
264
|
+
]
|
|
265
|
+
})
|
|
266
|
+
]
|
|
267
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
268
|
+
class: snapshotCardBody,
|
|
269
|
+
children: [
|
|
270
|
+
/* @__PURE__ */ jsx("a", {
|
|
271
|
+
href: `${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`,
|
|
272
|
+
children: /* @__PURE__ */ jsx(Thumbnail, {
|
|
273
|
+
src: `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/image`,
|
|
274
|
+
alt: `${snap.storyTitle} / ${snap.storyName}`
|
|
275
|
+
})
|
|
276
|
+
}),
|
|
277
|
+
/* @__PURE__ */ jsxs(SectionTitle, { children: [
|
|
278
|
+
snap.storyTitle,
|
|
279
|
+
" ",
|
|
280
|
+
/* @__PURE__ */ jsxs(Meta, {
|
|
281
|
+
as: "span",
|
|
282
|
+
children: ["/ ", snap.storyName]
|
|
283
|
+
})
|
|
284
|
+
] }),
|
|
285
|
+
/* @__PURE__ */ jsxs(Meta, {
|
|
286
|
+
as: "div",
|
|
287
|
+
children: [
|
|
288
|
+
snap.storyId,
|
|
289
|
+
snap.diffRatio !== null && snap.diffRatio !== void 0 ? ` · diff ${(snap.diffRatio * 100).toFixed(2)}%` : "",
|
|
290
|
+
snap.diffPixels === null || snap.diffPixels === void 0 ? "" : ` · ${snap.diffPixels} px`
|
|
291
|
+
]
|
|
292
|
+
}),
|
|
293
|
+
/* @__PURE__ */ jsxs(HStack, { children: [
|
|
294
|
+
/* @__PURE__ */ jsx(Button, {
|
|
295
|
+
variant: "secondary",
|
|
296
|
+
size: "sm",
|
|
297
|
+
href: `${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`,
|
|
298
|
+
children: "Review"
|
|
299
|
+
}),
|
|
300
|
+
/* @__PURE__ */ jsx(Button, {
|
|
301
|
+
variant: "ghost",
|
|
302
|
+
size: "sm",
|
|
303
|
+
href: urls.short(build.id),
|
|
304
|
+
children: "View Storybook"
|
|
305
|
+
}),
|
|
306
|
+
canReview && (snap.status === "new" || snap.status === "changed") ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("form", {
|
|
307
|
+
method: "post",
|
|
308
|
+
action: `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`,
|
|
309
|
+
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`,
|
|
310
|
+
"hx-target": "body",
|
|
311
|
+
"data-toast": `Approved ${snap.storyName}`,
|
|
312
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
313
|
+
variant: "ghost",
|
|
314
|
+
size: "sm",
|
|
315
|
+
type: "submit",
|
|
316
|
+
children: "Approve"
|
|
317
|
+
})
|
|
318
|
+
}), /* @__PURE__ */ jsx("form", {
|
|
319
|
+
method: "post",
|
|
320
|
+
action: `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`,
|
|
321
|
+
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`,
|
|
322
|
+
"hx-target": "body",
|
|
323
|
+
"data-toast": `Rejected ${snap.storyName}`,
|
|
324
|
+
"data-toast-tone": "warning",
|
|
325
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
326
|
+
variant: "ghost",
|
|
327
|
+
size: "sm",
|
|
328
|
+
type: "submit",
|
|
329
|
+
children: "Reject"
|
|
330
|
+
})
|
|
331
|
+
})] }) : null
|
|
332
|
+
] })
|
|
333
|
+
]
|
|
334
|
+
})]
|
|
335
|
+
}, snap.id))
|
|
336
|
+
}),
|
|
337
|
+
/* @__PURE__ */ jsx("div", {
|
|
338
|
+
class: "mt-1",
|
|
339
|
+
children: /* @__PURE__ */ jsxs(Card, { children: [
|
|
340
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: "Comments" }),
|
|
341
|
+
comments.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No comments. Add one in the diff review page for a specific snapshot." }) : null,
|
|
342
|
+
/* @__PURE__ */ jsx(VStack, { children: comments.map((comment) => /* @__PURE__ */ jsxs("div", {
|
|
343
|
+
class: reviewComment,
|
|
344
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
345
|
+
class: reviewCommentHead,
|
|
346
|
+
children: [
|
|
347
|
+
/* @__PURE__ */ jsx("strong", { children: comment.userId ?? "anonymous" }),
|
|
348
|
+
/* @__PURE__ */ jsxs("span", { children: ["· ", new Date(comment.createdAt).toLocaleString()] }),
|
|
349
|
+
comment.snapshotId ? /* @__PURE__ */ jsx(Badge, {
|
|
350
|
+
tone: "neutral",
|
|
351
|
+
children: snapshots.find((snapshot) => snapshot.id === comment.snapshotId)?.storyName ?? comment.snapshotId.slice(0, 6)
|
|
352
|
+
}) : /* @__PURE__ */ jsx(Badge, {
|
|
353
|
+
tone: "neutral",
|
|
354
|
+
children: "build"
|
|
355
|
+
}),
|
|
356
|
+
comment.resolved ? /* @__PURE__ */ jsx(Badge, {
|
|
357
|
+
tone: "success",
|
|
358
|
+
children: "resolved"
|
|
359
|
+
}) : null
|
|
360
|
+
]
|
|
361
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
362
|
+
class: reviewCommentBody,
|
|
363
|
+
children: comment.body
|
|
364
|
+
})]
|
|
365
|
+
}, comment.id)) }),
|
|
366
|
+
/* @__PURE__ */ jsx("form", {
|
|
367
|
+
method: "post",
|
|
368
|
+
action: `/api/v1/projects/${project.slug}/builds/${build.id}/comments`,
|
|
369
|
+
"hx-post": `/api/v1/projects/${project.slug}/builds/${build.id}/comments`,
|
|
370
|
+
"hx-target": "body",
|
|
371
|
+
class: "mt-1 max-w-prose",
|
|
372
|
+
children: /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(TextareaField, {
|
|
373
|
+
label: "Add build comment",
|
|
374
|
+
name: "body",
|
|
375
|
+
rows: 3,
|
|
376
|
+
required: true,
|
|
377
|
+
placeholder: "Leave a comment on this build…"
|
|
378
|
+
}), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
|
|
379
|
+
variant: "primary",
|
|
380
|
+
type: "submit",
|
|
381
|
+
children: "Comment"
|
|
382
|
+
}) })] })
|
|
383
|
+
})
|
|
384
|
+
] })
|
|
385
|
+
})
|
|
386
|
+
]
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
export { renderBuildDetailPage };
|
|
391
|
+
|
|
392
|
+
//# sourceMappingURL=build-detail.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-detail.mjs","names":[],"sources":["../../src/pages/build-detail.tsx"],"sourcesContent":["import { BuildModel } from \"@storyshelf/core/models\";\nimport { CaptureAttemptModel } from \"@storyshelf/core/models\";\nimport { CaptureLogModel } from \"@storyshelf/core/models\";\nimport { CommentModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { loadBaselineView } from \"../routers/baseline-guard.ts\";\nimport { getStore } from \"../store.ts\";\nimport {\n Badge,\n Button,\n Card,\n EmptyState,\n HStack,\n Meta,\n PageHeader,\n SectionTitle,\n Stat,\n TextareaField,\n Thumbnail,\n VStack,\n statusTone,\n} from \"../ui/components.tsx\";\nimport { DocumentLayout, type RenderedContent } from \"../ui/document.tsx\";\nimport {\n reviewComment,\n reviewCommentBody,\n reviewCommentHead,\n snapshotCard,\n snapshotCardBody,\n snapshotCardHead,\n snapshotCardMeta,\n snapshotGrid,\n} from \"../ui/styles/review.ts\";\nimport { StaleBaselineNotice } from \"./build-diff-stale.tsx\";\n\n/** Map a capture log level to its badge tone. */\nfunction logTone(level: string): \"neutral\" | \"success\" | \"warning\" | \"danger\" | \"info\" {\n if (level === \"error\") {\n return \"danger\";\n }\n if (level === \"warn\") {\n return \"warning\";\n }\n if (level === \"info\") {\n return \"info\";\n }\n return \"neutral\";\n}\n/** Build overview page: snapshot grid, bulk actions, and build comments. */\nexport async function renderBuildDetailPage(buildId: string): Promise<RenderedContent | null> {\n const { db } = getStore();\n const build = await new BuildModel(db).get(buildId);\n if (!build) {\n return null;\n }\n const project = await new ProjectModel(db).get(build.projectId);\n if (!project) {\n return null;\n }\n const snapshots = await new SnapshotModel(db).listByBuild(build.id);\n const comments = await new CommentModel(db).listByBuild(build.id);\n const attempts = await new CaptureAttemptModel(db).listByBuild(build.id);\n const logs = new CaptureLogModel(db);\n const attemptLogs = new Map<string, Awaited<ReturnType<typeof logs.listByAttempt>>>();\n /* oxlint-disable-next-line eslint/no-await-in-loop -- attempts are few; sequential reads keep code simple */\n for (const attempt of attempts) {\n attemptLogs.set(attempt.id, await logs.listByAttempt(attempt.id));\n }\n const canReview = !getStore().authEnabled || Boolean(getStore().user);\n const { drifted } = await loadBaselineView(project, build, snapshots);\n const staleCount = Object.keys(drifted).length;\n const urls = createUrlBuilder(\"/\", getStore().config.publishedBaseDomain);\n\n const grouped = new Map<string, typeof snapshots>();\n for (const snap of snapshots) {\n const key = snap.status;\n const list = grouped.get(key) ?? [];\n list.push(snap);\n grouped.set(key, list);\n }\n const inheritedCount = snapshots.filter((snap) => snap.inherited).length;\n const selective = build.affectedOnly && build.affectedImportPaths !== null;\n\n return (\n <DocumentLayout\n title={`Build ${build.gitBranch}`}\n nav={{\n active: \"builds\",\n projectSlug: project.slug,\n projectName: project.name,\n trail: [{ label: `${build.gitBranch} · ${build.gitSha.slice(0, 7)}` }],\n }}\n >\n <PageHeader\n title={\n <>\n {build.gitBranch} <Meta as=\"span\">· {build.gitSha.slice(0, 7)}</Meta>{\" \"}\n <Badge tone={statusTone(build.status)}>{build.status}</Badge>\n {selective ? (\n <>\n {\" \"}\n <Badge tone=\"info\">affected capture</Badge>\n </>\n ) : null}\n {!build.affectedOnly ? (\n <>\n {\" \"}\n <Badge tone=\"neutral\">full capture</Badge>\n </>\n ) : null}\n </>\n }\n description={\n <>\n {build.message ?? \"No message\"}{\" \"}\n {build.authorName\n ? `· ${build.authorName}${build.authorEmail ? ` <${build.authorEmail}>` : \"\"}`\n : \"\"}{\" \"}\n · {new Date(build.createdAt).toLocaleString()}\n {selective ? (\n <>\n {\" \"}\n · {snapshots.length - inheritedCount} rendered, {inheritedCount} inherited\n </>\n ) : null}\n </>\n }\n actions={\n <>\n <Button variant=\"primary\" href={urls.buildDiff(project.slug, build.id)}>\n Review diffs\n </Button>\n <Button variant=\"ghost\" href={urls.short(build.id)}>\n View Storybook\n </Button>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/retry`}\n hx-target=\"body\"\n >\n <Button variant=\"secondary\" type=\"submit\">\n Retry capture\n </Button>\n </form>\n </>\n }\n />\n\n {staleCount > 0 ? (\n <StaleBaselineNotice\n project={project}\n build={build}\n staleCount={staleCount}\n canReview={canReview}\n />\n ) : null}\n\n <div class=\"mb-1\">\n <div class=\"grid grid--3\">\n <Card>\n <Stat label=\"Snapshots\" value={snapshots.length} />\n </Card>\n <Card>\n <Stat label=\"Changed / new\" value={build.changedCount} tone=\"warning\" />\n </Card>\n <Card>\n <Stat label=\"Approved / unchanged\" value={build.approvedCount} tone=\"success\" />\n </Card>\n </div>\n </div>\n\n {attempts.length > 0 ? (\n <Card>\n <SectionTitle>Capture attempts</SectionTitle>\n {attempts.map((attempt, index): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <details key={attempt.id} open={index === attempts.length - 1}>\n <summary>\n Attempt {attempt.attemptNo}{\" \"}\n <Badge tone={statusTone(attempt.status)}>{attempt.status}</Badge>{\" \"}\n <Meta as=\"span\">\n {attempt.storyCount} stories\n {attempt.reqId ? ` · ${attempt.reqId}` : \"\"}\n </Meta>\n </summary>\n {attempt.error ? <Meta as=\"pre\">{attempt.error}</Meta> : null}\n <div class=\"mt-1\">\n <VStack>\n {(attemptLogs.get(attempt.id) ?? []).map(\n (line): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <HStack key={line.id} align=\"baseline\" wrap={false}>\n <Badge tone={logTone(line.level)}>{line.level}</Badge>\n <span>{line.message}</span>\n {line.fields ? <Meta as=\"code\">{line.fields}</Meta> : null}\n </HStack>\n ),\n )}\n </VStack>\n </div>\n </details>\n ))}\n </Card>\n ) : null}\n\n {canReview && (build.status === \"reviewing\" || build.status === \"comparing\") ? (\n <div class=\"mb-1\">\n <Card>\n <HStack>\n <Meta as=\"span\">Bulk actions:</Meta>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/approve-all`}\n hx-target=\"body\"\n hx-confirm=\"Approve all remaining changes?\"\n data-toast=\"Approved all remaining changes\"\n >\n <Button variant=\"primary\" size=\"sm\" type=\"submit\">\n Approve all\n </Button>\n </form>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/reject-all`}\n hx-target=\"body\"\n hx-confirm=\"Reject all remaining changes?\"\n data-toast=\"Rejected all remaining changes\"\n data-toast-tone=\"warning\"\n >\n <Button variant=\"danger\" size=\"sm\" type=\"submit\">\n Reject all\n </Button>\n </form>\n <Meta as=\"span\">Or review individually in the diff view.</Meta>\n </HStack>\n </Card>\n </div>\n ) : null}\n\n {snapshots.length === 0 ? (\n <EmptyState\n title=\"No snapshots yet\"\n description=\"Capture is pending or failed. Try retrying capture.\"\n />\n ) : (\n <div class={snapshotGrid}>\n {snapshots.map((snap): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <div key={snap.id} class={snapshotCard}>\n <div class={snapshotCardHead}>\n <Badge tone={statusTone(snap.status)}>{snap.status}</Badge>\n {snap.inherited ? (\n <>\n {\" \"}\n <Badge tone=\"neutral\">inherited</Badge>\n </>\n ) : null}\n <span class={snapshotCardMeta}>\n {snap.viewportName} · {snap.viewportWidth}×{snap.viewportHeight}\n </span>\n </div>\n <div class={snapshotCardBody}>\n <a href={`${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`}>\n <Thumbnail\n src={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/image`}\n alt={`${snap.storyTitle} / ${snap.storyName}`}\n />\n </a>\n <SectionTitle>\n {snap.storyTitle} <Meta as=\"span\">/ {snap.storyName}</Meta>\n </SectionTitle>\n <Meta as=\"div\">\n {snap.storyId}\n {snap.diffRatio !== null && snap.diffRatio !== undefined\n ? ` · diff ${(snap.diffRatio * 100).toFixed(2)}%`\n : \"\"}\n {snap.diffPixels === null || snap.diffPixels === undefined\n ? \"\"\n : ` · ${snap.diffPixels} px`}\n </Meta>\n <HStack>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n href={`${urls.buildDiff(project.slug, build.id)}?snapshot=${snap.id}`}\n >\n Review\n </Button>\n <Button variant=\"ghost\" size=\"sm\" href={urls.short(build.id)}>\n View Storybook\n </Button>\n {canReview && (snap.status === \"new\" || snap.status === \"changed\") ? (\n <>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/approve`}\n hx-target=\"body\"\n data-toast={`Approved ${snap.storyName}`}\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Approve\n </Button>\n </form>\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snap.id}/reject`}\n hx-target=\"body\"\n data-toast={`Rejected ${snap.storyName}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Reject\n </Button>\n </form>\n </>\n ) : null}\n </HStack>\n </div>\n </div>\n ))}\n </div>\n )}\n\n <div class=\"mt-1\">\n <Card>\n <SectionTitle>Comments</SectionTitle>\n {comments.length === 0 ? (\n <Meta>No comments. Add one in the diff review page for a specific snapshot.</Meta>\n ) : null}\n <VStack>\n {comments.map((comment): HtmlEscapedString | Promise<HtmlEscapedString> => (\n <div key={comment.id} class={reviewComment}>\n <div class={reviewCommentHead}>\n <strong>{comment.userId ?? \"anonymous\"}</strong>\n <span>· {new Date(comment.createdAt).toLocaleString()}</span>\n {comment.snapshotId ? (\n <Badge tone=\"neutral\">\n {snapshots.find((snapshot) => snapshot.id === comment.snapshotId)\n ?.storyName ?? comment.snapshotId.slice(0, 6)}\n </Badge>\n ) : (\n <Badge tone=\"neutral\">build</Badge>\n )}\n {comment.resolved ? <Badge tone=\"success\">resolved</Badge> : null}\n </div>\n <p class={reviewCommentBody}>{comment.body}</p>\n </div>\n ))}\n </VStack>\n\n <form\n method=\"post\"\n action={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`}\n hx-post={`/api/v1/projects/${project.slug}/builds/${build.id}/comments`}\n hx-target=\"body\"\n class=\"mt-1 max-w-prose\"\n >\n <VStack>\n <TextareaField\n label=\"Add build comment\"\n name=\"body\"\n rows={3}\n required\n placeholder=\"Leave a comment on this build…\"\n />\n <div>\n <Button variant=\"primary\" type=\"submit\">\n Comment\n </Button>\n </div>\n </VStack>\n </form>\n </Card>\n </div>\n </DocumentLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { Button } from "../ui/buttons.mjs";
|
|
5
|
+
import { Badge, statusTone } from "../ui/feedback.mjs";
|
|
6
|
+
import { Kbd } from "../ui/kbd.mjs";
|
|
7
|
+
import { actionBar, actionGroup, actionHints, actionPosition } from "../ui/styles/review-layout.mjs";
|
|
8
|
+
import { neighbours, snapshotPageUrl } from "./build-diff-model.mjs";
|
|
9
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
10
|
+
//#region src/pages/build-diff-actions.tsx
|
|
11
|
+
function snapshotBase(project, build, snapshot) {
|
|
12
|
+
return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}`;
|
|
13
|
+
}
|
|
14
|
+
/** Approve (or "Approve anyway" when the baseline moved) as its own form. */
|
|
15
|
+
function ApproveForm(props) {
|
|
16
|
+
const { project, build, selected, drifted } = props;
|
|
17
|
+
const url = `${snapshotBase(project, build, selected)}/approve${drifted ? "?force=true" : ""}`;
|
|
18
|
+
return /* @__PURE__ */ jsx("form", {
|
|
19
|
+
method: "post",
|
|
20
|
+
action: url,
|
|
21
|
+
"hx-post": url,
|
|
22
|
+
"hx-target": "body",
|
|
23
|
+
"data-toast": `Approved ${selected.storyName}`,
|
|
24
|
+
children: drifted ? /* @__PURE__ */ jsx(Button, {
|
|
25
|
+
variant: "secondary",
|
|
26
|
+
size: "sm",
|
|
27
|
+
type: "submit",
|
|
28
|
+
icon: "check",
|
|
29
|
+
title: "The baseline changed after this diff; replace it with this screenshot",
|
|
30
|
+
children: "Approve anyway"
|
|
31
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
32
|
+
variant: "primary",
|
|
33
|
+
size: "sm",
|
|
34
|
+
type: "submit",
|
|
35
|
+
icon: "check",
|
|
36
|
+
"data-approve": true,
|
|
37
|
+
title: "Approve (a)",
|
|
38
|
+
children: "Approve"
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Approve/reject for a reviewable snapshot; both advance to the next open one. */
|
|
43
|
+
function DecisionButtons(props) {
|
|
44
|
+
const { project, build, selected } = props;
|
|
45
|
+
const base = snapshotBase(project, build, selected);
|
|
46
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
47
|
+
class: actionGroup,
|
|
48
|
+
children: [/* @__PURE__ */ jsx(ApproveForm, { ...props }), /* @__PURE__ */ jsx("form", {
|
|
49
|
+
method: "post",
|
|
50
|
+
action: `${base}/reject`,
|
|
51
|
+
"hx-post": `${base}/reject`,
|
|
52
|
+
"hx-target": "body",
|
|
53
|
+
"data-toast": `Rejected ${selected.storyName}`,
|
|
54
|
+
"data-toast-tone": "warning",
|
|
55
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
56
|
+
variant: "danger",
|
|
57
|
+
size: "sm",
|
|
58
|
+
type: "submit",
|
|
59
|
+
icon: "x",
|
|
60
|
+
"data-reject": true,
|
|
61
|
+
title: "Reject (r)",
|
|
62
|
+
children: "Reject"
|
|
63
|
+
})
|
|
64
|
+
})]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function StepButton(props) {
|
|
68
|
+
const { target, project, build, direction } = props;
|
|
69
|
+
const label = direction === "prev" ? "Previous snapshot (k)" : "Next snapshot (j)";
|
|
70
|
+
const icon = direction === "prev" ? "chevron-left" : "chevron-right";
|
|
71
|
+
if (!target) return /* @__PURE__ */ jsx(Button, {
|
|
72
|
+
variant: "ghost",
|
|
73
|
+
size: "sm",
|
|
74
|
+
icon,
|
|
75
|
+
disabled: true,
|
|
76
|
+
"aria-label": label
|
|
77
|
+
});
|
|
78
|
+
const url = snapshotPageUrl(project, build, target.id);
|
|
79
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
80
|
+
variant: "ghost",
|
|
81
|
+
size: "sm",
|
|
82
|
+
icon,
|
|
83
|
+
href: url,
|
|
84
|
+
"aria-label": label,
|
|
85
|
+
title: label,
|
|
86
|
+
"hx-get": url,
|
|
87
|
+
"hx-target": "body",
|
|
88
|
+
"hx-push-url": "true",
|
|
89
|
+
...direction === "prev" ? { "data-snap-prev": true } : { "data-snap-next": true }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Previous/next stepping, position, shortcut hints, and the review decision. */
|
|
93
|
+
function DiffActionBar(props) {
|
|
94
|
+
const { project, build, snapshots, selected, canReview, drifted } = props;
|
|
95
|
+
const around = neighbours(snapshots, selected.id);
|
|
96
|
+
const reviewable = selected.status === "new" || selected.status === "changed";
|
|
97
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
98
|
+
class: actionBar,
|
|
99
|
+
role: "toolbar",
|
|
100
|
+
"aria-label": "Review actions",
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsxs("div", {
|
|
103
|
+
class: actionGroup,
|
|
104
|
+
children: [
|
|
105
|
+
/* @__PURE__ */ jsx(StepButton, {
|
|
106
|
+
target: around.prev,
|
|
107
|
+
project,
|
|
108
|
+
build,
|
|
109
|
+
direction: "prev"
|
|
110
|
+
}),
|
|
111
|
+
/* @__PURE__ */ jsxs("span", {
|
|
112
|
+
class: actionPosition,
|
|
113
|
+
"aria-live": "polite",
|
|
114
|
+
children: [
|
|
115
|
+
around.position,
|
|
116
|
+
" / ",
|
|
117
|
+
around.total
|
|
118
|
+
]
|
|
119
|
+
}),
|
|
120
|
+
/* @__PURE__ */ jsx(StepButton, {
|
|
121
|
+
target: around.next,
|
|
122
|
+
project,
|
|
123
|
+
build,
|
|
124
|
+
direction: "next"
|
|
125
|
+
})
|
|
126
|
+
]
|
|
127
|
+
}),
|
|
128
|
+
/* @__PURE__ */ jsxs("div", {
|
|
129
|
+
class: actionHints,
|
|
130
|
+
children: [
|
|
131
|
+
/* @__PURE__ */ jsx(Kbd, { keys: ["j", "k"] }),
|
|
132
|
+
" navigate ",
|
|
133
|
+
/* @__PURE__ */ jsx(Kbd, { keys: "a" }),
|
|
134
|
+
" approve ",
|
|
135
|
+
/* @__PURE__ */ jsx(Kbd, { keys: "r" }),
|
|
136
|
+
" reject",
|
|
137
|
+
/* @__PURE__ */ jsx(Button, {
|
|
138
|
+
variant: "ghost",
|
|
139
|
+
size: "sm",
|
|
140
|
+
"data-shortcuts-open": true,
|
|
141
|
+
"aria-label": "Keyboard shortcuts (?)",
|
|
142
|
+
children: /* @__PURE__ */ jsx(Kbd, { keys: "?" })
|
|
143
|
+
})
|
|
144
|
+
]
|
|
145
|
+
}),
|
|
146
|
+
canReview && reviewable ? /* @__PURE__ */ jsx(DecisionButtons, {
|
|
147
|
+
project,
|
|
148
|
+
build,
|
|
149
|
+
selected,
|
|
150
|
+
drifted
|
|
151
|
+
}) : /* @__PURE__ */ jsx(Badge, {
|
|
152
|
+
tone: statusTone(selected.status),
|
|
153
|
+
icon: true,
|
|
154
|
+
children: selected.status
|
|
155
|
+
})
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { DiffActionBar };
|
|
161
|
+
|
|
162
|
+
//# sourceMappingURL=build-diff-actions.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-diff-actions.mjs","names":[],"sources":["../../src/pages/build-diff-actions.tsx"],"sourcesContent":["import type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { HtmlEscapedString } from \"hono/utils/html\";\nimport { Badge, Button, Kbd, statusTone } from \"../ui/components.tsx\";\nimport { actionBar, actionGroup, actionHints, actionPosition } from \"../ui/styles/review-layout.ts\";\nimport { neighbours, snapshotPageUrl } from \"./build-diff-model.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Sticky bottom bar props. */\nexport interface DiffActionBarProps {\n project: Project;\n build: Build;\n snapshots: Snapshot[];\n selected: Snapshot;\n canReview: boolean;\n drifted: boolean;\n}\n\nfunction snapshotBase(project: Project, build: Build, snapshot: Snapshot): string {\n return `/api/v1/projects/${project.slug}/builds/${build.id}/snapshots/${snapshot.id}`;\n}\n\ntype DecisionProps = { project: Project; build: Build; selected: Snapshot; drifted: boolean };\n\n/** Approve (or \"Approve anyway\" when the baseline moved) as its own form. */\nfunction ApproveForm(props: DecisionProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected, drifted } = props;\n const url = `${snapshotBase(project, build, selected)}/approve${drifted ? \"?force=true\" : \"\"}`;\n return (\n <form\n method=\"post\"\n action={url}\n hx-post={url}\n hx-target=\"body\"\n data-toast={`Approved ${selected.storyName}`}\n >\n {drifted ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n title=\"The baseline changed after this diff; replace it with this screenshot\"\n >\n Approve anyway\n </Button>\n ) : (\n <Button\n variant=\"primary\"\n size=\"sm\"\n type=\"submit\"\n icon=\"check\"\n data-approve\n title=\"Approve (a)\"\n >\n Approve\n </Button>\n )}\n </form>\n );\n}\n\n/** Approve/reject for a reviewable snapshot; both advance to the next open one. */\nfunction DecisionButtons(props: DecisionProps): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, selected } = props;\n const base = snapshotBase(project, build, selected);\n return (\n <div class={actionGroup}>\n <ApproveForm {...props} />\n <form\n method=\"post\"\n action={`${base}/reject`}\n hx-post={`${base}/reject`}\n hx-target=\"body\"\n data-toast={`Rejected ${selected.storyName}`}\n data-toast-tone=\"warning\"\n >\n <Button variant=\"danger\" size=\"sm\" type=\"submit\" icon=\"x\" data-reject title=\"Reject (r)\">\n Reject\n </Button>\n </form>\n </div>\n );\n}\n\nfunction StepButton(props: {\n target: Snapshot | null;\n project: Project;\n build: Build;\n direction: \"prev\" | \"next\";\n}): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { target, project, build, direction } = props;\n const label = direction === \"prev\" ? \"Previous snapshot (k)\" : \"Next snapshot (j)\";\n const icon = direction === \"prev\" ? \"chevron-left\" : \"chevron-right\";\n if (!target) {\n return <Button variant=\"ghost\" size=\"sm\" icon={icon} disabled aria-label={label} />;\n }\n const url = snapshotPageUrl(project, build, target.id);\n const hook = direction === \"prev\" ? { \"data-snap-prev\": true } : { \"data-snap-next\": true };\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n icon={icon}\n href={url}\n aria-label={label}\n title={label}\n hx-get={url}\n hx-target=\"body\"\n hx-push-url=\"true\"\n {...hook}\n />\n );\n}\n\n/** Previous/next stepping, position, shortcut hints, and the review decision. */\nexport function DiffActionBar(\n props: DiffActionBarProps,\n): HtmlEscapedString | Promise<HtmlEscapedString> {\n const { project, build, snapshots, selected, canReview, drifted } = props;\n const around = neighbours(snapshots, selected.id);\n const reviewable = selected.status === \"new\" || selected.status === \"changed\";\n return (\n <div class={actionBar} role=\"toolbar\" aria-label=\"Review actions\">\n <div class={actionGroup}>\n <StepButton target={around.prev} project={project} build={build} direction=\"prev\" />\n <span class={actionPosition} aria-live=\"polite\">\n {around.position} / {around.total}\n </span>\n <StepButton target={around.next} project={project} build={build} direction=\"next\" />\n </div>\n <div class={actionHints}>\n <Kbd keys={[\"j\", \"k\"]} /> navigate <Kbd keys=\"a\" /> approve <Kbd keys=\"r\" /> reject\n <Button variant=\"ghost\" size=\"sm\" data-shortcuts-open aria-label=\"Keyboard shortcuts (?)\">\n <Kbd keys=\"?\" />\n </Button>\n </div>\n {canReview && reviewable ? (\n <DecisionButtons project={project} build={build} selected={selected} drifted={drifted} />\n ) : (\n <Badge tone={statusTone(selected.status)} icon>\n {selected.status}\n </Badge>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAkBA,SAAS,aAAa,SAAkB,OAAc,UAA4B;CAChF,OAAO,oBAAoB,QAAQ,KAAK,UAAU,MAAM,GAAG,aAAa,SAAS;AACnF;;AAKA,SAAS,YAAY,OAAsE;CACzF,MAAM,EAAE,SAAS,OAAO,UAAU,YAAY;CAC9C,MAAM,MAAM,GAAG,aAAa,SAAS,OAAO,QAAQ,EAAE,UAAU,UAAU,gBAAgB;CAC1F,OACE,oBAAC,QAAD;EACE,QAAO;EACP,QAAQ;EACR,WAAS;EACT,aAAU;EACV,cAAY,YAAY,SAAS;EAEhC,UAAA,UACC,oBAAC,QAAD;GACE,SAAQ;GACR,MAAK;GACL,MAAK;GACL,MAAK;GACL,OAAM;GACP,UAAA;EAEO,CAAA,IAER,oBAAC,QAAD;GACE,SAAQ;GACR,MAAK;GACL,MAAK;GACL,MAAK;GACL,gBAAA;GACA,OAAM;GACP,UAAA;EAEO,CAAA;CAEN,CAAA;AAEV;;AAGA,SAAS,gBAAgB,OAAsE;CAC7F,MAAM,EAAE,SAAS,OAAO,aAAa;CACrC,MAAM,OAAO,aAAa,SAAS,OAAO,QAAQ;CAClD,OACE,qBAAC,OAAD;EAAK,OAAO;EAAZ,UAAA,CACE,oBAAC,aAAD,EAAa,GAAI,MAAQ,CAAA,GACzB,oBAAC,QAAD;GACE,QAAO;GACP,QAAQ,GAAG,KAAK;GAChB,WAAS,GAAG,KAAK;GACjB,aAAU;GACV,cAAY,YAAY,SAAS;GACjC,mBAAgB;GAEhB,UAAA,oBAAC,QAAD;IAAQ,SAAQ;IAAS,MAAK;IAAK,MAAK;IAAS,MAAK;IAAI,eAAA;IAAY,OAAM;IAAa,UAAA;GAEjF,CAAA;EACJ,CAAA,CACH;;AAET;AAEA,SAAS,WAAW,OAK+B;CACjD,MAAM,EAAE,QAAQ,SAAS,OAAO,cAAc;CAC9C,MAAM,QAAQ,cAAc,SAAS,0BAA0B;CAC/D,MAAM,OAAO,cAAc,SAAS,iBAAiB;CACrD,IAAI,CAAC,QACH,OAAO,oBAAC,QAAD;EAAQ,SAAQ;EAAQ,MAAK;EAAW;EAAM,UAAA;EAAS,cAAY;CAAQ,CAAA;CAEpF,MAAM,MAAM,gBAAgB,SAAS,OAAO,OAAO,EAAE;CAErD,OACE,oBAAC,QAAD;EACE,SAAQ;EACR,MAAK;EACC;EACN,MAAM;EACN,cAAY;EACZ,OAAO;EACP,UAAQ;EACR,aAAU;EACV,eAAY;EACZ,GAZS,cAAc,SAAS,EAAE,kBAAkB,KAAK,IAAI,EAAE,kBAAkB,KAAK;CAavF,CAAA;AAEL;;AAGA,SAAgB,cACd,OACgD;CAChD,MAAM,EAAE,SAAS,OAAO,WAAW,UAAU,WAAW,YAAY;CACpE,MAAM,SAAS,WAAW,WAAW,SAAS,EAAE;CAChD,MAAM,aAAa,SAAS,WAAW,SAAS,SAAS,WAAW;CACpE,OACE,qBAAC,OAAD;EAAK,OAAO;EAAW,MAAK;EAAU,cAAW;EAAjD,UAAA;GACE,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA;KACE,oBAAC,YAAD;MAAY,QAAQ,OAAO;MAAe;MAAgB;MAAO,WAAU;KAAQ,CAAA;KACnF,qBAAC,QAAD;MAAM,OAAO;MAAgB,aAAU;MAAvC,UAAA;OACG,OAAO;OAAS;OAAI,OAAO;MACxB;;KACN,oBAAC,YAAD;MAAY,QAAQ,OAAO;MAAe;MAAgB;MAAO,WAAU;KAAQ,CAAA;IAChF;;GACL,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA;KACE,oBAAC,KAAD,EAAK,MAAM,CAAC,KAAK,GAAG,EAAI,CAAA;KAAC;KAAU,oBAAC,KAAD,EAAK,MAAK,IAAK,CAAA;KAAC;KAAS,oBAAC,KAAD,EAAK,MAAK,IAAK,CAAA;KAAC;KAC5E,oBAAC,QAAD;MAAQ,SAAQ;MAAQ,MAAK;MAAK,uBAAA;MAAoB,cAAW;MAC/D,UAAA,oBAAC,KAAD,EAAK,MAAK,IAAK,CAAA;KACT,CAAA;IACL;;GACJ,aAAa,aACZ,oBAAC,iBAAD;IAA0B;IAAgB;IAAiB;IAAmB;GAAU,CAAA,IAExF,oBAAC,OAAD;IAAO,MAAM,WAAW,SAAS,MAAM;IAAG,MAAA;IACvC,UAAA,SAAS;GACL,CAAA;EAEN;;AAET"}
|