@storyshelf/app 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +2 -6
- package/dist/asset-manifest.mjs +14 -7
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +9 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +9 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +79 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +17 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +74 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +45 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +58 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +96 -119
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +33 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +57 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +1 -2
- package/dist/credentials.mjs +1 -2
- package/dist/git-status.mjs +45 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +23 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -5
- package/dist/index.mjs +204 -127
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +48 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +20 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +21 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +39 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +102 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +58 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +97 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +27 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +112 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +20 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +59 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +47 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +13 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +50 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +131 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +93 -22
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +5 -4
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/csrf.mjs +1 -2
- package/dist/middleware/rate-limit.mjs +42 -35
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +4 -4
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +50 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +40 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +1 -2
- package/dist/middleware/setup-gate.mjs +1 -2
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +13 -4
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +32 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +3 -4
- package/dist/notify.mjs +79 -41
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +115 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +77 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +132 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +52 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +64 -15
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +105 -78
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +88 -35
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +17 -16
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-default.mjs +53 -0
- package/dist/pages/build-diff-default.mjs.map +1 -0
- package/dist/pages/build-diff-environment.mjs +81 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +23 -6
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +112 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +74 -12
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +29 -14
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-reference.mjs +84 -0
- package/dist/pages/build-diff-reference.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +6 -3
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-stale.mjs +1 -2
- package/dist/pages/build-diff-usage.mjs +62 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +97 -14
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +37 -22
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +73 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +248 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +167 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +14 -28
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +181 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +52 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +125 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/invite.mjs +1 -2
- package/dist/pages/label-detail.mjs +1 -2
- package/dist/pages/library-grid.mjs +104 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +46 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +129 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +113 -220
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/login.mjs +1 -2
- package/dist/pages/notifications-paused.mjs +21 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +1 -2
- package/dist/pages/profile.mjs +54 -50
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +3 -4
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +52 -23
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs +1 -2
- package/dist/pages/project-settings.mjs +52 -14
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +15 -3
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +7 -4
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +115 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +102 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +101 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/root.mjs +1 -2
- package/dist/pages/settings-ai.mjs +68 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +123 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +98 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +178 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +47 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +47 -23
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +44 -4
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +111 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +1 -2
- package/dist/pages/settings-notifications-git.mjs +127 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +80 -44
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +34 -4
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/settings-tokens.mjs +1 -2
- package/dist/pages/settings-webhooks.mjs +1 -2
- package/dist/pages/storybook.mjs +1 -2
- package/dist/pages/target-choices.mjs +38 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +88 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +12 -12
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +47 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +46 -5
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +92 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/admin.mjs +8 -4
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +10 -4
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/attempts.mjs +1 -2
- package/dist/routers/auth-sync.mjs +1 -2
- package/dist/routers/auth.mjs +2 -3
- package/dist/routers/baseline-guard.mjs +4 -5
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baseline-reference.mjs +72 -0
- package/dist/routers/baseline-reference.mjs.map +1 -0
- package/dist/routers/baselines.handlers.mjs +63 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +124 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +67 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +69 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +85 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +73 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +65 -60
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +77 -19
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-cache.mjs +29 -0
- package/dist/routers/capture-cache.mjs.map +1 -0
- package/dist/routers/capture-config.mjs +34 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +42 -17
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/comparison-image.mjs +29 -0
- package/dist/routers/comparison-image.mjs.map +1 -0
- package/dist/routers/component-catalog.handlers.mjs +70 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +120 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +117 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +43 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +113 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/flash.mjs +1 -2
- package/dist/routers/health-report.mjs +3 -5
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +5 -5
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +20 -5
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs +1 -2
- package/dist/routers/insights-ui.mjs +63 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +44 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +188 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/labels.mjs +1 -2
- package/dist/routers/links.handlers.mjs +79 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +138 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +30 -7
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +3 -3
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +18 -25
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/notification-subscriptions.mjs +1 -2
- package/dist/routers/oauth-state.mjs +44 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +37 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +50 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +4 -3
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +18 -3
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +85 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +51 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-compression.mjs +109 -0
- package/dist/routers/published-compression.mjs.map +1 -0
- package/dist/routers/published-files.mjs +69 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +71 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-next.mjs +1 -2
- package/dist/routers/review-section.handlers.mjs +84 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +28 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +141 -24
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +21 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +34 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +76 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +102 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +56 -12
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +76 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +28 -3
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +62 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +5 -9
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +16 -12
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +3 -7
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +8 -26
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +108 -28
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +17 -6
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +55 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +62 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +104 -7
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +57 -104
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +3 -7
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +38 -7
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +6 -13
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +12 -6
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +92 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +37 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +21 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +103 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +19 -15
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +4 -0
- package/dist/store.mjs +20 -3
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +45 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/avatar.mjs +1 -2
- package/dist/ui/buttons.mjs +17 -5
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/code-block.mjs +1 -2
- package/dist/ui/compare-stage.mjs +86 -54
- package/dist/ui/compare-stage.mjs.map +1 -1
- package/dist/ui/csrf-field.mjs +1 -2
- package/dist/ui/css.mjs +1 -2
- package/dist/ui/design-reference.mjs +96 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +2 -4
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/dropdown.mjs +1 -2
- package/dist/ui/feedback.mjs +1 -2
- package/dist/ui/filter-input.mjs +1 -2
- package/dist/ui/forms.mjs +7 -7
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/icon.mjs +1 -2
- package/dist/ui/icons/paths.mjs +2 -2
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/icons/sprite.mjs +1 -2
- package/dist/ui/kbd.mjs +1 -2
- package/dist/ui/layout.mjs +1 -2
- package/dist/ui/preview-embed.mjs +81 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/progress.mjs +1 -2
- package/dist/ui/segmented.mjs +1 -2
- package/dist/ui/shell/account.mjs +1 -2
- package/dist/ui/shell/bare.mjs +1 -2
- package/dist/ui/shell/boot-script.mjs +1 -2
- package/dist/ui/shell/client-script.mjs +86 -2
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +13 -10
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +25 -3
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +3 -4
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/dist/ui/shell/topbar.mjs +1 -2
- package/dist/ui/stacks.mjs +1 -2
- package/dist/ui/styles/base.mjs +1 -2
- package/dist/ui/styles/behavior.mjs +1 -2
- package/dist/ui/styles/review-layout.mjs +1 -2
- package/dist/ui/styles/review.mjs +1 -2
- package/dist/ui/styles/tokens.mjs +1 -2
- package/dist/ui/styles.mjs +1 -2
- package/dist/ui/subnav.mjs +1 -2
- package/dist/ui/table.mjs +1 -2
- package/dist/ui/theme.mjs +1 -2
- package/dist/ui/thumbnail.mjs +1 -2
- package/dist/ui/time.mjs +1 -2
- package/package.json +14 -12
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-notifications-git.mjs","names":[],"sources":["../../src/pages/settings-notifications-git.tsx"],"sourcesContent":["import { isServerlessHost } from \"@storyshelf/core/config\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Button,\n Card,\n Field,\n FormActions,\n SectionTitle,\n SelectField,\n TextareaField,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport type { NotificationsFormState, SettingsNotifyProvider } from \"./settings-notifications.tsx\";\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\n/** GitHub only: show builds as check runs posted by a GitHub App, not as commit statuses. */\nconst GitHubAppFields: FC<{ errors?: Record<string, string> }> = ({ errors }) => (\n <>\n <SelectField\n label=\"GitHub mode\"\n name=\"mode\"\n options={[\n { value: \"statuses\", label: \"Commit statuses (token)\" },\n { value: \"checks\", label: \"Check runs (GitHub App)\" },\n ]}\n hint={\n errors?.[\"mode\"] ??\n \"GitHub only. Check runs list each changed snapshot and need a GitHub App installed on the repository; GitLab always uses statuses.\"\n }\n />\n <Field label=\"App ID\" name=\"appId\" placeholder=\"123456\" error={errors?.[\"appId\"]} />\n <Field\n label=\"Installation ID\"\n name=\"installationId\"\n placeholder=\"7654321\"\n error={errors?.[\"installationId\"]}\n />\n <TextareaField\n label=\"App private key\"\n name=\"privateKey\"\n rows={4}\n placeholder=\"-----BEGIN RSA PRIVATE KEY-----\"\n hint=\"The PEM key generated for the App. It is stored encrypted and never shown again.\"\n error={errors?.[\"privateKey\"]}\n />\n </>\n);\n\n/** The repository, prefix and token fields of the git channel form. */\nconst GitChannelFields: FC<{ errors?: Record<string, string> }> = ({ errors }) => (\n <>\n <Field\n label=\"Owner\"\n name=\"owner\"\n required\n placeholder=\"my-org\"\n hint=\"Organization, user or group that owns the repository.\"\n error={errors?.[\"owner\"]}\n />\n <Field label=\"Repository\" name=\"repo\" required placeholder=\"my-repo\" error={errors?.[\"repo\"]} />\n <Field\n label=\"Host\"\n name=\"host\"\n placeholder=\"https://gitlab.example.com\"\n hint=\"GitLab only: a self-managed host. Leave blank for gitlab.com.\"\n error={errors?.[\"host\"]}\n />\n <Field\n label=\"Status prefix\"\n name=\"prefix\"\n placeholder=\"storyshelf\"\n hint=\"Statuses (or check runs) are named <prefix>/testing and <prefix>/storybook; require <prefix>/testing in branch protection (`storyshelf protect` prints the rule). Give each project in a shared repository its own prefix.\"\n error={errors?.[\"prefix\"]}\n />\n <Field\n label=\"Token\"\n name=\"token\"\n type=\"password\"\n placeholder=\"ghp_…\"\n hint=\"A token that can write commit statuses (GitHub: repo:status; GitLab: api). Not needed for GitHub checks.\"\n error={errors?.[\"token\"]}\n />\n <GitHubAppFields errors={errors} />\n </>\n);\n\n/** Git channel creation card (admins only): repository, status prefix and token. */\nexport const CreateGitChannelCard: FC<{\n project: Project;\n providers: SettingsNotifyProvider[];\n formState?: NotificationsFormState;\n}> = ({ project, providers, formState }) => (\n <Card>\n <SectionTitle level={3}>Add a git channel</SectionTitle>\n {isServerlessHost() ? (\n <Alert tone=\"info\" title=\"Statuses are delivered inline\">\n This host has no job consumer, so a build's state is written in the request that changes it,\n without retries. A failure shows on the channel and the next change writes the current state\n again.\n </Alert>\n ) : null}\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications`}\n hx-post={`/projects/${project.slug}/settings/notifications`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Git provider\"\n name=\"gitProvider\"\n options={providers.map((provider) => ({ value: provider.kind, label: provider.name }))}\n hint={formState?.errors?.[\"gitProvider\"]}\n />\n <GitChannelFields errors={formState?.errors} />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add git channel\n </Button>\n </FormActions>\n </form>\n </Card>\n);\n"],"mappings":";;;;;;;;;;;;AAmBA,MAAM,mBAA4D,EAAE,aAClE,qBAAA,UAAA,EAAA,UAAA;CACE,oBAAC,aAAD;EACE,OAAM;EACN,MAAK;EACL,SAAS,CACP;GAAE,OAAO;GAAY,OAAO;EAA0B,GACtD;GAAE,OAAO;GAAU,OAAO;EAA0B,CACtD;EACA,MACE,SAAS,WACT;CAEH,CAAA;CACD,oBAAC,OAAD;EAAO,OAAM;EAAS,MAAK;EAAQ,aAAY;EAAS,OAAO,SAAS;CAAW,CAAA;CACnF,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,aAAY;EACZ,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,eAAD;EACE,OAAM;EACN,MAAK;EACL,MAAM;EACN,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;AACD,EAAA,CAAA;;AAIJ,MAAM,oBAA6D,EAAE,aACnE,qBAAA,UAAA,EAAA,UAAA;CACE,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,UAAA;EACA,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,OAAD;EAAO,OAAM;EAAa,MAAK;EAAO,UAAA;EAAS,aAAY;EAAU,OAAO,SAAS;CAAU,CAAA;CAC/F,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,OAAD;EACE,OAAM;EACN,MAAK;EACL,MAAK;EACL,aAAY;EACZ,MAAK;EACL,OAAO,SAAS;CACjB,CAAA;CACD,oBAAC,iBAAD,EAAyB,OAAS,CAAA;AAClC,EAAA,CAAA;;AAIJ,MAAa,wBAIP,EAAE,SAAS,WAAW,gBAC1B,qBAAC,MAAD,EAAA,UAAA;CACE,oBAAC,cAAD;EAAc,OAAO;EAAG,UAAA;CAA+B,CAAA;CACtD,iBAAiB,IAChB,oBAAC,OAAD;EAAO,MAAK;EAAO,OAAM;EAAgC,UAAA;CAIlD,CAAA,IACL;CACJ,qBAAC,QAAD;EACE,QAAO;EACP,QAAQ,aAAa,QAAQ,KAAK;EAClC,WAAS,aAAa,QAAQ,KAAK;EACnC,aAAU;EAJZ,UAAA;GAMG,UAAU;GACX,oBAAC,aAAD;IACE,OAAM;IACN,MAAK;IACL,SAAS,UAAU,KAAK,cAAc;KAAE,OAAO,SAAS;KAAM,OAAO,SAAS;IAAK,EAAE;IACrF,MAAM,WAAW,SAAS;GAC3B,CAAA;GACD,oBAAC,kBAAD,EAAkB,QAAQ,WAAW,OAAS,CAAA;GAC9C,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;IAAQ,SAAQ;IAAU,MAAK;IAAS,MAAK;IAAO,UAAA;GAE5C,CAAA,EACG,CAAA;EACT;;AACF,EAAA,CAAA"}
|
|
@@ -5,11 +5,14 @@ import { Button } from "../ui/buttons.mjs";
|
|
|
5
5
|
import { Alert, Badge, Meta } from "../ui/feedback.mjs";
|
|
6
6
|
import { CheckField, Field, FormActions, SelectField } from "../ui/forms.mjs";
|
|
7
7
|
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { VStack } from "../ui/stacks.mjs";
|
|
9
9
|
import { Table } from "../ui/table.mjs";
|
|
10
|
+
import { RelativeTime } from "../ui/time.mjs";
|
|
10
11
|
import { csrfField } from "../ui/csrf-field.mjs";
|
|
12
|
+
import { NotificationsPausedNotice } from "./notifications-paused.mjs";
|
|
13
|
+
import { CreateGitChannelCard } from "./settings-notifications-git.mjs";
|
|
11
14
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
12
|
-
|
|
15
|
+
// src/pages/settings-notifications.tsx
|
|
13
16
|
/** Notification topic catalog (mirrors the webhook event names). */
|
|
14
17
|
const NOTIFICATION_TOPICS = [
|
|
15
18
|
{
|
|
@@ -28,6 +31,18 @@ const NOTIFICATION_TOPICS = [
|
|
|
28
31
|
event: "build:rejected",
|
|
29
32
|
label: "Build rejected"
|
|
30
33
|
},
|
|
34
|
+
{
|
|
35
|
+
event: "build:pending",
|
|
36
|
+
label: "Build sent back to pending"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
event: "build:failed",
|
|
40
|
+
label: "Build failed"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
event: "build:cancelled",
|
|
44
|
+
label: "Build cancelled"
|
|
45
|
+
},
|
|
31
46
|
{
|
|
32
47
|
event: "baseline:created",
|
|
33
48
|
label: "Baseline created"
|
|
@@ -83,14 +98,43 @@ const MyNotificationsCard = ({ project, mySubscription }) => {
|
|
|
83
98
|
})
|
|
84
99
|
] });
|
|
85
100
|
};
|
|
86
|
-
/**
|
|
101
|
+
/** A config value as text; anything but a string reads as empty. */
|
|
102
|
+
function textOf(value) {
|
|
103
|
+
return typeof value === "string" ? value : "";
|
|
104
|
+
}
|
|
105
|
+
/** Human-readable channel target (git repository, email address or masked webhook host). */
|
|
87
106
|
function targetOf(channel) {
|
|
107
|
+
if (channel.delivery === "state") {
|
|
108
|
+
const where = `${textOf(channel.config["owner"])}/${textOf(channel.config["repo"])}`;
|
|
109
|
+
const prefix = textOf(channel.config["prefix"]);
|
|
110
|
+
return prefix ? `${where} (${prefix})` : where;
|
|
111
|
+
}
|
|
88
112
|
const to = channel.config["to"];
|
|
89
113
|
if (typeof to === "string" && to) return to;
|
|
90
114
|
return channel.hasSecret ? `${channel.provider} webhook` : "—";
|
|
91
115
|
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
116
|
+
/** What a channel reacts to: its topic filter, or the build state for git channels. */
|
|
117
|
+
function reactsTo(channel) {
|
|
118
|
+
if (channel.delivery === "state") return "build state";
|
|
119
|
+
return channel.events.length === 0 ? "all events" : channel.events.join(", ");
|
|
120
|
+
}
|
|
121
|
+
/** Delivery health: failing with the last error and time, or ok. */
|
|
122
|
+
const ChannelHealth = ({ channel }) => channel.lastError === null ? /* @__PURE__ */ jsx(Badge, {
|
|
123
|
+
tone: "success",
|
|
124
|
+
children: "ok"
|
|
125
|
+
}) : /* @__PURE__ */ jsxs(VStack, { children: [/* @__PURE__ */ jsx(Badge, {
|
|
126
|
+
tone: "danger",
|
|
127
|
+
children: "failing"
|
|
128
|
+
}), /* @__PURE__ */ jsxs(Meta, {
|
|
129
|
+
as: "span",
|
|
130
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
131
|
+
class: "truncate max-w-cell",
|
|
132
|
+
title: channel.lastError,
|
|
133
|
+
children: channel.lastError
|
|
134
|
+
}), channel.lastErrorAt ? /* @__PURE__ */ jsx(RelativeTime, { value: channel.lastErrorAt }) : null]
|
|
135
|
+
})] });
|
|
136
|
+
/** One channel table row; the delete action renders for admins only. */
|
|
137
|
+
const ChannelRow = ({ project, channel, isAdmin }) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
94
138
|
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Badge, {
|
|
95
139
|
tone: "neutral",
|
|
96
140
|
children: channel.provider
|
|
@@ -100,64 +144,49 @@ const ChannelRow = ({ project, channel }) => /* @__PURE__ */ jsxs("tr", { childr
|
|
|
100
144
|
title: targetOf(channel),
|
|
101
145
|
children: targetOf(channel)
|
|
102
146
|
}),
|
|
103
|
-
/* @__PURE__ */ jsx("td", { children:
|
|
147
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
|
|
104
148
|
as: "span",
|
|
105
|
-
children:
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
})) }) }),
|
|
110
|
-
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("form", {
|
|
149
|
+
children: reactsTo(channel)
|
|
150
|
+
}) }),
|
|
151
|
+
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(ChannelHealth, { channel }) }),
|
|
152
|
+
/* @__PURE__ */ jsx("td", { children: isAdmin ? /* @__PURE__ */ jsxs("form", {
|
|
111
153
|
method: "post",
|
|
112
154
|
action: `/projects/${project.slug}/settings/notifications/${channel.id}/delete`,
|
|
113
155
|
"hx-post": `/projects/${project.slug}/settings/notifications/${channel.id}/delete`,
|
|
114
156
|
"hx-target": "body",
|
|
115
|
-
"hx-confirm": "Remove this
|
|
157
|
+
"hx-confirm": "Remove this channel?",
|
|
116
158
|
children: [csrfField(), /* @__PURE__ */ jsx(Button, {
|
|
117
159
|
variant: "ghost",
|
|
118
160
|
size: "sm",
|
|
119
161
|
type: "submit",
|
|
120
162
|
children: "Delete"
|
|
121
163
|
})]
|
|
122
|
-
}) })
|
|
123
|
-
] });
|
|
124
|
-
/** Channel list card
|
|
164
|
+
}) : null })
|
|
165
|
+
] }, channel.id);
|
|
166
|
+
/** Channel list card: chat, email and git channels side by side, each with its delivery health. */
|
|
125
167
|
const ChannelsCard = ({ project, channels, isAdmin }) => /* @__PURE__ */ jsxs(Card, { children: [
|
|
126
168
|
/* @__PURE__ */ jsx(SectionTitle, { children: "Channels" }),
|
|
127
|
-
/* @__PURE__ */ jsx(Meta, { children: "Project admins route build events to Slack, Teams, or
|
|
169
|
+
/* @__PURE__ */ jsx(Meta, { children: "Project admins route build events to Slack, Teams or email, and show each build's state on GitHub or GitLab as a commit status. Secrets and tokens are stored encrypted and decrypted only in memory at delivery time. Git channels follow the build state, not a topic list, and their delivery is best-effort: a failure shows here, and the next build state retries it." }),
|
|
128
170
|
/* @__PURE__ */ jsx(Table, { children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
129
171
|
/* @__PURE__ */ jsx("th", { children: "Provider" }),
|
|
130
172
|
/* @__PURE__ */ jsx("th", { children: "Target" }),
|
|
131
|
-
/* @__PURE__ */ jsx("th", { children: "
|
|
173
|
+
/* @__PURE__ */ jsx("th", { children: "Reacts to" }),
|
|
174
|
+
/* @__PURE__ */ jsx("th", { children: "Delivery" }),
|
|
132
175
|
/* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx("span", {
|
|
133
176
|
class: "visually-hidden",
|
|
134
177
|
children: "Actions"
|
|
135
178
|
}) })
|
|
136
|
-
] }) }), /* @__PURE__ */ jsx("tbody", { children: channels.map((channel) =>
|
|
179
|
+
] }) }), /* @__PURE__ */ jsx("tbody", { children: channels.map((channel) => /* @__PURE__ */ jsx(ChannelRow, {
|
|
137
180
|
project,
|
|
138
|
-
channel
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
tone: "neutral",
|
|
142
|
-
children: channel.provider
|
|
143
|
-
}) }),
|
|
144
|
-
/* @__PURE__ */ jsx("td", {
|
|
145
|
-
class: "truncate max-w-cell",
|
|
146
|
-
title: targetOf(channel),
|
|
147
|
-
children: targetOf(channel)
|
|
148
|
-
}),
|
|
149
|
-
/* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(Meta, {
|
|
150
|
-
as: "span",
|
|
151
|
-
children: channel.events.length === 0 ? "all events" : channel.events.join(", ")
|
|
152
|
-
}) }),
|
|
153
|
-
/* @__PURE__ */ jsx("td", { children: null })
|
|
154
|
-
] }, channel.id)) })] }) }),
|
|
181
|
+
channel,
|
|
182
|
+
isAdmin
|
|
183
|
+
})) })] }) }),
|
|
155
184
|
channels.length === 0 ? /* @__PURE__ */ jsx(Meta, { children: "No channels configured." }) : null
|
|
156
185
|
] });
|
|
157
186
|
/** Channel creation card (admins only). */
|
|
158
187
|
const CreateChannelCard = ({ project, providers, formState }) => /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(SectionTitle, {
|
|
159
188
|
level: 3,
|
|
160
|
-
children: "
|
|
189
|
+
children: "Add a message channel"
|
|
161
190
|
}), /* @__PURE__ */ jsxs("form", {
|
|
162
191
|
method: "post",
|
|
163
192
|
action: `/projects/${project.slug}/settings/notifications`,
|
|
@@ -169,8 +198,8 @@ const CreateChannelCard = ({ project, providers, formState }) => /* @__PURE__ */
|
|
|
169
198
|
label: "Provider",
|
|
170
199
|
name: "provider",
|
|
171
200
|
options: providers.map((provider) => ({
|
|
172
|
-
value: provider,
|
|
173
|
-
label: provider
|
|
201
|
+
value: provider.kind,
|
|
202
|
+
label: provider.name
|
|
174
203
|
})),
|
|
175
204
|
hint: formState?.errors?.["provider"]
|
|
176
205
|
}),
|
|
@@ -208,11 +237,14 @@ const CreateChannelCard = ({ project, providers, formState }) => /* @__PURE__ */
|
|
|
208
237
|
}) })
|
|
209
238
|
]
|
|
210
239
|
})] });
|
|
211
|
-
/** Notifications settings tab: channel list, create
|
|
212
|
-
function renderSettingsNotifications(project, channels, providers, mySubscription, isAdmin, formState) {
|
|
240
|
+
/** Notifications settings tab: channel list, create forms, and my preferences. */
|
|
241
|
+
function renderSettingsNotifications(project, channels, providers, mySubscription, isAdmin, formState, paused) {
|
|
242
|
+
const messageProviders = providers.filter((provider) => provider.delivery === "message");
|
|
243
|
+
const gitProviders = providers.filter((provider) => provider.delivery === "state");
|
|
213
244
|
return /* @__PURE__ */ jsxs("div", {
|
|
214
245
|
class: "grid max-w-form",
|
|
215
246
|
children: [
|
|
247
|
+
/* @__PURE__ */ jsx(NotificationsPausedNotice, { paused }),
|
|
216
248
|
formState?.globalError ? /* @__PURE__ */ jsx(Alert, {
|
|
217
249
|
tone: "danger",
|
|
218
250
|
children: formState.globalError
|
|
@@ -226,15 +258,19 @@ function renderSettingsNotifications(project, channels, providers, mySubscriptio
|
|
|
226
258
|
channels,
|
|
227
259
|
isAdmin
|
|
228
260
|
}),
|
|
229
|
-
isAdmin ? /* @__PURE__ */ jsx(CreateChannelCard, {
|
|
261
|
+
isAdmin && messageProviders.length > 0 ? /* @__PURE__ */ jsx(CreateChannelCard, {
|
|
262
|
+
project,
|
|
263
|
+
providers: messageProviders,
|
|
264
|
+
formState
|
|
265
|
+
}) : null,
|
|
266
|
+
isAdmin && gitProviders.length > 0 ? /* @__PURE__ */ jsx(CreateGitChannelCard, {
|
|
230
267
|
project,
|
|
231
|
-
providers,
|
|
268
|
+
providers: gitProviders,
|
|
232
269
|
formState
|
|
233
270
|
}) : null
|
|
234
271
|
]
|
|
235
272
|
});
|
|
236
273
|
}
|
|
237
|
-
//#endregion
|
|
238
274
|
export { NOTIFICATION_TOPICS, renderSettingsNotifications };
|
|
239
275
|
|
|
240
276
|
//# sourceMappingURL=settings-notifications.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/pages/settings-notifications.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n HStack,\n Meta,\n SectionTitle,\n SelectField,\n Table,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Notification topic catalog (mirrors the webhook event names). */\nexport const NOTIFICATION_TOPICS: { event: string; label: string }[] = [\n { event: \"build:created\", label: \"Build created\" },\n { event: \"build:reviewing\", label: \"Build needs review\" },\n { event: \"build:approved\", label: \"Build approved\" },\n { event: \"build:rejected\", label: \"Build rejected\" },\n { event: \"baseline:created\", label: \"Baseline created\" },\n { event: \"baseline:updated\", label: \"Baseline updated\" },\n { event: \"comment:created\", label: \"Comment posted\" },\n];\n\n/** Channel row as rendered in the notifications settings tab. */\nexport interface SettingsNotificationChannel {\n id: string;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n}\n\n/** The viewer's own subscription for this project (null when opted out). */\nexport interface SettingsMySubscription {\n events: string[];\n via: string[];\n enabled: boolean;\n}\n\n/** Form state for the notifications tab (field errors, global error). */\nexport interface NotificationsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\n/** Whether a topic checkbox renders checked (empty allowlist means all). */\nfunction topicChecked(events: string[], event: string): boolean {\n return events.length === 0 || events.includes(event);\n}\n\n/** The viewer's own preferences card (visible to every member). */\nconst MyNotificationsCard: FC<{\n project: Project;\n mySubscription: SettingsMySubscription | null;\n}> = ({ project, mySubscription }) => {\n const events = mySubscription?.events ?? [];\n return (\n <Card>\n <SectionTitle>My notifications for this project</SectionTitle>\n <Meta>Opt in to email alerts for this project. Leave everything off to stay silent.</Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/me`}\n hx-post={`/projects/${project.slug}/settings/notifications/me`}\n hx-target=\"body\"\n >\n {csrfField()}\n <CheckField\n label=\"Email me about this project\"\n name=\"enabled\"\n checked={mySubscription?.enabled ?? false}\n />\n {NOTIFICATION_TOPICS.map((topic) => (\n <CheckField\n label={topic.label}\n name=\"events\"\n value={topic.event}\n checked={topicChecked(events, topic.event)}\n />\n ))}\n <p>\n <Meta as=\"span\">No boxes ticked means all topics.</Meta>\n </p>\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save my preferences\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n};\n\n/** Human-readable channel target (email address or masked webhook host). */\nfunction targetOf(channel: SettingsNotificationChannel): string {\n const to = channel.config[\"to\"];\n if (typeof to === \"string\" && to) {\n return to;\n }\n return channel.hasSecret ? `${channel.provider} webhook` : \"—\";\n}\n\n/** One channel table row with the admin delete action. */\nconst ChannelRow: FC<{ project: Project; channel: SettingsNotificationChannel }> = ({\n project,\n channel,\n}) => (\n <tr>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n {channel.events.length === 0 ? (\n <Meta as=\"span\">all events</Meta>\n ) : (\n <HStack>\n {channel.events.map((event) => (\n <Badge tone=\"neutral\">{event}</Badge>\n ))}\n </HStack>\n )}\n </td>\n <td>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-post={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this notification channel?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Delete\n </Button>\n </form>\n </td>\n </tr>\n);\n\n/** Channel list card (delete action renders for admins only). */\nconst ChannelsCard: FC<{\n project: Project;\n channels: SettingsNotificationChannel[];\n isAdmin: boolean;\n}> = ({ project, channels, isAdmin }) => (\n <Card>\n <SectionTitle>Channels</SectionTitle>\n <Meta>\n Project admins route build events to Slack, Teams, or email. Secrets are stored encrypted and\n decrypted only in memory at send time.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Target</th>\n <th>Events</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {channels.map((channel) =>\n isAdmin ? (\n <ChannelRow project={project} channel={channel} />\n ) : (\n <tr key={channel.id}>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n <Meta as=\"span\">\n {channel.events.length === 0 ? \"all events\" : channel.events.join(\", \")}\n </Meta>\n </td>\n <td>{null}</td>\n </tr>\n ),\n )}\n </tbody>\n </table>\n </Table>\n {channels.length === 0 ? <Meta>No channels configured.</Meta> : null}\n </Card>\n);\n\n/** Channel creation card (admins only). */\nconst CreateChannelCard: FC<{\n project: Project;\n providers: string[];\n formState?: NotificationsFormState;\n}> = ({ project, providers, formState }) => (\n <Card>\n <SectionTitle level={3}>Create channel</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications`}\n hx-post={`/projects/${project.slug}/settings/notifications`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Provider\"\n name=\"provider\"\n options={providers.map((provider) => ({ value: provider, label: provider }))}\n hint={formState?.errors?.[\"provider\"]}\n />\n <Field\n label=\"Target\"\n name=\"target\"\n type=\"text\"\n required\n placeholder=\"team@example.com or https://hooks.slack.com/…\"\n hint=\"Email address for email channels, webhook URL otherwise.\"\n error={formState?.errors?.[\"target\"]}\n />\n <Field\n label=\"Events\"\n name=\"events\"\n placeholder=\"build.reviewing, build.approved\"\n hint=\"Comma-separated. Leave blank to receive all events.\"\n />\n <SelectField\n label=\"Style\"\n name=\"style\"\n options={[\n { value: \"compact\", label: \"Compact\" },\n { value: \"verbose\", label: \"Verbose\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add channel\n </Button>\n </FormActions>\n </form>\n </Card>\n);\n\n/** Notifications settings tab: channel list, create form, and my preferences. */\nexport function renderSettingsNotifications(\n project: Project,\n channels: SettingsNotificationChannel[],\n providers: string[],\n mySubscription: SettingsMySubscription | null,\n isAdmin: boolean,\n formState?: NotificationsFormState,\n): unknown {\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <MyNotificationsCard project={project} mySubscription={mySubscription} />\n <ChannelsCard project={project} channels={channels} isAdmin={isAdmin} />\n {isAdmin ? (\n <CreateChannelCard project={project} providers={providers} formState={formState} />\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"settings-notifications.mjs","names":[],"sources":["../../src/pages/settings-notifications.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport type { FC } from \"hono/jsx\";\nimport {\n Alert,\n Badge,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n Meta,\n RelativeTime,\n SectionTitle,\n SelectField,\n Table,\n VStack,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\nimport { NotificationsPausedNotice } from \"./notifications-paused.tsx\";\nimport { CreateGitChannelCard } from \"./settings-notifications-git.tsx\";\n\n/** Notification topic catalog (mirrors the webhook event names). */\nexport const NOTIFICATION_TOPICS: { event: string; label: string }[] = [\n { event: \"build:created\", label: \"Build created\" },\n { event: \"build:reviewing\", label: \"Build needs review\" },\n { event: \"build:approved\", label: \"Build approved\" },\n { event: \"build:rejected\", label: \"Build rejected\" },\n { event: \"build:pending\", label: \"Build sent back to pending\" },\n { event: \"build:failed\", label: \"Build failed\" },\n { event: \"build:cancelled\", label: \"Build cancelled\" },\n { event: \"baseline:created\", label: \"Baseline created\" },\n { event: \"baseline:updated\", label: \"Baseline updated\" },\n { event: \"comment:created\", label: \"Comment posted\" },\n];\n\n/** A wired provider as offered by the create forms. */\nexport interface SettingsNotifyProvider {\n kind: string;\n name: string;\n /** `message` providers send a message once; `state` providers (git hosts) show a build's state. */\n delivery: \"message\" | \"state\";\n}\n\n/** Channel row as rendered in the notifications settings tab. */\nexport interface SettingsNotificationChannel {\n id: string;\n provider: string;\n delivery: \"message\" | \"state\";\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n /** Last delivery error, null while the channel delivers cleanly. */\n lastError: string | null;\n lastErrorAt: string | null;\n}\n\n/** The viewer's own subscription for this project (null when opted out). */\nexport interface SettingsMySubscription {\n events: string[];\n via: string[];\n enabled: boolean;\n}\n\n/** Form state for the notifications tab (field errors, global error). */\nexport interface NotificationsFormState {\n errors?: Record<string, string>;\n globalError?: string;\n}\n\n/* eslint-disable promise-function-async -- JSX components return HtmlEscapedString */\n\n/** Whether a topic checkbox renders checked (empty allowlist means all). */\nfunction topicChecked(events: string[], event: string): boolean {\n return events.length === 0 || events.includes(event);\n}\n\n/** The viewer's own preferences card (visible to every member). */\nconst MyNotificationsCard: FC<{\n project: Project;\n mySubscription: SettingsMySubscription | null;\n}> = ({ project, mySubscription }) => {\n const events = mySubscription?.events ?? [];\n return (\n <Card>\n <SectionTitle>My notifications for this project</SectionTitle>\n <Meta>Opt in to email alerts for this project. Leave everything off to stay silent.</Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/me`}\n hx-post={`/projects/${project.slug}/settings/notifications/me`}\n hx-target=\"body\"\n >\n {csrfField()}\n <CheckField\n label=\"Email me about this project\"\n name=\"enabled\"\n checked={mySubscription?.enabled ?? false}\n />\n {NOTIFICATION_TOPICS.map((topic) => (\n <CheckField\n label={topic.label}\n name=\"events\"\n value={topic.event}\n checked={topicChecked(events, topic.event)}\n />\n ))}\n <p>\n <Meta as=\"span\">No boxes ticked means all topics.</Meta>\n </p>\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save my preferences\n </Button>\n </FormActions>\n </form>\n </Card>\n );\n};\n\n/** A config value as text; anything but a string reads as empty. */\nfunction textOf(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\n/** Human-readable channel target (git repository, email address or masked webhook host). */\nfunction targetOf(channel: SettingsNotificationChannel): string {\n if (channel.delivery === \"state\") {\n const where = `${textOf(channel.config[\"owner\"])}/${textOf(channel.config[\"repo\"])}`;\n const prefix = textOf(channel.config[\"prefix\"]);\n return prefix ? `${where} (${prefix})` : where;\n }\n const to = channel.config[\"to\"];\n if (typeof to === \"string\" && to) {\n return to;\n }\n return channel.hasSecret ? `${channel.provider} webhook` : \"—\";\n}\n\n/** What a channel reacts to: its topic filter, or the build state for git channels. */\nfunction reactsTo(channel: SettingsNotificationChannel): string {\n if (channel.delivery === \"state\") {\n return \"build state\";\n }\n return channel.events.length === 0 ? \"all events\" : channel.events.join(\", \");\n}\n\n/** Delivery health: failing with the last error and time, or ok. */\nconst ChannelHealth: FC<{ channel: SettingsNotificationChannel }> = ({ channel }) =>\n channel.lastError === null ? (\n <Badge tone=\"success\">ok</Badge>\n ) : (\n <VStack>\n <Badge tone=\"danger\">failing</Badge>\n <Meta as=\"span\">\n <span class=\"truncate max-w-cell\" title={channel.lastError}>\n {channel.lastError}\n </span>\n {channel.lastErrorAt ? <RelativeTime value={channel.lastErrorAt} /> : null}\n </Meta>\n </VStack>\n );\n\n/** One channel table row; the delete action renders for admins only. */\nconst ChannelRow: FC<{\n project: Project;\n channel: SettingsNotificationChannel;\n isAdmin: boolean;\n}> = ({ project, channel, isAdmin }) => (\n <tr key={channel.id}>\n <td>\n <Badge tone=\"neutral\">{channel.provider}</Badge>\n </td>\n <td class=\"truncate max-w-cell\" title={targetOf(channel)}>\n {targetOf(channel)}\n </td>\n <td>\n <Meta as=\"span\">{reactsTo(channel)}</Meta>\n </td>\n <td>\n <ChannelHealth channel={channel} />\n </td>\n <td>\n {isAdmin ? (\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-post={`/projects/${project.slug}/settings/notifications/${channel.id}/delete`}\n hx-target=\"body\"\n hx-confirm=\"Remove this channel?\"\n >\n {csrfField()}\n <Button variant=\"ghost\" size=\"sm\" type=\"submit\">\n Delete\n </Button>\n </form>\n ) : null}\n </td>\n </tr>\n);\n\n/** Channel list card: chat, email and git channels side by side, each with its delivery health. */\nconst ChannelsCard: FC<{\n project: Project;\n channels: SettingsNotificationChannel[];\n isAdmin: boolean;\n}> = ({ project, channels, isAdmin }) => (\n <Card>\n <SectionTitle>Channels</SectionTitle>\n <Meta>\n Project admins route build events to Slack, Teams or email, and show each build's state on\n GitHub or GitLab as a commit status. Secrets and tokens are stored encrypted and decrypted\n only in memory at delivery time. Git channels follow the build state, not a topic list, and\n their delivery is best-effort: a failure shows here, and the next build state retries it.\n </Meta>\n <Table>\n <table>\n <thead>\n <tr>\n <th>Provider</th>\n <th>Target</th>\n <th>Reacts to</th>\n <th>Delivery</th>\n <th>\n <span class=\"visually-hidden\">Actions</span>\n </th>\n </tr>\n </thead>\n <tbody>\n {channels.map((channel) => (\n <ChannelRow project={project} channel={channel} isAdmin={isAdmin} />\n ))}\n </tbody>\n </table>\n </Table>\n {channels.length === 0 ? <Meta>No channels configured.</Meta> : null}\n </Card>\n);\n\n/** Channel creation card (admins only). */\nconst CreateChannelCard: FC<{\n project: Project;\n providers: SettingsNotifyProvider[];\n formState?: NotificationsFormState;\n}> = ({ project, providers, formState }) => (\n <Card>\n <SectionTitle level={3}>Add a message channel</SectionTitle>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/notifications`}\n hx-post={`/projects/${project.slug}/settings/notifications`}\n hx-target=\"body\"\n >\n {csrfField()}\n <SelectField\n label=\"Provider\"\n name=\"provider\"\n options={providers.map((provider) => ({ value: provider.kind, label: provider.name }))}\n hint={formState?.errors?.[\"provider\"]}\n />\n <Field\n label=\"Target\"\n name=\"target\"\n type=\"text\"\n required\n placeholder=\"team@example.com or https://hooks.slack.com/…\"\n hint=\"Email address for email channels, webhook URL otherwise.\"\n error={formState?.errors?.[\"target\"]}\n />\n <Field\n label=\"Events\"\n name=\"events\"\n placeholder=\"build.reviewing, build.approved\"\n hint=\"Comma-separated. Leave blank to receive all events.\"\n />\n <SelectField\n label=\"Style\"\n name=\"style\"\n options={[\n { value: \"compact\", label: \"Compact\" },\n { value: \"verbose\", label: \"Verbose\" },\n ]}\n />\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"plus\">\n Add channel\n </Button>\n </FormActions>\n </form>\n </Card>\n);\n\n/** Notifications settings tab: channel list, create forms, and my preferences. */\nexport function renderSettingsNotifications(\n project: Project,\n channels: SettingsNotificationChannel[],\n providers: SettingsNotifyProvider[],\n mySubscription: SettingsMySubscription | null,\n isAdmin: boolean,\n formState?: NotificationsFormState,\n paused?: boolean,\n): unknown {\n const messageProviders = providers.filter((provider) => provider.delivery === \"message\");\n const gitProviders = providers.filter((provider) => provider.delivery === \"state\");\n return (\n <div class=\"grid max-w-form\">\n <NotificationsPausedNotice paused={paused} />\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <MyNotificationsCard project={project} mySubscription={mySubscription} />\n <ChannelsCard project={project} channels={channels} isAdmin={isAdmin} />\n {isAdmin && messageProviders.length > 0 ? (\n <CreateChannelCard project={project} providers={messageProviders} formState={formState} />\n ) : null}\n {isAdmin && gitProviders.length > 0 ? (\n <CreateGitChannelCard project={project} providers={gitProviders} formState={formState} />\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -7,8 +7,8 @@ import { CheckField, Field, FormActions } from "../ui/forms.mjs";
|
|
|
7
7
|
import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
8
8
|
import { csrfField } from "../ui/csrf-field.mjs";
|
|
9
9
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
10
|
-
|
|
11
|
-
/**
|
|
10
|
+
// src/pages/settings-tests.tsx
|
|
11
|
+
/** Tests settings tab: play execution, Storybook 10 test runs, accessibility checks, play timeout. */
|
|
12
12
|
function renderSettingsTests(project, isAdmin, formState) {
|
|
13
13
|
return /* @__PURE__ */ jsxs("div", {
|
|
14
14
|
class: "grid max-w-form",
|
|
@@ -18,7 +18,7 @@ function renderSettingsTests(project, isAdmin, formState) {
|
|
|
18
18
|
children: formState.globalError
|
|
19
19
|
}) : null,
|
|
20
20
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
21
|
-
/* @__PURE__ */ jsx(SectionTitle, { children: "Interaction tests" }),
|
|
21
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: "Interaction and accessibility tests" }),
|
|
22
22
|
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
23
23
|
"When enabled, Storybook ",
|
|
24
24
|
/* @__PURE__ */ jsx("code", { children: "play" }),
|
|
@@ -37,6 +37,21 @@ function renderSettingsTests(project, isAdmin, formState) {
|
|
|
37
37
|
/* @__PURE__ */ jsx("code", { children: "disableSnapshot" }),
|
|
38
38
|
" to skip a story entirely."
|
|
39
39
|
] }),
|
|
40
|
+
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
41
|
+
"Accessibility checks run axe on every captured story. Violations appear with the snapshot in review and never block a build; a story opts out with",
|
|
42
|
+
" ",
|
|
43
|
+
/* @__PURE__ */ jsx("code", { children: "parameters: { a11y: { disable: true } }" }),
|
|
44
|
+
"."
|
|
45
|
+
] }),
|
|
46
|
+
/* @__PURE__ */ jsxs(Meta, { children: [
|
|
47
|
+
"Storybook 10 ",
|
|
48
|
+
/* @__PURE__ */ jsx("code", { children: ".test()" }),
|
|
49
|
+
" entries (",
|
|
50
|
+
/* @__PURE__ */ jsx("code", { children: "story.test(\"name\", async ...)" }),
|
|
51
|
+
") can run as interaction tests too. They take no screenshot: each runs in the Storybook iframe and is reported as passed or failed on the build. A failing test fails the build like a failing ",
|
|
52
|
+
/* @__PURE__ */ jsx("code", { children: "play" }),
|
|
53
|
+
" function. Storybook 8 and 9 builds have none."
|
|
54
|
+
] }),
|
|
40
55
|
/* @__PURE__ */ jsxs("form", {
|
|
41
56
|
method: "post",
|
|
42
57
|
action: `/projects/${project.slug}/settings/tests`,
|
|
@@ -53,6 +68,22 @@ function renderSettingsTests(project, isAdmin, formState) {
|
|
|
53
68
|
disabled: !isAdmin,
|
|
54
69
|
hint: "When enabled, failing play blocks the build (failed). Flaky stories show warnings but status stays successful."
|
|
55
70
|
}),
|
|
71
|
+
/* @__PURE__ */ jsx(CheckField, {
|
|
72
|
+
label: "Run Storybook tests (.test())",
|
|
73
|
+
name: "runTests",
|
|
74
|
+
value: "true",
|
|
75
|
+
checked: project.runTests === true ? true : void 0,
|
|
76
|
+
disabled: !isAdmin,
|
|
77
|
+
hint: "Off by default, because it changes what a build contains. When enabled, each .test() entry runs once on the primary browser and a failing test fails the build (flaky stories excepted)."
|
|
78
|
+
}),
|
|
79
|
+
/* @__PURE__ */ jsx(CheckField, {
|
|
80
|
+
label: "Accessibility checks (axe)",
|
|
81
|
+
name: "runA11y",
|
|
82
|
+
value: "true",
|
|
83
|
+
checked: project.runA11y === false ? void 0 : true,
|
|
84
|
+
disabled: !isAdmin,
|
|
85
|
+
hint: "On by default. Each story is scanned with axe-core (WCAG A/AA and best-practice rules). Violations are shown on the snapshot in review and never block the build."
|
|
86
|
+
}),
|
|
56
87
|
/* @__PURE__ */ jsx(Field, {
|
|
57
88
|
label: "Play timeout (ms)",
|
|
58
89
|
name: "playTimeoutMs",
|
|
@@ -105,7 +136,6 @@ export const Flaky2: Story = {
|
|
|
105
136
|
]
|
|
106
137
|
});
|
|
107
138
|
}
|
|
108
|
-
//#endregion
|
|
109
139
|
export { renderSettingsTests };
|
|
110
140
|
|
|
111
141
|
//# sourceMappingURL=settings-tests.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-tests.mjs","names":[],"sources":["../../src/pages/settings-tests.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport {\n Alert,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/**
|
|
1
|
+
{"version":3,"file":"settings-tests.mjs","names":[],"sources":["../../src/pages/settings-tests.tsx"],"sourcesContent":["import type { Project } from \"@storyshelf/core/schema\";\nimport {\n Alert,\n Button,\n Card,\n CheckField,\n Field,\n FormActions,\n Meta,\n SectionTitle,\n} from \"../ui/components.tsx\";\nimport { csrfField } from \"../ui/csrf-field.tsx\";\n\n/** Tests settings tab: play execution, Storybook 10 test runs, accessibility checks, play timeout. */\nexport function renderSettingsTests(\n project: Project,\n isAdmin: boolean,\n formState?: { globalError?: string },\n): unknown {\n return (\n <div class=\"grid max-w-form\">\n {formState?.globalError ? <Alert tone=\"danger\">{formState.globalError}</Alert> : null}\n <Card>\n <SectionTitle>Interaction and accessibility tests</SectionTitle>\n <Meta>\n When enabled, Storybook <code>play</code> functions run before each screenshot. Failures\n block the build unless the story is marked\n <code>flaky-test</code> via <code>tags: ['flaky-test']</code> or\n <code>parameters: { flakyTest: true }</code> (supports both{\" \"}\n <code>chromatic</code> and <code>storyshelf</code> keys, story-level, case-insensitive).\n Use <code>disableSnapshot</code> to skip a story entirely.\n </Meta>\n <Meta>\n Accessibility checks run axe on every captured story. Violations appear with the snapshot\n in review and never block a build; a story opts out with{\" \"}\n <code>parameters: { a11y: { disable: true } }</code>.\n </Meta>\n <Meta>\n Storybook 10 <code>.test()</code> entries (<code>story.test(\"name\", async ...)</code>) can\n run as interaction tests too. They take no screenshot: each runs in the Storybook iframe\n and is reported as passed or failed on the build. A failing test fails the build like a\n failing <code>play</code> function. Storybook 8 and 9 builds have none.\n </Meta>\n <form\n method=\"post\"\n action={`/projects/${project.slug}/settings/tests`}\n hx-post={`/projects/${project.slug}/settings/tests`}\n hx-target=\"body\"\n hx-swap=\"outerHTML\"\n >\n {csrfField()}\n <CheckField\n label=\"Enable interaction tests (play)\"\n name=\"executePlay\"\n value=\"true\"\n checked={project.executePlay ? true : undefined}\n disabled={!isAdmin}\n hint=\"When enabled, failing play blocks the build (failed). Flaky stories show warnings but status stays successful.\"\n />\n <CheckField\n label=\"Run Storybook tests (.test())\"\n name=\"runTests\"\n value=\"true\"\n checked={project.runTests === true ? true : undefined}\n disabled={!isAdmin}\n hint=\"Off by default, because it changes what a build contains. When enabled, each .test() entry runs once on the primary browser and a failing test fails the build (flaky stories excepted).\"\n />\n <CheckField\n label=\"Accessibility checks (axe)\"\n name=\"runA11y\"\n value=\"true\"\n checked={project.runA11y === false ? undefined : true}\n disabled={!isAdmin}\n hint=\"On by default. Each story is scanned with axe-core (WCAG A/AA and best-practice rules). Violations are shown on the snapshot in review and never block the build.\"\n />\n <Field\n label=\"Play timeout (ms)\"\n name=\"playTimeoutMs\"\n type=\"number\"\n min=\"1000\"\n max=\"30000\"\n step=\"1000\"\n value={String(project.playTimeoutMs)}\n disabled={!isAdmin}\n hint=\"Timeout for each play function (1000–30000 ms, default 10000).\"\n />\n {isAdmin ? (\n <FormActions>\n <Button variant=\"primary\" type=\"submit\" icon=\"check\">\n Save changes\n </Button>\n </FormActions>\n ) : (\n <Meta>You need admin access to edit settings.</Meta>\n )}\n </form>\n </Card>\n <Card>\n <SectionTitle level={3}>How to mark stories</SectionTitle>\n <Meta as=\"pre\">\n {`// Disable snapshot entirely (skip capture + play)\nexport const Hidden: Story = {\n parameters: { storyshelf: { disableSnapshot: true } }\n};\n// Mark as flaky (non-blocking, shows warning)\nexport const Flaky: Story = {\n tags: ['flaky-test'],\n play: async ({canvasElement}) => { /* ... */ }\n};\n// or via parameters (both keys work, storyshelf wins)\nexport const Flaky2: Story = {\n parameters: { chromatic: { flakyTest: true } }\n};`}\n </Meta>\n <Meta>\n Tags are story-level and case-insensitive (<code>flaky-test</code>). Whole story is\n non-blocking when flaky. GitHub status stays\n <code>success</code> with a warning comment.\n </Meta>\n </Card>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAcA,SAAgB,oBACd,SACA,SACA,WACS;CACT,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACG,WAAW,cAAc,oBAAC,OAAD;IAAO,MAAK;IAAU,UAAA,UAAU;GAAmB,CAAA,IAAI;GACjF,qBAAC,MAAD,EAAA,UAAA;IACE,oBAAC,cAAD,EAAA,UAAc,sCAAiD,CAAA;IAC/D,qBAAC,MAAD,EAAA,UAAA;KAAM;KACoB,oBAAC,QAAD,EAAA,UAAM,OAAU,CAAA;KAAC;KAEzC,oBAAC,QAAD,EAAA,UAAM,aAAgB,CAAA;KAAC;KAAK,oBAAC,QAAD,EAAA,UAAM,uBAA0B,CAAA;KAAC;KAC7D,oBAAC,QAAD,EAAA,UAAM,kCAA+C,CAAA;KAAC;KAAgB;KACtE,oBAAC,QAAD,EAAA,UAAM,YAAe,CAAA;KAAC;KAAK,oBAAC,QAAD,EAAA,UAAM,aAAgB,CAAA;KAAC;KAC9C,oBAAC,QAAD,EAAA,UAAM,kBAAqB,CAAA;KAAC;IAC5B,EAAA,CAAA;IACN,qBAAC,MAAD,EAAA,UAAA;KAAM;KAEqD;KACzD,oBAAC,QAAD,EAAA,UAAM,0CAAiE,CAAA;KAAC;IACpE,EAAA,CAAA;IACN,qBAAC,MAAD,EAAA,UAAA;KAAM;KACS,oBAAC,QAAD,EAAA,UAAM,UAAa,CAAA;KAAC;KAAU,oBAAC,QAAD,EAAA,UAAM,kCAAmC,CAAA;KAAC;KAG7E,oBAAC,QAAD,EAAA,UAAM,OAAU,CAAA;KAAC;IACrB,EAAA,CAAA;IACN,qBAAC,QAAD;KACE,QAAO;KACP,QAAQ,aAAa,QAAQ,KAAK;KAClC,WAAS,aAAa,QAAQ,KAAK;KACnC,aAAU;KACV,WAAQ;KALV,UAAA;MAOG,UAAU;MACX,oBAAC,YAAD;OACE,OAAM;OACN,MAAK;OACL,OAAM;OACN,SAAS,QAAQ,cAAc,OAAO,KAAA;OACtC,UAAU,CAAC;OACX,MAAK;MACN,CAAA;MACD,oBAAC,YAAD;OACE,OAAM;OACN,MAAK;OACL,OAAM;OACN,SAAS,QAAQ,aAAa,OAAO,OAAO,KAAA;OAC5C,UAAU,CAAC;OACX,MAAK;MACN,CAAA;MACD,oBAAC,YAAD;OACE,OAAM;OACN,MAAK;OACL,OAAM;OACN,SAAS,QAAQ,YAAY,QAAQ,KAAA,IAAY;OACjD,UAAU,CAAC;OACX,MAAK;MACN,CAAA;MACD,oBAAC,OAAD;OACE,OAAM;OACN,MAAK;OACL,MAAK;OACL,KAAI;OACJ,KAAI;OACJ,MAAK;OACL,OAAO,OAAO,QAAQ,aAAa;OACnC,UAAU,CAAC;OACX,MAAK;MACN,CAAA;MACA,UACC,oBAAC,aAAD,EAAA,UACE,oBAAC,QAAD;OAAQ,SAAQ;OAAU,MAAK;OAAS,MAAK;OAAQ,UAAA;MAE7C,CAAA,EACG,CAAA,IAEb,oBAAC,MAAD,EAAA,UAAM,0CAA6C,CAAA;KAEjD;;GACF,EAAA,CAAA;GACN,qBAAC,MAAD,EAAA,UAAA;IACE,oBAAC,cAAD;KAAc,OAAO;KAAG,UAAA;IAAiC,CAAA;IACzD,oBAAC,MAAD;KAAM,IAAG;KACN,UAAA;;;;;;;;;;;;;IAaG,CAAA;IACN,qBAAC,MAAD,EAAA,UAAA;KAAM;KACuC,oBAAC,QAAD,EAAA,UAAM,aAAgB,CAAA;KAAC;KAElE,oBAAC,QAAD,EAAA,UAAM,UAAa,CAAA;KAAC;IAChB,EAAA,CAAA;GACF,EAAA,CAAA;EACH;;AAET"}
|
|
@@ -8,7 +8,7 @@ import { Card, SectionTitle } from "../ui/layout.mjs";
|
|
|
8
8
|
import { Table } from "../ui/table.mjs";
|
|
9
9
|
import { csrfField } from "../ui/csrf-field.mjs";
|
|
10
10
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
11
|
-
|
|
11
|
+
// src/pages/settings-tokens.tsx
|
|
12
12
|
/** Tokens settings tab: project CLI tokens plus the create-token form. */
|
|
13
13
|
function renderSettingsTokens(project, tokens, isAdmin, formState) {
|
|
14
14
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -84,7 +84,6 @@ function renderSettingsTokens(project, tokens, isAdmin, formState) {
|
|
|
84
84
|
]
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
-
//#endregion
|
|
88
87
|
export { renderSettingsTokens };
|
|
89
88
|
|
|
90
89
|
//# sourceMappingURL=settings-tokens.mjs.map
|
|
@@ -9,7 +9,7 @@ import { HStack } from "../ui/stacks.mjs";
|
|
|
9
9
|
import { Table } from "../ui/table.mjs";
|
|
10
10
|
import { csrfField } from "../ui/csrf-field.mjs";
|
|
11
11
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
12
|
-
|
|
12
|
+
// src/pages/settings-webhooks.tsx
|
|
13
13
|
/** Webhooks settings tab: webhook list plus the create-webhook form. */
|
|
14
14
|
function renderSettingsWebhooks(project, webhooks, isAdmin, formState) {
|
|
15
15
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -98,7 +98,6 @@ function renderSettingsWebhooks(project, webhooks, isAdmin, formState) {
|
|
|
98
98
|
]
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
|
-
//#endregion
|
|
102
101
|
export { renderSettingsWebhooks };
|
|
103
102
|
|
|
104
103
|
//# sourceMappingURL=settings-webhooks.mjs.map
|
package/dist/pages/storybook.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
|
-
|
|
5
|
+
// src/pages/storybook.tsx
|
|
6
6
|
/**
|
|
7
7
|
* Placeholder shown while a build's statics are not yet available.
|
|
8
8
|
* Refreshes itself; once extraction lands, the route serves the live page.
|
|
@@ -110,7 +110,6 @@ function escapeHtml(value) {
|
|
|
110
110
|
function escapeAttr(value) {
|
|
111
111
|
return escapeHtml(value);
|
|
112
112
|
}
|
|
113
|
-
//#endregion
|
|
114
113
|
export { renderStorybookPage, renderStorybookPreparingPage };
|
|
115
114
|
|
|
116
115
|
//# sourceMappingURL=storybook.mjs.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { targetLabel } from "../targets.mjs";
|
|
5
|
+
import { BROWSER_NAMES, unavailableReason } from "@storyshelf/core/adapter/capture-runner";
|
|
6
|
+
// src/pages/target-choices.ts
|
|
7
|
+
/** The capture targets a project can pick, from what the runner offers (ADR 0007, decision 7). */
|
|
8
|
+
/**
|
|
9
|
+
* Selectable targets: every ready one; a project's current target that is no longer ready stays,
|
|
10
|
+
* marked, so the form round-trips it. Without a runner list every engine is offered.
|
|
11
|
+
*/
|
|
12
|
+
function targetChoices(offered, current) {
|
|
13
|
+
if (offered === void 0) return {
|
|
14
|
+
choices: BROWSER_NAMES.map((name) => ({
|
|
15
|
+
value: name,
|
|
16
|
+
label: targetLabel(name)
|
|
17
|
+
})),
|
|
18
|
+
unavailable: []
|
|
19
|
+
};
|
|
20
|
+
const choices = offered.filter((entry) => unavailableReason(entry.status) === null).map((entry) => ({
|
|
21
|
+
value: entry.target,
|
|
22
|
+
label: targetLabel(entry.target)
|
|
23
|
+
}));
|
|
24
|
+
for (const target of current) if (!choices.some((choice) => choice.value === target)) choices.push({
|
|
25
|
+
value: target,
|
|
26
|
+
label: `${targetLabel(target)} (not available)`
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
choices,
|
|
30
|
+
unavailable: offered.flatMap((entry) => {
|
|
31
|
+
const reason = unavailableReason(entry.status);
|
|
32
|
+
return reason === null ? [] : [`${entry.target}: ${reason}`];
|
|
33
|
+
})
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export { targetChoices };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=target-choices.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target-choices.mjs","names":[],"sources":["../../src/pages/target-choices.ts"],"sourcesContent":["/** The capture targets a project can pick, from what the runner offers (ADR 0007, decision 7). */\nimport {\n BROWSER_NAMES,\n unavailableReason,\n type CaptureTarget,\n type RunnerTarget,\n} from \"@storyshelf/core/adapter/capture-runner\";\nimport { targetLabel } from \"../targets.ts\";\n\n/** One selectable target. */\nexport interface TargetChoice {\n value: string;\n label: string;\n}\n\n/** What the capture settings offer. */\nexport interface TargetChoices {\n /** Targets that can be picked now, plus the project's own (so saving never silently drops one). */\n choices: TargetChoice[];\n /** Offered but not selectable, with why (`firefox: declared, not installed`). */\n unavailable: string[];\n}\n\n/**\n * Selectable targets: every ready one; a project's current target that is no longer ready stays,\n * marked, so the form round-trips it. Without a runner list every engine is offered.\n */\nexport function targetChoices(\n offered: readonly RunnerTarget[] | undefined,\n current: readonly CaptureTarget[],\n): TargetChoices {\n if (offered === undefined) {\n return {\n choices: BROWSER_NAMES.map((name) => ({ value: name, label: targetLabel(name) })),\n unavailable: [],\n };\n }\n const ready = offered.filter((entry) => unavailableReason(entry.status) === null);\n const choices = ready.map((entry) => ({\n value: entry.target,\n label: targetLabel(entry.target),\n }));\n for (const target of current) {\n if (!choices.some((choice) => choice.value === target)) {\n choices.push({ value: target, label: `${targetLabel(target)} (not available)` });\n }\n }\n const unavailable = offered.flatMap((entry) => {\n const reason = unavailableReason(entry.status);\n return reason === null ? [] : [`${entry.target}: ${reason}`];\n });\n return { choices, unavailable };\n}\n"],"mappings":";;;;;;;;;;;AA2BA,SAAgB,cACd,SACA,SACe;CACf,IAAI,YAAY,KAAA,GACd,OAAO;EACL,SAAS,cAAc,KAAK,UAAU;GAAE,OAAO;GAAM,OAAO,YAAY,IAAI;EAAE,EAAE;EAChF,aAAa,CAAC;CAChB;CAGF,MAAM,UADQ,QAAQ,QAAQ,UAAU,kBAAkB,MAAM,MAAM,MAAM,IACxD,CAAC,CAAC,KAAK,WAAW;EACpC,OAAO,MAAM;EACb,OAAO,YAAY,MAAM,MAAM;CACjC,EAAE;CACF,KAAK,MAAM,UAAU,SACnB,IAAI,CAAC,QAAQ,MAAM,WAAW,OAAO,UAAU,MAAM,GACnD,QAAQ,KAAK;EAAE,OAAO;EAAQ,OAAO,GAAG,YAAY,MAAM,EAAE;CAAkB,CAAC;CAOnF,OAAO;EAAE;EAAS,aAJE,QAAQ,SAAS,UAAU;GAC7C,MAAM,SAAS,kBAAkB,MAAM,MAAM;GAC7C,OAAO,WAAW,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,OAAO,IAAI,QAAQ;EAC7D,CAC4B;CAAE;AAChC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { DEFAULT_SCRATCH_DIR } from "@storyshelf/core/config";
|
|
5
|
+
import { createCatalogHook, createCatalogLoader, createPublishCompleter, createPublishHandler, extractionLimitsOf, resolvePublishLimits } from "@storyshelf/core/capture";
|
|
6
|
+
// src/publish-setup.ts
|
|
7
|
+
/**
|
|
8
|
+
* Publishing wiring (ADR 0006, decision 10): the completer every upload route ends in, and the
|
|
9
|
+
* `publish` job kind on the shared database queue. Its handler runs on every instance with a
|
|
10
|
+
* database, so a hosting-only instance can publish large zips; it is absent on serverless hosts,
|
|
11
|
+
* where an over-limit zip is refused instead. The consumer that calls the handler is built in
|
|
12
|
+
* `jobs-setup.ts`, together with the capture handler.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Wire publishing for `options` (the instrumented options; `config` is the resolved config).
|
|
16
|
+
* `captureWillRun` is whether uploads get a capture (`enqueueCapture` exists): capture owns the
|
|
17
|
+
* build status then, and publishing leaves it alone. `delivery.jobs` is the shared queue; a host
|
|
18
|
+
* that polls no `publish` jobs (serverless) has none of the job side. When publishing ends,
|
|
19
|
+
* `delivery.buildState` shows the build's state on its git channels (the `storybook` status) and
|
|
20
|
+
* `delivery.reviewRequested` asks the reviewers for a verdict on a build that waits for one.
|
|
21
|
+
*/
|
|
22
|
+
function setupPublishing(options, config, logger, captureWillRun, delivery) {
|
|
23
|
+
const { jobs } = delivery;
|
|
24
|
+
const catalog = createCatalogLoader({
|
|
25
|
+
db: options.database,
|
|
26
|
+
storage: options.storage,
|
|
27
|
+
logger: logger.child({ component: "catalog" })
|
|
28
|
+
});
|
|
29
|
+
const afterPublish = publishHooks(catalog, captureWillRun, delivery);
|
|
30
|
+
const completer = createPublishCompleter({
|
|
31
|
+
db: options.database,
|
|
32
|
+
captureWillRun,
|
|
33
|
+
afterPublish,
|
|
34
|
+
logger
|
|
35
|
+
});
|
|
36
|
+
if (!jobs.kinds.includes("publish")) return {
|
|
37
|
+
completer,
|
|
38
|
+
afterPublish,
|
|
39
|
+
catalog,
|
|
40
|
+
enqueuePublish: void 0,
|
|
41
|
+
handler: null
|
|
42
|
+
};
|
|
43
|
+
jobs.exhausted.publish = async (job, error) => {
|
|
44
|
+
await completer.complete(job.buildId, {
|
|
45
|
+
ok: false,
|
|
46
|
+
error
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
completer,
|
|
51
|
+
afterPublish,
|
|
52
|
+
catalog,
|
|
53
|
+
enqueuePublish: async (buildId, reqId) => {
|
|
54
|
+
await jobs.get().enqueueJob({
|
|
55
|
+
kind: "publish",
|
|
56
|
+
buildId,
|
|
57
|
+
...reqId ? { reqId } : {}
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
handler: createPublishHandler({
|
|
61
|
+
db: options.database,
|
|
62
|
+
storage: options.storage,
|
|
63
|
+
scratchDir: config.scratchDir ?? DEFAULT_SCRATCH_DIR,
|
|
64
|
+
limits: extractionLimitsOf(resolvePublishLimits(config)),
|
|
65
|
+
completer,
|
|
66
|
+
logger
|
|
67
|
+
})
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Hooks run after every publish: the catalog, the build's state on its git channels (the Storybook
|
|
72
|
+
* status shows the end of publishing, a transition no build status change marks) and, without
|
|
73
|
+
* `testing`, the request for a verdict on a build left `pending` (ADR 0013, decision 4).
|
|
74
|
+
*/
|
|
75
|
+
function publishHooks(catalog, captureWillRun, delivery) {
|
|
76
|
+
return [
|
|
77
|
+
createCatalogHook(catalog),
|
|
78
|
+
async ({ build }) => {
|
|
79
|
+
await delivery.buildState.request(build.id);
|
|
80
|
+
},
|
|
81
|
+
async ({ build, project, outcome }) => {
|
|
82
|
+
if (outcome.ok && !captureWillRun && build.status === "pending") await delivery.reviewRequested(project, build);
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
export { setupPublishing };
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=publish-setup.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-setup.mjs","names":[],"sources":["../src/publish-setup.ts"],"sourcesContent":["/**\n * Publishing wiring (ADR 0006, decision 10): the completer every upload route ends in, and the\n * `publish` job kind on the shared database queue. Its handler runs on every instance with a\n * database, so a hosting-only instance can publish large zips; it is absent on serverless hosts,\n * where an over-limit zip is refused instead. The consumer that calls the handler is built in\n * `jobs-setup.ts`, together with the capture handler.\n */\nimport {\n createCatalogHook,\n createCatalogLoader,\n createPublishCompleter,\n createPublishHandler,\n extractionLimitsOf,\n resolvePublishLimits,\n type AfterPublishHook,\n type BuildStatePublisher,\n type CatalogLoader,\n type KindHandler,\n type PublishCompleter,\n} from \"@storyshelf/core/capture\";\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\nimport { DEFAULT_SCRATCH_DIR } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport type { JobQueue } from \"./job-queue.ts\";\n\n/** What request handlers and the consumer use of publishing. */\nexport interface PublishWiring {\n /** Applies the acceptance rule and the hooks when a publish ends. */\n completer: PublishCompleter;\n /** Hooks run after every publish; the catalog registers here. */\n afterPublish: AfterPublishHook[];\n /** The Storybook catalog of published builds (cached in memory; catalogues old builds lazily). */\n catalog: CatalogLoader;\n /** Puts a `publish` job on the queue; undefined when this host runs no consumer. */\n enqueuePublish: ((buildId: string, reqId?: string) => Promise<void>) | undefined;\n /** Runs `publish` jobs; null when this host runs none. */\n handler: KindHandler | null;\n}\n\n/** The request-scoped part of {@link PublishWiring}. */\nexport type PublishService = Pick<\n PublishWiring,\n \"completer\" | \"afterPublish\" | \"catalog\" | \"enqueuePublish\"\n>;\n\n/**\n * Wire publishing for `options` (the instrumented options; `config` is the resolved config).\n * `captureWillRun` is whether uploads get a capture (`enqueueCapture` exists): capture owns the\n * build status then, and publishing leaves it alone. `delivery.jobs` is the shared queue; a host\n * that polls no `publish` jobs (serverless) has none of the job side. When publishing ends,\n * `delivery.buildState` shows the build's state on its git channels (the `storybook` status) and\n * `delivery.reviewRequested` asks the reviewers for a verdict on a build that waits for one.\n */\nexport function setupPublishing(\n options: ShelfOptions,\n config: ShelfConfig,\n logger: Logger,\n captureWillRun: boolean,\n delivery: {\n jobs: JobQueue;\n buildState: BuildStatePublisher;\n /** Tells the project's reviewers a build waits for a verdict (`review:requested`). */\n reviewRequested: (project: Project, build: Build) => Promise<void>;\n },\n): PublishWiring {\n const { jobs } = delivery;\n const catalog = createCatalogLoader({\n db: options.database,\n storage: options.storage,\n logger: logger.child({ component: \"catalog\" }),\n });\n const afterPublish = publishHooks(catalog, captureWillRun, delivery);\n const completer = createPublishCompleter({\n db: options.database,\n captureWillRun,\n afterPublish,\n logger,\n });\n if (!jobs.kinds.includes(\"publish\")) {\n return { completer, afterPublish, catalog, enqueuePublish: undefined, handler: null };\n }\n // A publish whose last attempt lost its lease has no running handler to fail the build.\n jobs.exhausted.publish = async (job, error): Promise<void> => {\n await completer.complete(job.buildId, { ok: false, error });\n };\n return {\n completer,\n afterPublish,\n catalog,\n enqueuePublish: async (buildId, reqId): Promise<void> => {\n await jobs.get().enqueueJob({ kind: \"publish\", buildId, ...(reqId ? { reqId } : {}) });\n },\n handler: createPublishHandler({\n db: options.database,\n storage: options.storage,\n scratchDir: config.scratchDir ?? DEFAULT_SCRATCH_DIR,\n limits: extractionLimitsOf(resolvePublishLimits(config)),\n completer,\n logger,\n }),\n };\n}\n\n/**\n * Hooks run after every publish: the catalog, the build's state on its git channels (the Storybook\n * status shows the end of publishing, a transition no build status change marks) and, without\n * `testing`, the request for a verdict on a build left `pending` (ADR 0013, decision 4).\n */\nfunction publishHooks(\n catalog: CatalogLoader,\n captureWillRun: boolean,\n delivery: Pick<Parameters<typeof setupPublishing>[4], \"buildState\" | \"reviewRequested\">,\n): AfterPublishHook[] {\n return [\n createCatalogHook(catalog),\n async ({ build }) => {\n await delivery.buildState.request(build.id);\n },\n async ({ build, project, outcome }) => {\n if (outcome.ok && !captureWillRun && build.status === \"pending\") {\n await delivery.reviewRequested(project, build);\n }\n },\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsDA,SAAgB,gBACd,SACA,QACA,QACA,gBACA,UAMe;CACf,MAAM,EAAE,SAAS;CACjB,MAAM,UAAU,oBAAoB;EAClC,IAAI,QAAQ;EACZ,SAAS,QAAQ;EACjB,QAAQ,OAAO,MAAM,EAAE,WAAW,UAAU,CAAC;CAC/C,CAAC;CACD,MAAM,eAAe,aAAa,SAAS,gBAAgB,QAAQ;CACnE,MAAM,YAAY,uBAAuB;EACvC,IAAI,QAAQ;EACZ;EACA;EACA;CACF,CAAC;CACD,IAAI,CAAC,KAAK,MAAM,SAAS,SAAS,GAChC,OAAO;EAAE;EAAW;EAAc;EAAS,gBAAgB,KAAA;EAAW,SAAS;CAAK;CAGtF,KAAK,UAAU,UAAU,OAAO,KAAK,UAAyB;EAC5D,MAAM,UAAU,SAAS,IAAI,SAAS;GAAE,IAAI;GAAO;EAAM,CAAC;CAC5D;CACA,OAAO;EACL;EACA;EACA;EACA,gBAAgB,OAAO,SAAS,UAAyB;GACvD,MAAM,KAAK,IAAI,CAAC,CAAC,WAAW;IAAE,MAAM;IAAW;IAAS,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GAAG,CAAC;EACvF;EACA,SAAS,qBAAqB;GAC5B,IAAI,QAAQ;GACZ,SAAS,QAAQ;GACjB,YAAY,OAAO,cAAc;GACjC,QAAQ,mBAAmB,qBAAqB,MAAM,CAAC;GACvD;GACA;EACF,CAAC;CACH;AACF;;;;;;AAOA,SAAS,aACP,SACA,gBACA,UACoB;CACpB,OAAO;EACL,kBAAkB,OAAO;EACzB,OAAO,EAAE,YAAY;GACnB,MAAM,SAAS,WAAW,QAAQ,MAAM,EAAE;EAC5C;EACA,OAAO,EAAE,OAAO,SAAS,cAAc;GACrC,IAAI,QAAQ,MAAM,CAAC,kBAAkB,MAAM,WAAW,WACpD,MAAM,SAAS,gBAAgB,SAAS,KAAK;EAEjD;CACF;AACF"}
|