@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,337 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { z } from "@hono/zod-openapi";
|
|
5
|
+
import { BUILD_STATUSES, PROJECT_ROLES, SNAPSHOT_STATUSES } from "@storyshelf/core/types";
|
|
6
|
+
import { BROWSER_NAMES } from "@storyshelf/core/adapter/capture-runner";
|
|
7
|
+
//#region src/routers/schemas.ts
|
|
8
|
+
const storybookMetaSchema = z.record(z.string(), z.unknown()).nullable().optional();
|
|
9
|
+
/** OpenAPI schema for a project record. */
|
|
10
|
+
const projectSchema = z.object({
|
|
11
|
+
id: z.string(),
|
|
12
|
+
name: z.string(),
|
|
13
|
+
slug: z.string(),
|
|
14
|
+
gitRepository: z.string().nullable(),
|
|
15
|
+
gitDefaultBranch: z.string(),
|
|
16
|
+
pixelThreshold: z.number(),
|
|
17
|
+
maxDiffRatio: z.number(),
|
|
18
|
+
publicBranchRegex: z.string().nullable(),
|
|
19
|
+
storybookMeta: z.string().nullable().optional(),
|
|
20
|
+
executePlay: z.boolean(),
|
|
21
|
+
playTimeoutMs: z.number(),
|
|
22
|
+
browser: z.string().optional(),
|
|
23
|
+
createdAt: z.string(),
|
|
24
|
+
updatedAt: z.string()
|
|
25
|
+
}).openapi("Project");
|
|
26
|
+
/** OpenAPI schema for creating a project. */
|
|
27
|
+
const projectCreateSchema = z.object({
|
|
28
|
+
name: z.string().min(1),
|
|
29
|
+
gitRepository: z.string().optional(),
|
|
30
|
+
gitDefaultBranch: z.string().optional(),
|
|
31
|
+
storybookMeta: storybookMetaSchema,
|
|
32
|
+
executePlay: z.boolean().optional(),
|
|
33
|
+
playTimeoutMs: z.number().int().min(1e3).max(3e4).optional()
|
|
34
|
+
}).openapi("ProjectCreateInput");
|
|
35
|
+
/** OpenAPI schema for patching a project. */
|
|
36
|
+
const projectUpdateSchema = z.object({
|
|
37
|
+
name: z.string().optional(),
|
|
38
|
+
gitRepository: z.string().optional(),
|
|
39
|
+
gitDefaultBranch: z.string().optional(),
|
|
40
|
+
pixelThreshold: z.number().optional(),
|
|
41
|
+
maxDiffRatio: z.number().optional(),
|
|
42
|
+
publicBranchRegex: z.string().nullable().optional(),
|
|
43
|
+
storybookMeta: storybookMetaSchema,
|
|
44
|
+
executePlay: z.boolean().optional(),
|
|
45
|
+
playTimeoutMs: z.number().int().min(1e3).max(3e4).optional(),
|
|
46
|
+
browser: z.enum(BROWSER_NAMES).optional()
|
|
47
|
+
}).openapi("ProjectUpdateInput");
|
|
48
|
+
/** OpenAPI schema for a build record. */
|
|
49
|
+
const buildSchema = z.object({
|
|
50
|
+
id: z.string(),
|
|
51
|
+
projectId: z.string(),
|
|
52
|
+
gitSha: z.string(),
|
|
53
|
+
gitBranch: z.string(),
|
|
54
|
+
isDefault: z.boolean(),
|
|
55
|
+
authorEmail: z.string().nullable(),
|
|
56
|
+
authorName: z.string().nullable(),
|
|
57
|
+
message: z.string().nullable(),
|
|
58
|
+
public: z.boolean(),
|
|
59
|
+
status: z.enum(BUILD_STATUSES),
|
|
60
|
+
snapshotCount: z.number(),
|
|
61
|
+
changedCount: z.number(),
|
|
62
|
+
approvedCount: z.number(),
|
|
63
|
+
rejectedCount: z.number(),
|
|
64
|
+
affectedOnly: z.boolean(),
|
|
65
|
+
baselineSha: z.string().nullable(),
|
|
66
|
+
changedFiles: z.string().nullable(),
|
|
67
|
+
affectedImportPaths: z.string().nullable(),
|
|
68
|
+
createdAt: z.string(),
|
|
69
|
+
updatedAt: z.string()
|
|
70
|
+
}).openapi("Build");
|
|
71
|
+
/** OpenAPI schema for a snapshot record. */
|
|
72
|
+
const snapshotSchema = z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
projectId: z.string(),
|
|
75
|
+
buildId: z.string(),
|
|
76
|
+
storyId: z.string(),
|
|
77
|
+
storyName: z.string(),
|
|
78
|
+
storyTitle: z.string(),
|
|
79
|
+
storyImportPath: z.string().nullable(),
|
|
80
|
+
viewportName: z.string(),
|
|
81
|
+
viewportWidth: z.number(),
|
|
82
|
+
viewportHeight: z.number(),
|
|
83
|
+
screenshotPath: z.string(),
|
|
84
|
+
diffPath: z.string().nullable(),
|
|
85
|
+
diffPixels: z.number().nullable(),
|
|
86
|
+
diffRatio: z.number().nullable(),
|
|
87
|
+
diffPassed: z.boolean().nullable(),
|
|
88
|
+
status: z.enum(SNAPSHOT_STATUSES),
|
|
89
|
+
reviewedBy: z.string().nullable(),
|
|
90
|
+
reviewedAt: z.string().nullable(),
|
|
91
|
+
inherited: z.boolean(),
|
|
92
|
+
createdAt: z.string(),
|
|
93
|
+
updatedAt: z.string()
|
|
94
|
+
}).openapi("Snapshot");
|
|
95
|
+
/** OpenAPI schema for a capture attempt (one run of a build's capture). */
|
|
96
|
+
const attemptSchema = z.object({
|
|
97
|
+
id: z.string(),
|
|
98
|
+
projectId: z.string(),
|
|
99
|
+
buildId: z.string(),
|
|
100
|
+
attemptNo: z.number(),
|
|
101
|
+
status: z.enum([
|
|
102
|
+
"queued",
|
|
103
|
+
"running",
|
|
104
|
+
"completed",
|
|
105
|
+
"failed",
|
|
106
|
+
"cancelled"
|
|
107
|
+
]),
|
|
108
|
+
error: z.string().nullable(),
|
|
109
|
+
reqId: z.string().nullable(),
|
|
110
|
+
storyCount: z.number(),
|
|
111
|
+
failedCount: z.number(),
|
|
112
|
+
queuedAt: z.string(),
|
|
113
|
+
startedAt: z.string().nullable(),
|
|
114
|
+
finishedAt: z.string().nullable(),
|
|
115
|
+
createdAt: z.string(),
|
|
116
|
+
updatedAt: z.string()
|
|
117
|
+
}).openapi("CaptureAttempt");
|
|
118
|
+
/** OpenAPI schema for a single capture log line within an attempt. */
|
|
119
|
+
const attemptLogSchema = z.object({
|
|
120
|
+
id: z.string(),
|
|
121
|
+
projectId: z.string(),
|
|
122
|
+
buildId: z.string(),
|
|
123
|
+
attemptId: z.string(),
|
|
124
|
+
seq: z.number(),
|
|
125
|
+
level: z.string(),
|
|
126
|
+
message: z.string(),
|
|
127
|
+
fields: z.string().nullable(),
|
|
128
|
+
createdAt: z.string()
|
|
129
|
+
}).openapi("CaptureLog");
|
|
130
|
+
/** OpenAPI schema for a build comment. */
|
|
131
|
+
const commentSchema = z.object({
|
|
132
|
+
id: z.string(),
|
|
133
|
+
projectId: z.string(),
|
|
134
|
+
buildId: z.string(),
|
|
135
|
+
snapshotId: z.string().nullable(),
|
|
136
|
+
userId: z.string().nullable(),
|
|
137
|
+
body: z.string(),
|
|
138
|
+
parentId: z.string().nullable(),
|
|
139
|
+
resolved: z.boolean(),
|
|
140
|
+
createdAt: z.string(),
|
|
141
|
+
updatedAt: z.string()
|
|
142
|
+
}).openapi("Comment");
|
|
143
|
+
/** OpenAPI schema for creating a build comment. */
|
|
144
|
+
const commentCreateSchema = z.object({
|
|
145
|
+
body: z.string().min(1),
|
|
146
|
+
snapshotId: z.string().optional(),
|
|
147
|
+
parentId: z.string().optional()
|
|
148
|
+
}).openapi("CommentCreateInput");
|
|
149
|
+
/** OpenAPI schema for a project label type. */
|
|
150
|
+
const labelTypeSchema = z.object({
|
|
151
|
+
id: z.string(),
|
|
152
|
+
projectId: z.string(),
|
|
153
|
+
key: z.string(),
|
|
154
|
+
name: z.string(),
|
|
155
|
+
linkTemplate: z.string().nullable(),
|
|
156
|
+
color: z.string().nullable(),
|
|
157
|
+
createdAt: z.string()
|
|
158
|
+
}).openapi("LabelType");
|
|
159
|
+
/** OpenAPI schema for creating a label type. */
|
|
160
|
+
const labelTypeCreateSchema = z.object({
|
|
161
|
+
key: z.string().min(1),
|
|
162
|
+
name: z.string().min(1),
|
|
163
|
+
linkTemplate: z.string().optional(),
|
|
164
|
+
color: z.string().optional()
|
|
165
|
+
}).openapi("LabelTypeCreateInput");
|
|
166
|
+
/** OpenAPI schema for patching a label type. */
|
|
167
|
+
const labelTypeUpdateSchema = z.object({
|
|
168
|
+
name: z.string().min(1).optional(),
|
|
169
|
+
linkTemplate: z.string().nullable().optional(),
|
|
170
|
+
color: z.string().nullable().optional()
|
|
171
|
+
}).openapi("LabelTypeUpdateInput");
|
|
172
|
+
/** OpenAPI schema for a public API token (hash omitted). */
|
|
173
|
+
const tokenPublicSchema = z.object({
|
|
174
|
+
id: z.string(),
|
|
175
|
+
projectId: z.string(),
|
|
176
|
+
name: z.string(),
|
|
177
|
+
lastUsedAt: z.string().nullable(),
|
|
178
|
+
createdAt: z.string()
|
|
179
|
+
}).openapi("Token");
|
|
180
|
+
/** OpenAPI schema for creating an API token. */
|
|
181
|
+
const tokenCreateSchema = z.object({ name: z.string().min(1) }).openapi("TokenCreateInput");
|
|
182
|
+
/** OpenAPI schema for a newly created token including its secret value. */
|
|
183
|
+
const tokenCreatedSchema = z.object({
|
|
184
|
+
name: z.string(),
|
|
185
|
+
token: z.string()
|
|
186
|
+
}).openapi("TokenCreated");
|
|
187
|
+
/** OpenAPI schema for a project member record. */
|
|
188
|
+
const memberSchema = z.object({
|
|
189
|
+
id: z.string(),
|
|
190
|
+
projectId: z.string(),
|
|
191
|
+
userId: z.string(),
|
|
192
|
+
role: z.enum(PROJECT_ROLES),
|
|
193
|
+
createdAt: z.string()
|
|
194
|
+
}).openapi("ProjectMember");
|
|
195
|
+
/** OpenAPI schema for updating a member's role. */
|
|
196
|
+
const memberRoleSchema = z.object({ role: z.enum(PROJECT_ROLES) }).openapi("MemberRoleInput");
|
|
197
|
+
/** OpenAPI schema for adding a member to a project. */
|
|
198
|
+
const memberSetSchema = z.object({
|
|
199
|
+
userId: z.string().min(1),
|
|
200
|
+
role: z.enum(PROJECT_ROLES)
|
|
201
|
+
}).openapi("MemberSetInput");
|
|
202
|
+
/** OpenAPI schema for an identity-provider group mapping. */
|
|
203
|
+
const groupMappingSchema = z.object({
|
|
204
|
+
id: z.string(),
|
|
205
|
+
projectId: z.string(),
|
|
206
|
+
groupName: z.string(),
|
|
207
|
+
role: z.enum(PROJECT_ROLES),
|
|
208
|
+
createdAt: z.string()
|
|
209
|
+
}).openapi("GroupMapping");
|
|
210
|
+
/** OpenAPI schema for creating a group mapping. */
|
|
211
|
+
const groupMappingCreateSchema = z.object({
|
|
212
|
+
groupName: z.string().min(1),
|
|
213
|
+
role: z.enum(PROJECT_ROLES)
|
|
214
|
+
}).openapi("GroupMappingCreateInput");
|
|
215
|
+
/** OpenAPI schema for a public webhook (secret omitted). */
|
|
216
|
+
const webhookPublicSchema = z.object({
|
|
217
|
+
id: z.string(),
|
|
218
|
+
url: z.string(),
|
|
219
|
+
events: z.array(z.string())
|
|
220
|
+
}).openapi("Webhook");
|
|
221
|
+
/** OpenAPI schema for creating a webhook. */
|
|
222
|
+
const webhookCreateSchema = z.object({
|
|
223
|
+
url: z.string().url(),
|
|
224
|
+
events: z.array(z.string().min(1)).optional()
|
|
225
|
+
}).openapi("WebhookCreateInput");
|
|
226
|
+
/** OpenAPI schema for a newly created webhook including its signing secret. */
|
|
227
|
+
const webhookCreatedSchema = z.object({
|
|
228
|
+
id: z.string(),
|
|
229
|
+
url: z.string(),
|
|
230
|
+
events: z.array(z.string()),
|
|
231
|
+
secret: z.string()
|
|
232
|
+
}).openapi("WebhookCreated");
|
|
233
|
+
/** OpenAPI schema for a git status-check configuration. */
|
|
234
|
+
const statusConfigSchema = z.object({
|
|
235
|
+
id: z.string(),
|
|
236
|
+
projectId: z.string(),
|
|
237
|
+
provider: z.string(),
|
|
238
|
+
config: z.record(z.string(), z.unknown()),
|
|
239
|
+
hasToken: z.boolean(),
|
|
240
|
+
createdAt: z.string(),
|
|
241
|
+
updatedAt: z.string()
|
|
242
|
+
}).openapi("StatusConfig");
|
|
243
|
+
/** OpenAPI schema for creating a git status-check configuration. */
|
|
244
|
+
const statusConfigCreateSchema = z.object({
|
|
245
|
+
provider: z.string().min(1),
|
|
246
|
+
config: z.record(z.string(), z.unknown()),
|
|
247
|
+
token: z.string().min(1)
|
|
248
|
+
}).openapi("StatusConfigCreateInput");
|
|
249
|
+
/** OpenAPI schema for a public notification channel (secret omitted). */
|
|
250
|
+
const notificationChannelSchema = z.object({
|
|
251
|
+
id: z.string(),
|
|
252
|
+
projectId: z.string().nullable(),
|
|
253
|
+
provider: z.string(),
|
|
254
|
+
config: z.record(z.string(), z.unknown()),
|
|
255
|
+
hasSecret: z.boolean(),
|
|
256
|
+
events: z.array(z.string()),
|
|
257
|
+
enabled: z.boolean(),
|
|
258
|
+
createdAt: z.string(),
|
|
259
|
+
updatedAt: z.string()
|
|
260
|
+
}).openapi("NotificationChannel");
|
|
261
|
+
/** OpenAPI schema for creating a notification channel. */
|
|
262
|
+
const notificationChannelCreateSchema = z.object({
|
|
263
|
+
provider: z.string().min(1),
|
|
264
|
+
config: z.record(z.string(), z.unknown()),
|
|
265
|
+
secret: z.string().min(1).optional(),
|
|
266
|
+
events: z.array(z.string().min(1)).optional(),
|
|
267
|
+
enabled: z.boolean().optional()
|
|
268
|
+
}).openapi("NotificationChannelCreateInput");
|
|
269
|
+
/** OpenAPI schema for a per-user notification subscription. */
|
|
270
|
+
const notificationSubscriptionSchema = z.object({
|
|
271
|
+
projectId: z.string(),
|
|
272
|
+
userId: z.string(),
|
|
273
|
+
events: z.array(z.string()),
|
|
274
|
+
via: z.array(z.string()),
|
|
275
|
+
enabled: z.boolean()
|
|
276
|
+
}).openapi("NotificationSubscription");
|
|
277
|
+
/** OpenAPI schema for setting your own notification subscription. */
|
|
278
|
+
const notificationSubscriptionPutSchema = z.object({
|
|
279
|
+
events: z.array(z.string().min(1)).optional(),
|
|
280
|
+
via: z.array(z.string().min(1)).optional(),
|
|
281
|
+
enabled: z.boolean().optional()
|
|
282
|
+
}).openapi("NotificationSubscriptionPutInput");
|
|
283
|
+
/** OpenAPI schema for a generic `{ ok: true }` acknowledgement. */
|
|
284
|
+
const okSchema = z.object({ ok: z.boolean() }).openapi("Ok");
|
|
285
|
+
/** Bulk approve result: ids skipped because their baseline changed after the diff. */
|
|
286
|
+
const approveAllResultSchema = z.object({
|
|
287
|
+
ok: z.boolean(),
|
|
288
|
+
skipped: z.array(z.string())
|
|
289
|
+
}).openapi("ApproveAllResult");
|
|
290
|
+
/** Result of re-diffing a build against the baselines that apply now. */
|
|
291
|
+
const rediffResultSchema = z.object({
|
|
292
|
+
ok: z.boolean(),
|
|
293
|
+
rediffed: z.number().int(),
|
|
294
|
+
statusChanged: z.number().int(),
|
|
295
|
+
missingScreenshots: z.array(z.string())
|
|
296
|
+
}).openapi("RediffResult");
|
|
297
|
+
/** 409 returned when approving a snapshot whose baseline changed after the diff. */
|
|
298
|
+
const baselineChangedSchema = z.object({
|
|
299
|
+
code: z.literal("baseline_changed"),
|
|
300
|
+
message: z.string(),
|
|
301
|
+
snapshotId: z.string(),
|
|
302
|
+
baselineStatus: z.enum(["stale", "removed"])
|
|
303
|
+
}).openapi("BaselineChanged");
|
|
304
|
+
/** OpenAPI schema for an error response message. */
|
|
305
|
+
const errorSchema = z.object({ message: z.string() }).openapi("Error");
|
|
306
|
+
/** OpenAPI schema for the retention purge result. */
|
|
307
|
+
const purgeSchema = z.object({
|
|
308
|
+
removedBuilds: z.number(),
|
|
309
|
+
removedBranches: z.number().optional(),
|
|
310
|
+
removedBaselines: z.number().optional()
|
|
311
|
+
}).openapi("PurgeResult");
|
|
312
|
+
/** OpenAPI schema for the retention purge input. */
|
|
313
|
+
const purgeInputSchema = z.object({ ttlDays: z.number().optional() }).openapi("PurgeInput");
|
|
314
|
+
/** Shared response spec for a 400 Bad Request. */
|
|
315
|
+
const badRequest = { 400: {
|
|
316
|
+
content: { "application/json": { schema: errorSchema } },
|
|
317
|
+
description: "Bad request"
|
|
318
|
+
} };
|
|
319
|
+
/** Shared response spec for a 401 Unauthorized. */
|
|
320
|
+
const unauthorized = { 401: {
|
|
321
|
+
content: { "application/json": { schema: errorSchema } },
|
|
322
|
+
description: "Unauthorized"
|
|
323
|
+
} };
|
|
324
|
+
/** Shared response spec for a 403 Forbidden. */
|
|
325
|
+
const forbidden = { 403: {
|
|
326
|
+
content: { "application/json": { schema: errorSchema } },
|
|
327
|
+
description: "Forbidden"
|
|
328
|
+
} };
|
|
329
|
+
/** Shared response spec for a 404 Not Found. */
|
|
330
|
+
const notFound = { 404: {
|
|
331
|
+
content: { "application/json": { schema: errorSchema } },
|
|
332
|
+
description: "Not found"
|
|
333
|
+
} };
|
|
334
|
+
//#endregion
|
|
335
|
+
export { approveAllResultSchema, attemptLogSchema, attemptSchema, badRequest, baselineChangedSchema, buildSchema, commentCreateSchema, commentSchema, errorSchema, forbidden, groupMappingCreateSchema, groupMappingSchema, labelTypeCreateSchema, labelTypeSchema, labelTypeUpdateSchema, memberRoleSchema, memberSchema, memberSetSchema, notFound, notificationChannelCreateSchema, notificationChannelSchema, notificationSubscriptionPutSchema, notificationSubscriptionSchema, okSchema, projectCreateSchema, projectSchema, projectUpdateSchema, purgeInputSchema, purgeSchema, rediffResultSchema, snapshotSchema, statusConfigCreateSchema, statusConfigSchema, tokenCreateSchema, tokenCreatedSchema, tokenPublicSchema, unauthorized, webhookCreateSchema, webhookCreatedSchema, webhookPublicSchema };
|
|
336
|
+
|
|
337
|
+
//# sourceMappingURL=schemas.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.mjs","names":[],"sources":["../../src/routers/schemas.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport { BROWSER_NAMES } from \"@storyshelf/core/adapter/capture-runner\";\nimport { BUILD_STATUSES, PROJECT_ROLES, SNAPSHOT_STATUSES } from \"@storyshelf/core/types\";\n\nconst storybookMetaSchema = z.record(z.string(), z.unknown()).nullable().optional();\n\n/** OpenAPI schema for a project record. */\nexport const projectSchema = z\n .object({\n id: z.string(),\n name: z.string(),\n slug: z.string(),\n gitRepository: z.string().nullable(),\n gitDefaultBranch: z.string(),\n pixelThreshold: z.number(),\n maxDiffRatio: z.number(),\n publicBranchRegex: z.string().nullable(),\n storybookMeta: z.string().nullable().optional(),\n executePlay: z.boolean(),\n playTimeoutMs: z.number(),\n // Stored rows predate enum validation, so the response stays a plain string.\n browser: z.string().optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Project\");\n\n/** OpenAPI schema for creating a project. */\nexport const projectCreateSchema = z\n .object({\n name: z.string().min(1),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n storybookMeta: storybookMetaSchema,\n executePlay: z.boolean().optional(),\n playTimeoutMs: z.number().int().min(1000).max(30_000).optional(),\n })\n .openapi(\"ProjectCreateInput\");\n\n/** OpenAPI schema for patching a project. */\nexport const projectUpdateSchema = z\n .object({\n name: z.string().optional(),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n pixelThreshold: z.number().optional(),\n maxDiffRatio: z.number().optional(),\n publicBranchRegex: z.string().nullable().optional(),\n storybookMeta: storybookMetaSchema,\n executePlay: z.boolean().optional(),\n playTimeoutMs: z.number().int().min(1000).max(30_000).optional(),\n browser: z.enum(BROWSER_NAMES).optional(),\n })\n .openapi(\"ProjectUpdateInput\");\n\n/** OpenAPI schema for a build record. */\nexport const buildSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n gitSha: z.string(),\n gitBranch: z.string(),\n isDefault: z.boolean(),\n authorEmail: z.string().nullable(),\n authorName: z.string().nullable(),\n message: z.string().nullable(),\n public: z.boolean(),\n status: z.enum(BUILD_STATUSES),\n snapshotCount: z.number(),\n changedCount: z.number(),\n approvedCount: z.number(),\n rejectedCount: z.number(),\n affectedOnly: z.boolean(),\n baselineSha: z.string().nullable(),\n changedFiles: z.string().nullable(),\n affectedImportPaths: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Build\");\n\n/** OpenAPI schema for a snapshot record. */\nexport const snapshotSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n storyId: z.string(),\n storyName: z.string(),\n storyTitle: z.string(),\n storyImportPath: z.string().nullable(),\n viewportName: z.string(),\n viewportWidth: z.number(),\n viewportHeight: z.number(),\n screenshotPath: z.string(),\n diffPath: z.string().nullable(),\n diffPixels: z.number().nullable(),\n diffRatio: z.number().nullable(),\n diffPassed: z.boolean().nullable(),\n status: z.enum(SNAPSHOT_STATUSES),\n reviewedBy: z.string().nullable(),\n reviewedAt: z.string().nullable(),\n inherited: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Snapshot\");\n\n/** OpenAPI schema for a capture attempt (one run of a build's capture). */\nexport const attemptSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n attemptNo: z.number(),\n status: z.enum([\"queued\", \"running\", \"completed\", \"failed\", \"cancelled\"]),\n error: z.string().nullable(),\n reqId: z.string().nullable(),\n storyCount: z.number(),\n failedCount: z.number(),\n queuedAt: z.string(),\n startedAt: z.string().nullable(),\n finishedAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"CaptureAttempt\");\n\n/** OpenAPI schema for a single capture log line within an attempt. */\nexport const attemptLogSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n attemptId: z.string(),\n seq: z.number(),\n level: z.string(),\n message: z.string(),\n fields: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"CaptureLog\");\n\n/** OpenAPI schema for a build comment. */\nexport const commentSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n snapshotId: z.string().nullable(),\n userId: z.string().nullable(),\n body: z.string(),\n parentId: z.string().nullable(),\n resolved: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"Comment\");\n\n/** OpenAPI schema for creating a build comment. */\nexport const commentCreateSchema = z\n .object({\n body: z.string().min(1),\n snapshotId: z.string().optional(),\n parentId: z.string().optional(),\n })\n .openapi(\"CommentCreateInput\");\n\n/** OpenAPI schema for a project label type. */\nexport const labelTypeSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n key: z.string(),\n name: z.string(),\n linkTemplate: z.string().nullable(),\n color: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"LabelType\");\n\n/** OpenAPI schema for creating a label type. */\nexport const labelTypeCreateSchema = z\n .object({\n key: z.string().min(1),\n name: z.string().min(1),\n linkTemplate: z.string().optional(),\n color: z.string().optional(),\n })\n .openapi(\"LabelTypeCreateInput\");\n\n/** OpenAPI schema for patching a label type. */\nexport const labelTypeUpdateSchema = z\n .object({\n name: z.string().min(1).optional(),\n linkTemplate: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n })\n .openapi(\"LabelTypeUpdateInput\");\n\n/** OpenAPI schema for a public API token (hash omitted). */\nexport const tokenPublicSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n name: z.string(),\n lastUsedAt: z.string().nullable(),\n createdAt: z.string(),\n })\n .openapi(\"Token\");\n\n/** OpenAPI schema for creating an API token. */\nexport const tokenCreateSchema = z.object({ name: z.string().min(1) }).openapi(\"TokenCreateInput\");\n\n/** OpenAPI schema for a newly created token including its secret value. */\nexport const tokenCreatedSchema = z\n .object({\n name: z.string(),\n token: z.string(),\n })\n .openapi(\"TokenCreated\");\n\n/** OpenAPI schema for a project member record. */\nexport const memberSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n userId: z.string(),\n role: z.enum(PROJECT_ROLES),\n createdAt: z.string(),\n })\n .openapi(\"ProjectMember\");\n\n/** OpenAPI schema for updating a member's role. */\nexport const memberRoleSchema = z\n .object({ role: z.enum(PROJECT_ROLES) })\n .openapi(\"MemberRoleInput\");\n\n/** OpenAPI schema for adding a member to a project. */\nexport const memberSetSchema = z\n .object({\n userId: z.string().min(1),\n role: z.enum(PROJECT_ROLES),\n })\n .openapi(\"MemberSetInput\");\n\n/** OpenAPI schema for an identity-provider group mapping. */\nexport const groupMappingSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n groupName: z.string(),\n role: z.enum(PROJECT_ROLES),\n createdAt: z.string(),\n })\n .openapi(\"GroupMapping\");\n\n/** OpenAPI schema for creating a group mapping. */\nexport const groupMappingCreateSchema = z\n .object({\n groupName: z.string().min(1),\n role: z.enum(PROJECT_ROLES),\n })\n .openapi(\"GroupMappingCreateInput\");\n\n/** OpenAPI schema for a public webhook (secret omitted). */\nexport const webhookPublicSchema = z\n .object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n })\n .openapi(\"Webhook\");\n\n/** OpenAPI schema for creating a webhook. */\nexport const webhookCreateSchema = z\n .object({\n // oxlint-disable-next-line typescript/no-deprecated -- z.string().url() kept for zod v3 API compat\n url: z.string().url(),\n events: z.array(z.string().min(1)).optional(),\n })\n .openapi(\"WebhookCreateInput\");\n\n/** OpenAPI schema for a newly created webhook including its signing secret. */\nexport const webhookCreatedSchema = z\n .object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n secret: z.string(),\n })\n .openapi(\"WebhookCreated\");\n\n/** OpenAPI schema for a git status-check configuration. */\nexport const statusConfigSchema = z\n .object({\n id: z.string(),\n projectId: z.string(),\n provider: z.string(),\n config: z.record(z.string(), z.unknown()),\n hasToken: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"StatusConfig\");\n\n/** OpenAPI schema for creating a git status-check configuration. */\nexport const statusConfigCreateSchema = z\n .object({\n provider: z.string().min(1),\n config: z.record(z.string(), z.unknown()),\n token: z.string().min(1),\n })\n .openapi(\"StatusConfigCreateInput\");\n\n/** OpenAPI schema for a public notification channel (secret omitted). */\nexport const notificationChannelSchema = z\n .object({\n id: z.string(),\n projectId: z.string().nullable(),\n provider: z.string(),\n config: z.record(z.string(), z.unknown()),\n hasSecret: z.boolean(),\n events: z.array(z.string()),\n enabled: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n })\n .openapi(\"NotificationChannel\");\n\n/** OpenAPI schema for creating a notification channel. */\nexport const notificationChannelCreateSchema = z\n .object({\n provider: z.string().min(1),\n config: z.record(z.string(), z.unknown()),\n secret: z.string().min(1).optional(),\n events: z.array(z.string().min(1)).optional(),\n enabled: z.boolean().optional(),\n })\n .openapi(\"NotificationChannelCreateInput\");\n\n/** OpenAPI schema for a per-user notification subscription. */\nexport const notificationSubscriptionSchema = z\n .object({\n projectId: z.string(),\n userId: z.string(),\n events: z.array(z.string()),\n via: z.array(z.string()),\n enabled: z.boolean(),\n })\n .openapi(\"NotificationSubscription\");\n\n/** OpenAPI schema for setting your own notification subscription. */\nexport const notificationSubscriptionPutSchema = z\n .object({\n events: z.array(z.string().min(1)).optional(),\n via: z.array(z.string().min(1)).optional(),\n enabled: z.boolean().optional(),\n })\n .openapi(\"NotificationSubscriptionPutInput\");\n\n/** OpenAPI schema for a generic `{ ok: true }` acknowledgement. */\nexport const okSchema = z.object({ ok: z.boolean() }).openapi(\"Ok\");\n\n/** Bulk approve result: ids skipped because their baseline changed after the diff. */\nexport const approveAllResultSchema = z\n .object({ ok: z.boolean(), skipped: z.array(z.string()) })\n .openapi(\"ApproveAllResult\");\n\n/** Result of re-diffing a build against the baselines that apply now. */\nexport const rediffResultSchema = z\n .object({\n ok: z.boolean(),\n rediffed: z.number().int(),\n statusChanged: z.number().int(),\n missingScreenshots: z.array(z.string()),\n })\n .openapi(\"RediffResult\");\n\n/** 409 returned when approving a snapshot whose baseline changed after the diff. */\nexport const baselineChangedSchema = z\n .object({\n code: z.literal(\"baseline_changed\"),\n message: z.string(),\n snapshotId: z.string(),\n baselineStatus: z.enum([\"stale\", \"removed\"]),\n })\n .openapi(\"BaselineChanged\");\n\n/** OpenAPI schema for an error response message. */\nexport const errorSchema = z.object({ message: z.string() }).openapi(\"Error\");\n\n/** OpenAPI schema for the retention purge result. */\nexport const purgeSchema = z\n .object({\n removedBuilds: z.number(),\n removedBranches: z.number().optional(),\n removedBaselines: z.number().optional(),\n })\n .openapi(\"PurgeResult\");\n\n/** OpenAPI schema for the retention purge input. */\nexport const purgeInputSchema = z.object({ ttlDays: z.number().optional() }).openapi(\"PurgeInput\");\n\n/** Shared response spec for a 400 Bad Request. */\nexport const badRequest = {\n 400: { content: { \"application/json\": { schema: errorSchema } }, description: \"Bad request\" },\n} as const;\n\n/** Shared response spec for a 401 Unauthorized. */\nexport const unauthorized = {\n 401: { content: { \"application/json\": { schema: errorSchema } }, description: \"Unauthorized\" },\n} as const;\n\n/** Shared response spec for a 403 Forbidden. */\nexport const forbidden = {\n 403: { content: { \"application/json\": { schema: errorSchema } }, description: \"Forbidden\" },\n} as const;\n\n/** Shared response spec for a 404 Not Found. */\nexport const notFound = {\n 404: { content: { \"application/json\": { schema: errorSchema } }, description: \"Not found\" },\n} as const;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { hxRedirect } from "./htmx.mjs";
|
|
6
|
+
import { flash } from "./flash.mjs";
|
|
7
|
+
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
8
|
+
import { ProjectModel } from "@storyshelf/core/models";
|
|
9
|
+
import { BROWSER_NAMES } from "@storyshelf/core/adapter/capture-runner";
|
|
10
|
+
//#region src/routers/settings-general.ts
|
|
11
|
+
/** General project settings (name, repo, thresholds) and danger-zone delete. */
|
|
12
|
+
function registerGeneralSettings(app) {
|
|
13
|
+
app.get("/projects/:slug/settings", async (c) => c.html(await renderSettingsPage(c, "general")));
|
|
14
|
+
app.get("/projects/:slug/settings/tests", async (c) => c.html(await renderSettingsPage(c, "tests")));
|
|
15
|
+
app.post("/projects/:slug/settings", handleGeneralUpdate);
|
|
16
|
+
app.post("/projects/:slug/settings/tests", handleTestsUpdate);
|
|
17
|
+
app.post("/projects/:slug/delete", handleDeleteProject);
|
|
18
|
+
}
|
|
19
|
+
/** Read the general-settings form fields (name validated by the caller). */
|
|
20
|
+
function readGeneralFields(form) {
|
|
21
|
+
return {
|
|
22
|
+
gitRepository: asString(form.get("gitRepository")),
|
|
23
|
+
gitDefaultBranch: asString(form.get("gitDefaultBranch")),
|
|
24
|
+
pixelThreshold: asString(form.get("pixelThreshold")),
|
|
25
|
+
maxDiffRatio: asString(form.get("maxDiffRatio")),
|
|
26
|
+
publicBranchRegex: asString(form.get("publicBranchRegex")),
|
|
27
|
+
browser: asString(form.get("browser"))
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function handleGeneralUpdate(c) {
|
|
31
|
+
const slug = c.req.param("slug") ?? "";
|
|
32
|
+
const project = await findProject(slug);
|
|
33
|
+
const form = await c.req.formData();
|
|
34
|
+
const name = form.get("name");
|
|
35
|
+
if (typeof name !== "string" || name.trim() === "") return c.html(await renderSettingsPage(c, "general", { errors: { name: "Name is required" } }) ?? "", 400);
|
|
36
|
+
const fields = {
|
|
37
|
+
...readGeneralFields(form),
|
|
38
|
+
name: name.trim()
|
|
39
|
+
};
|
|
40
|
+
if (fields.browser !== void 0 && !BROWSER_NAMES.includes(fields.browser)) return c.html(await renderSettingsPage(c, "general", { globalError: `Invalid browser "${fields.browser}". Choose one of: ${BROWSER_NAMES.join(", ")}.` }) ?? "", 400);
|
|
41
|
+
return await persistGeneralUpdate(c, project, slug, fields);
|
|
42
|
+
}
|
|
43
|
+
async function persistGeneralUpdate(c, project, slug, fields) {
|
|
44
|
+
try {
|
|
45
|
+
await new ProjectModel(getStore().db).update(project.id, {
|
|
46
|
+
name: fields.name,
|
|
47
|
+
gitRepository: fields.gitRepository ?? void 0,
|
|
48
|
+
gitDefaultBranch: fields.gitDefaultBranch ?? void 0,
|
|
49
|
+
pixelThreshold: fields.pixelThreshold ? Number(fields.pixelThreshold) : void 0,
|
|
50
|
+
maxDiffRatio: fields.maxDiffRatio ? Number(fields.maxDiffRatio) : void 0,
|
|
51
|
+
publicBranchRegex: fields.publicBranchRegex === "" ? null : fields.publicBranchRegex ?? void 0,
|
|
52
|
+
browser: fields.browser ?? void 0
|
|
53
|
+
});
|
|
54
|
+
flash(c, "Project settings saved");
|
|
55
|
+
return hxRedirect(c, `/projects/${slug}/settings`);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : "Failed to update";
|
|
58
|
+
return c.html(await renderSettingsPage(c, "general", { globalError: message }) ?? "", 400);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function handleTestsUpdate(c) {
|
|
62
|
+
const slug = c.req.param("slug") ?? "";
|
|
63
|
+
const project = await findProject(slug);
|
|
64
|
+
const { executePlay, playTimeoutMs } = readTestsFields(await c.req.formData());
|
|
65
|
+
const rangeError = playTimeoutError(playTimeoutMs);
|
|
66
|
+
if (rangeError) return c.html(await renderSettingsPage(c, "tests", { globalError: rangeError }) ?? "", 400);
|
|
67
|
+
return await persistTestsUpdate(c, project, slug, executePlay, playTimeoutMs);
|
|
68
|
+
}
|
|
69
|
+
/** Read the tests-tab form fields. */
|
|
70
|
+
function readTestsFields(form) {
|
|
71
|
+
const playTimeoutMsRaw = asString(form.get("playTimeoutMs"));
|
|
72
|
+
return {
|
|
73
|
+
executePlay: form.get("executePlay") === "true",
|
|
74
|
+
playTimeoutMs: playTimeoutMsRaw ? Number(playTimeoutMsRaw) : void 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Range error for the play timeout, or null when valid. */
|
|
78
|
+
function playTimeoutError(playTimeoutMs) {
|
|
79
|
+
if (playTimeoutMs !== void 0 && (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1e3 || playTimeoutMs > 3e4)) return "Play timeout must be between 1000 and 30000";
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
async function persistTestsUpdate(c, project, slug, executePlay, playTimeoutMs) {
|
|
83
|
+
try {
|
|
84
|
+
await new ProjectModel(getStore().db).update(project.id, {
|
|
85
|
+
executePlay,
|
|
86
|
+
playTimeoutMs: playTimeoutMs ?? void 0
|
|
87
|
+
});
|
|
88
|
+
flash(c, "Test settings saved");
|
|
89
|
+
return hxRedirect(c, `/projects/${slug}/settings/tests`);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : "Failed to update";
|
|
92
|
+
return c.html(await renderSettingsPage(c, "tests", { globalError: message }) ?? "", 400);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function handleDeleteProject(c) {
|
|
96
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
97
|
+
await new ProjectModel(getStore().db).remove(project.id);
|
|
98
|
+
flash(c, `Deleted ${project.name}`);
|
|
99
|
+
return hxRedirect(c, "/projects");
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { registerGeneralSettings };
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=settings-general.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/routers/settings-general.ts"],"sourcesContent":["import { BROWSER_NAMES } from \"@storyshelf/core/adapter/capture-runner\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** General project settings (name, repo, thresholds) and danger-zone delete. */\nexport function registerGeneralSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings\", async (c) => c.html(await renderSettingsPage(c, \"general\")));\n app.get(\"/projects/:slug/settings/tests\", async (c) =>\n c.html(await renderSettingsPage(c, \"tests\")),\n );\n app.post(\"/projects/:slug/settings\", handleGeneralUpdate);\n app.post(\"/projects/:slug/settings/tests\", handleTestsUpdate);\n app.post(\"/projects/:slug/delete\", handleDeleteProject);\n}\n\ninterface GeneralFields {\n name: string;\n gitRepository?: string;\n gitDefaultBranch?: string;\n pixelThreshold?: string;\n maxDiffRatio?: string;\n publicBranchRegex?: string;\n browser?: string;\n}\n\n/** Read the general-settings form fields (name validated by the caller). */\nfunction readGeneralFields(form: FormData): Omit<GeneralFields, \"name\"> {\n return {\n gitRepository: asString(form.get(\"gitRepository\")),\n gitDefaultBranch: asString(form.get(\"gitDefaultBranch\")),\n pixelThreshold: asString(form.get(\"pixelThreshold\")),\n maxDiffRatio: asString(form.get(\"maxDiffRatio\")),\n publicBranchRegex: asString(form.get(\"publicBranchRegex\")),\n browser: asString(form.get(\"browser\")),\n };\n}\n\nasync function handleGeneralUpdate(c: Context): Promise<Response> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const project = await findProject(slug);\n const form = await c.req.formData();\n const name = form.get(\"name\");\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return c.html(\n (await renderSettingsPage(c, \"general\", { errors: { name: \"Name is required\" } })) ?? \"\",\n 400,\n );\n }\n const fields: GeneralFields = { ...readGeneralFields(form), name: name.trim() };\n if (\n fields.browser !== undefined &&\n !(BROWSER_NAMES as readonly string[]).includes(fields.browser)\n ) {\n return c.html(\n (await renderSettingsPage(c, \"general\", {\n globalError: `Invalid browser \"${fields.browser}\". Choose one of: ${BROWSER_NAMES.join(\", \")}.`,\n })) ?? \"\",\n 400,\n );\n }\n return await persistGeneralUpdate(c, project, slug, fields);\n}\n\nasync function persistGeneralUpdate(\n c: Context,\n project: Project,\n slug: string,\n fields: GeneralFields,\n): Promise<Response> {\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n name: fields.name,\n gitRepository: fields.gitRepository ?? undefined,\n gitDefaultBranch: fields.gitDefaultBranch ?? undefined,\n pixelThreshold: fields.pixelThreshold ? Number(fields.pixelThreshold) : undefined,\n maxDiffRatio: fields.maxDiffRatio ? Number(fields.maxDiffRatio) : undefined,\n publicBranchRegex:\n fields.publicBranchRegex === \"\" ? null : (fields.publicBranchRegex ?? undefined),\n browser: fields.browser ?? undefined,\n });\n flash(c, \"Project settings saved\");\n return hxRedirect(c, `/projects/${slug}/settings`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"general\", { globalError: message })) ?? \"\", 400);\n }\n}\n\nasync function handleTestsUpdate(c: Context): Promise<Response> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const project = await findProject(slug);\n const form = await c.req.formData();\n const { executePlay, playTimeoutMs } = readTestsFields(form);\n const rangeError = playTimeoutError(playTimeoutMs);\n if (rangeError) {\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: rangeError })) ?? \"\", 400);\n }\n return await persistTestsUpdate(c, project, slug, executePlay, playTimeoutMs);\n}\n\n/** Read the tests-tab form fields. */\nfunction readTestsFields(form: FormData): {\n executePlay: boolean;\n playTimeoutMs: number | undefined;\n} {\n const playTimeoutMsRaw = asString(form.get(\"playTimeoutMs\"));\n return {\n executePlay: form.get(\"executePlay\") === \"true\",\n playTimeoutMs: playTimeoutMsRaw ? Number(playTimeoutMsRaw) : undefined,\n };\n}\n\n/** Range error for the play timeout, or null when valid. */\nfunction playTimeoutError(playTimeoutMs: number | undefined): string | null {\n if (\n playTimeoutMs !== undefined &&\n (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1000 || playTimeoutMs > 30_000)\n ) {\n return \"Play timeout must be between 1000 and 30000\";\n }\n return null;\n}\n\nasync function persistTestsUpdate(\n c: Context,\n project: Project,\n slug: string,\n executePlay: boolean,\n playTimeoutMs: number | undefined,\n): Promise<Response> {\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n executePlay,\n playTimeoutMs: playTimeoutMs ?? undefined,\n });\n flash(c, \"Test settings saved\");\n return hxRedirect(c, `/projects/${slug}/settings/tests`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: message })) ?? \"\", 400);\n }\n}\n\nasync function handleDeleteProject(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new ProjectModel(getStore().db).remove(project.id);\n flash(c, `Deleted ${project.name}`);\n return hxRedirect(c, \"/projects\");\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { hxRedirect } from "./htmx.mjs";
|
|
6
|
+
import { flash } from "./flash.mjs";
|
|
7
|
+
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
8
|
+
import { LabelModel } from "@storyshelf/core/models";
|
|
9
|
+
//#region src/routers/settings-labels.ts
|
|
10
|
+
/** Label-type settings (create custom types, delete removable ones). */
|
|
11
|
+
function registerLabelSettings(app) {
|
|
12
|
+
app.get("/projects/:slug/settings/labels", async (c) => c.html(await renderSettingsPage(c, "labels")));
|
|
13
|
+
app.post("/projects/:slug/settings/labels", handleCreateLabelType);
|
|
14
|
+
app.post("/projects/:slug/settings/labels/:key/delete", handleDeleteLabelType);
|
|
15
|
+
}
|
|
16
|
+
async function handleCreateLabelType(c) {
|
|
17
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
18
|
+
const form = await c.req.formData();
|
|
19
|
+
const key = asString(form.get("key"));
|
|
20
|
+
const labelName = asString(form.get("labelName")) ?? asString(form.get("name"));
|
|
21
|
+
const linkTemplate = asString(form.get("linkTemplate"));
|
|
22
|
+
if (!key || !labelName) return c.html(await renderSettingsPage(c, "labels", { globalError: "Key and name are required" }) ?? "", 400);
|
|
23
|
+
return await persistLabelType(c, project.id, project.slug, key, labelName, linkTemplate);
|
|
24
|
+
}
|
|
25
|
+
async function persistLabelType(c, projectId, projectSlug, key, labelName, linkTemplate) {
|
|
26
|
+
try {
|
|
27
|
+
await new LabelModel(getStore().db).createType(projectId, {
|
|
28
|
+
key,
|
|
29
|
+
name: labelName,
|
|
30
|
+
linkTemplate
|
|
31
|
+
});
|
|
32
|
+
flash(c, "Label type added");
|
|
33
|
+
return hxRedirect(c, `/projects/${projectSlug}/settings/labels`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : "Failed to create label";
|
|
36
|
+
return c.html(await renderSettingsPage(c, "labels", { globalError: message }) ?? "", 400);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function handleDeleteLabelType(c) {
|
|
40
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
41
|
+
try {
|
|
42
|
+
await new LabelModel(getStore().db).removeType(project.id, c.req.param("key") ?? "");
|
|
43
|
+
} catch {
|
|
44
|
+
return c.html(await renderSettingsPage(c, "labels", { globalError: "Cannot delete built-in label" }) ?? "", 400);
|
|
45
|
+
}
|
|
46
|
+
flash(c, "Label type deleted");
|
|
47
|
+
return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { registerLabelSettings };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=settings-labels.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-labels.mjs","names":[],"sources":["../../src/routers/settings-labels.ts"],"sourcesContent":["import { LabelModel } from \"@storyshelf/core/models\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** Label-type settings (create custom types, delete removable ones). */\nexport function registerLabelSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/labels\", async (c) =>\n c.html(await renderSettingsPage(c, \"labels\")),\n );\n app.post(\"/projects/:slug/settings/labels\", handleCreateLabelType);\n app.post(\"/projects/:slug/settings/labels/:key/delete\", handleDeleteLabelType);\n}\n\nasync function handleCreateLabelType(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const key = asString(form.get(\"key\"));\n const labelName = asString(form.get(\"labelName\")) ?? asString(form.get(\"name\"));\n const linkTemplate = asString(form.get(\"linkTemplate\"));\n if (!key || !labelName) {\n return c.html(\n (await renderSettingsPage(c, \"labels\", { globalError: \"Key and name are required\" })) ?? \"\",\n 400,\n );\n }\n return await persistLabelType(c, project.id, project.slug, key, labelName, linkTemplate);\n}\n\nasync function persistLabelType(\n c: Context,\n projectId: string,\n projectSlug: string,\n key: string,\n labelName: string,\n linkTemplate: string | undefined,\n): Promise<Response> {\n try {\n await new LabelModel(getStore().db).createType(projectId, {\n key,\n name: labelName,\n linkTemplate,\n });\n flash(c, \"Label type added\");\n return hxRedirect(c, `/projects/${projectSlug}/settings/labels`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create label\";\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: message })) ?? \"\", 400);\n }\n}\n\nasync function handleDeleteLabelType(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n try {\n await new LabelModel(getStore().db).removeType(project.id, c.req.param(\"key\") ?? \"\");\n } catch {\n return c.html(\n (await renderSettingsPage(c, \"labels\", { globalError: \"Cannot delete built-in label\" })) ??\n \"\",\n 400,\n );\n }\n flash(c, \"Label type deleted\");\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,sBAAsB,KAAwB;CAC5D,IAAI,IAAI,mCAAmC,OAAO,MAChD,EAAE,KAAK,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAC9C;CACA,IAAI,KAAK,mCAAmC,qBAAqB;CACjE,IAAI,KAAK,+CAA+C,qBAAqB;AAC/E;AAEA,eAAe,sBAAsB,GAA+B;CAClE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,MAAM,SAAS,KAAK,IAAI,KAAK,CAAC;CACpC,MAAM,YAAY,SAAS,KAAK,IAAI,WAAW,CAAC,KAAK,SAAS,KAAK,IAAI,MAAM,CAAC;CAC9E,MAAM,eAAe,SAAS,KAAK,IAAI,cAAc,CAAC;CACtD,IAAI,CAAC,OAAO,CAAC,WACX,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,UAAU,EAAE,aAAa,4BAA4B,CAAC,KAAM,IACzF,GACF;CAEF,OAAO,MAAM,iBAAiB,GAAG,QAAQ,IAAI,QAAQ,MAAM,KAAK,WAAW,YAAY;AACzF;AAEA,eAAe,iBACb,GACA,WACA,aACA,KACA,WACA,cACmB;CACnB,IAAI;EACF,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,WAAW;GACxD;GACA,MAAM;GACN;EACF,CAAC;EACD,MAAM,GAAG,kBAAkB;EAC3B,OAAO,WAAW,GAAG,aAAa,YAAY,iBAAiB;CACjE,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;EACzD,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,UAAU,EAAE,aAAa,QAAQ,CAAC,KAAM,IAAI,GAAG;CAC5F;AACF;AAEA,eAAe,sBAAsB,GAA+B;CAClE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,IAAI;EACF,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,QAAQ,IAAI,EAAE,IAAI,MAAM,KAAK,KAAK,EAAE;CACrF,QAAQ;EACN,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,UAAU,EAAE,aAAa,+BAA+B,CAAC,KACpF,IACF,GACF;CACF;CACA,MAAM,GAAG,oBAAoB;CAC7B,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,iBAAiB;AAClE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { hxRedirect } from "./htmx.mjs";
|
|
6
|
+
import { flash } from "./flash.mjs";
|
|
7
|
+
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
8
|
+
import { MemberModel, ProjectGroupMappingModel } from "@storyshelf/core/models";
|
|
9
|
+
//#region src/routers/settings-members.ts
|
|
10
|
+
/** Project member settings (assign roles, remove members). */
|
|
11
|
+
function registerMemberSettings(app) {
|
|
12
|
+
app.get("/projects/:slug/settings/members", async (c) => c.html(await renderSettingsPage(c, "members")));
|
|
13
|
+
app.post("/projects/:slug/settings/members", async (c) => {
|
|
14
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
15
|
+
const form = await c.req.formData();
|
|
16
|
+
const userId = asString(form.get("userId"));
|
|
17
|
+
const role = asString(form.get("role"));
|
|
18
|
+
if (!userId || !role) return c.html(await renderSettingsPage(c, "members", { globalError: "User and role are required" }) ?? "", 400);
|
|
19
|
+
await new MemberModel(getStore().db).set(project.id, userId, role);
|
|
20
|
+
flash(c, "Member saved");
|
|
21
|
+
return hxRedirect(c, `/projects/${project.slug}/settings/members`);
|
|
22
|
+
});
|
|
23
|
+
app.post("/projects/:slug/settings/members/:userId/remove", async (c) => {
|
|
24
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
25
|
+
await new MemberModel(getStore().db).remove(project.id, c.req.param("userId") ?? "");
|
|
26
|
+
flash(c, "Member removed");
|
|
27
|
+
return hxRedirect(c, `/projects/${project.slug}/settings/members`);
|
|
28
|
+
});
|
|
29
|
+
registerGroupMappingSettings(app);
|
|
30
|
+
}
|
|
31
|
+
/** Identity-provider group mapping settings (exact-match group to role). */
|
|
32
|
+
function registerGroupMappingSettings(app) {
|
|
33
|
+
app.post("/projects/:slug/settings/members/groups", async (c) => {
|
|
34
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
35
|
+
const form = await c.req.formData();
|
|
36
|
+
const groupName = asString(form.get("groupName"));
|
|
37
|
+
const role = asString(form.get("role"));
|
|
38
|
+
if (!groupName || !role || ![
|
|
39
|
+
"viewer",
|
|
40
|
+
"developer",
|
|
41
|
+
"approver",
|
|
42
|
+
"admin"
|
|
43
|
+
].includes(role)) return c.html(await renderSettingsPage(c, "members", { globalError: "Group and role are required" }) ?? "", 400);
|
|
44
|
+
await new ProjectGroupMappingModel(getStore().db).create(project.id, groupName, role);
|
|
45
|
+
flash(c, "Group mapping added");
|
|
46
|
+
return hxRedirect(c, `/projects/${project.slug}/settings/members`);
|
|
47
|
+
});
|
|
48
|
+
app.post("/projects/:slug/settings/members/groups/:mappingId/remove", async (c) => {
|
|
49
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
50
|
+
await new ProjectGroupMappingModel(getStore().db).remove(project.id, c.req.param("mappingId") ?? "");
|
|
51
|
+
flash(c, "Group mapping removed");
|
|
52
|
+
return hxRedirect(c, `/projects/${project.slug}/settings/members`);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { registerMemberSettings };
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=settings-members.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-members.mjs","names":[],"sources":["../../src/routers/settings-members.ts"],"sourcesContent":["import { MemberModel, ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n/** Project member settings (assign roles, remove members). */\nexport function registerMemberSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/members\", async (c) =>\n c.html(await renderSettingsPage(c, \"members\")),\n );\n\n app.post(\"/projects/:slug/settings/members\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const userId = asString(form.get(\"userId\"));\n const role = asString(form.get(\"role\"));\n if (!userId || !role) {\n return c.html(\n (await renderSettingsPage(c, \"members\", { globalError: \"User and role are required\" })) ??\n \"\",\n 400,\n );\n }\n await new MemberModel(getStore().db).set(project.id, userId, role as ProjectRole);\n flash(c, \"Member saved\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/:userId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new MemberModel(getStore().db).remove(project.id, c.req.param(\"userId\") ?? \"\");\n flash(c, \"Member removed\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n registerGroupMappingSettings(app);\n}\n\n/** Identity-provider group mapping settings (exact-match group to role). */\nfunction registerGroupMappingSettings(app: ShelfRouter): void {\n app.post(\"/projects/:slug/settings/members/groups\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n const groupName = asString(form.get(\"groupName\"));\n const role = asString(form.get(\"role\")) as ProjectRole | undefined;\n if (!groupName || !role || ![\"viewer\", \"developer\", \"approver\", \"admin\"].includes(role)) {\n return c.html(\n (await renderSettingsPage(c, \"members\", { globalError: \"Group and role are required\" })) ??\n \"\",\n 400,\n );\n }\n await new ProjectGroupMappingModel(getStore().db).create(project.id, groupName, role);\n flash(c, \"Group mapping added\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/groups/:mappingId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n await new ProjectGroupMappingModel(getStore().db).remove(\n project.id,\n c.req.param(\"mappingId\") ?? \"\",\n );\n flash(c, \"Group mapping removed\");\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n}\n"],"mappings":";;;;;;;;;;AAQA,SAAgB,uBAAuB,KAAwB;CAC7D,IAAI,IAAI,oCAAoC,OAAO,MACjD,EAAE,KAAK,MAAM,mBAAmB,GAAG,SAAS,CAAC,CAC/C;CAEA,IAAI,KAAK,oCAAoC,OAAO,MAAM;EACxD,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;EAClC,MAAM,SAAS,SAAS,KAAK,IAAI,QAAQ,CAAC;EAC1C,MAAM,OAAO,SAAS,KAAK,IAAI,MAAM,CAAC;EACtC,IAAI,CAAC,UAAU,CAAC,MACd,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,WAAW,EAAE,aAAa,6BAA6B,CAAC,KACnF,IACF,GACF;EAEF,MAAM,IAAI,YAAY,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,IAAI,QAAQ,IAAmB;EAChF,MAAM,GAAG,cAAc;EACvB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;CAED,IAAI,KAAK,mDAAmD,OAAO,MAAM;EACvE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,IAAI,YAAY,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,EAAE,IAAI,MAAM,QAAQ,KAAK,EAAE;EACnF,MAAM,GAAG,gBAAgB;EACzB,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;CAED,6BAA6B,GAAG;AAClC;;AAGA,SAAS,6BAA6B,KAAwB;CAC5D,IAAI,KAAK,2CAA2C,OAAO,MAAM;EAC/D,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;EAClC,MAAM,YAAY,SAAS,KAAK,IAAI,WAAW,CAAC;EAChD,MAAM,OAAO,SAAS,KAAK,IAAI,MAAM,CAAC;EACtC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;GAAC;GAAU;GAAa;GAAY;EAAO,CAAC,CAAC,SAAS,IAAI,GACpF,OAAO,EAAE,KACN,MAAM,mBAAmB,GAAG,WAAW,EAAE,aAAa,8BAA8B,CAAC,KACpF,IACF,GACF;EAEF,MAAM,IAAI,yBAAyB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,WAAW,IAAI;EACpF,MAAM,GAAG,qBAAqB;EAC9B,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;CAED,IAAI,KAAK,6DAA6D,OAAO,MAAM;EACjF,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;EAC3D,MAAM,IAAI,yBAAyB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAChD,QAAQ,IACR,EAAE,IAAI,MAAM,WAAW,KAAK,EAC9B;EACA,MAAM,GAAG,uBAAuB;EAChC,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;CACnE,CAAC;AACH"}
|