@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,21 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
|
+
import { getStore } from "../store.mjs";
|
|
6
|
+
import { NotificationChannelModel, WebhookModel } from "@storyshelf/core/models";
|
|
7
|
+
// src/routers/scoped-models.ts
|
|
8
|
+
/** Models that encrypt secrets at rest, bound to the current request's db and server secret. */
|
|
9
|
+
/** Webhook model for the current request. */
|
|
10
|
+
function webhookModel() {
|
|
11
|
+
const { db, config } = getStore();
|
|
12
|
+
return new WebhookModel(db, void 0, credentialKeys(config));
|
|
13
|
+
}
|
|
14
|
+
/** Notification-channel model for the current request. */
|
|
15
|
+
function notificationChannelModel() {
|
|
16
|
+
const { db, config } = getStore();
|
|
17
|
+
return new NotificationChannelModel(db, void 0, credentialKeys(config));
|
|
18
|
+
}
|
|
19
|
+
export { notificationChannelModel, webhookModel };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=scoped-models.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoped-models.mjs","names":[],"sources":["../../src/routers/scoped-models.ts"],"sourcesContent":["import { NotificationChannelModel, WebhookModel } from \"@storyshelf/core/models\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\n\n/** Models that encrypt secrets at rest, bound to the current request's db and server secret. */\n\n/** Webhook model for the current request. */\nexport function webhookModel(): WebhookModel {\n const { db, config } = getStore();\n return new WebhookModel(db, undefined, credentialKeys(config));\n}\n\n/** Notification-channel model for the current request. */\nexport function notificationChannelModel(): NotificationChannelModel {\n const { db, config } = getStore();\n return new NotificationChannelModel(db, undefined, credentialKeys(config));\n}\n"],"mappings":";;;;;;;;;AAOA,SAAgB,eAA6B;CAC3C,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,OAAO,IAAI,aAAa,IAAI,KAAA,GAAW,eAAe,MAAM,CAAC;AAC/D;;AAGA,SAAgB,2BAAqD;CACnE,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,OAAO,IAAI,yBAAyB,IAAI,KAAA,GAAW,eAAe,MAAM,CAAC;AAC3E"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { requireSiteAdmin } from "./helpers.mjs";
|
|
6
|
+
import { hxRedirect } from "./htmx.mjs";
|
|
7
|
+
import { flash } from "./flash.mjs";
|
|
8
|
+
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
9
|
+
import { ProjectModel } from "@storyshelf/core/models";
|
|
10
|
+
// src/routers/settings-ai.ts
|
|
11
|
+
/** Register the project AI settings tab (site admins set the profile gate). */
|
|
12
|
+
function registerAiSettings(app) {
|
|
13
|
+
app.get("/projects/:slug/settings/ai", async (c) => c.html(await renderSettingsPage(c, "ai")));
|
|
14
|
+
app.post("/projects/:slug/settings/ai", handleAiUpdate);
|
|
15
|
+
}
|
|
16
|
+
/** Error text for an unusable choice, or null when it is allowed. */
|
|
17
|
+
function profileError(ai, requested) {
|
|
18
|
+
if (!ai) return "AI is not configured";
|
|
19
|
+
return requested !== "" && !ai.profileNames().includes(requested) ? `Unknown profile "${requested}"` : null;
|
|
20
|
+
}
|
|
21
|
+
async function handleAiUpdate(c) {
|
|
22
|
+
requireSiteAdmin(c);
|
|
23
|
+
const { ai, db } = getStore();
|
|
24
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
25
|
+
const requested = asString((await c.req.formData()).get("aiProfile")) ?? "";
|
|
26
|
+
const error = profileError(ai, requested);
|
|
27
|
+
if (error) return c.html(await renderSettingsPage(c, "ai", { globalError: error }), 400);
|
|
28
|
+
await new ProjectModel(db).update(project.id, { aiProfile: requested === "" ? null : requested });
|
|
29
|
+
flash(c, requested === "" ? "AI turned off for this project" : "AI profile saved");
|
|
30
|
+
return hxRedirect(c, `/projects/${project.slug}/settings/ai`);
|
|
31
|
+
}
|
|
32
|
+
export { registerAiSettings };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=settings-ai.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-ai.mjs","names":[],"sources":["../../src/routers/settings-ai.ts"],"sourcesContent":["import type { Ai } from \"@storyshelf/core/ai\";\nimport { ProjectModel } 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 { requireSiteAdmin } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Register the project AI settings tab (site admins set the profile gate). */\nexport function registerAiSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/ai\", async (c) => c.html(await renderSettingsPage(c, \"ai\")));\n app.post(\"/projects/:slug/settings/ai\", handleAiUpdate);\n}\n\n/** Error text for an unusable choice, or null when it is allowed. */\nfunction profileError(ai: Ai | undefined, requested: string): string | null {\n if (!ai) {\n return \"AI is not configured\";\n }\n return requested !== \"\" && !ai.profileNames().includes(requested)\n ? `Unknown profile \"${requested}\"`\n : null;\n}\n\nasync function handleAiUpdate(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { ai, db } = getStore();\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const requested = asString((await c.req.formData()).get(\"aiProfile\")) ?? \"\";\n const error = profileError(ai, requested);\n if (error) {\n return c.html(await renderSettingsPage(c, \"ai\", { globalError: error }), 400);\n }\n await new ProjectModel(db).update(project.id, { aiProfile: requested === \"\" ? null : requested });\n flash(c, requested === \"\" ? \"AI turned off for this project\" : \"AI profile saved\");\n return hxRedirect(c, `/projects/${project.slug}/settings/ai`);\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgB,mBAAmB,KAAwB;CACzD,IAAI,IAAI,+BAA+B,OAAO,MAAM,EAAE,KAAK,MAAM,mBAAmB,GAAG,IAAI,CAAC,CAAC;CAC7F,IAAI,KAAK,+BAA+B,cAAc;AACxD;;AAGA,SAAS,aAAa,IAAoB,WAAkC;CAC1E,IAAI,CAAC,IACH,OAAO;CAET,OAAO,cAAc,MAAM,CAAC,GAAG,aAAa,CAAC,CAAC,SAAS,SAAS,IAC5D,oBAAoB,UAAU,KAC9B;AACN;AAEA,eAAe,eAAe,GAA+B;CAC3D,iBAAiB,CAAC;CAClB,MAAM,EAAE,IAAI,OAAO,SAAS;CAC5B,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,YAAY,UAAU,MAAM,EAAE,IAAI,SAAS,EAAA,CAAG,IAAI,WAAW,CAAC,KAAK;CACzE,MAAM,QAAQ,aAAa,IAAI,SAAS;CACxC,IAAI,OACF,OAAO,EAAE,KAAK,MAAM,mBAAmB,GAAG,MAAM,EAAE,aAAa,MAAM,CAAC,GAAG,GAAG;CAE9E,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,EAAE,WAAW,cAAc,KAAK,OAAO,UAAU,CAAC;CAChG,MAAM,GAAG,cAAc,KAAK,mCAAmC,kBAAkB;CACjF,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,aAAa;AAC9D"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { assertTargetsSelectable } from "../targets.mjs";
|
|
6
|
+
import { hxRedirect } from "./htmx.mjs";
|
|
7
|
+
import { flash } from "./flash.mjs";
|
|
8
|
+
import { toProjectPatch } from "./capture-config.mjs";
|
|
9
|
+
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
10
|
+
import { storybookUrlTtlOverrideError } from "@storyshelf/core/utils";
|
|
11
|
+
import { resolveBrowsers } from "@storyshelf/core/capture";
|
|
12
|
+
import { ProjectModel } from "@storyshelf/core/models";
|
|
13
|
+
import { HTTPException } from "hono/http-exception";
|
|
14
|
+
import { isCaptureTarget } from "@storyshelf/core/adapter/capture-runner";
|
|
15
|
+
// src/routers/settings-capture.ts
|
|
16
|
+
/** Capture settings: browsers (primary + extras), the mode allowlist and the signed URL lifetime. */
|
|
17
|
+
function registerCaptureSettings(app) {
|
|
18
|
+
app.get("/projects/:slug/settings/capture", async (c) => c.html(await renderSettingsPage(c, "capture")));
|
|
19
|
+
app.post("/projects/:slug/settings/capture", handleCaptureUpdate);
|
|
20
|
+
}
|
|
21
|
+
const EXTRA_PREFIX = "extra_";
|
|
22
|
+
/** Primary target first, then the ticked extras; throws 400 when the form is malformed. */
|
|
23
|
+
function readBrowsers(form) {
|
|
24
|
+
const primary = asString(form.get("browser")) ?? "chromium";
|
|
25
|
+
if (!isCaptureTarget(primary)) throw badBrowser(primary);
|
|
26
|
+
const extras = [...form.entries()].flatMap(([key, value]) => key.startsWith(EXTRA_PREFIX) && value === key.slice(6) ? [key.slice(6)] : []);
|
|
27
|
+
const bad = extras.find((extra) => !isCaptureTarget(extra));
|
|
28
|
+
if (bad !== void 0) throw badBrowser(bad);
|
|
29
|
+
return [.../* @__PURE__ */ new Set([primary, ...extras])];
|
|
30
|
+
}
|
|
31
|
+
function badBrowser(value) {
|
|
32
|
+
return new HTTPException(400, { message: `Unknown browser "${value}"` });
|
|
33
|
+
}
|
|
34
|
+
/** Mode names, one per line (blank lines and surrounding space ignored). */
|
|
35
|
+
function readAllowlist(form) {
|
|
36
|
+
const lines = (asString(form.get("modesAllowlist")) ?? "").split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
37
|
+
return lines.length > 0 ? lines : null;
|
|
38
|
+
}
|
|
39
|
+
/** Seconds of the signed Storybook URL lifetime; empty derives it from the stories (null). */
|
|
40
|
+
function readUrlTtl(form) {
|
|
41
|
+
const raw = asString(form.get("storybookUrlTtlSeconds"));
|
|
42
|
+
if (!raw) return null;
|
|
43
|
+
const seconds = Number(raw);
|
|
44
|
+
const message = storybookUrlTtlOverrideError(seconds);
|
|
45
|
+
if (message !== void 0) throw new HTTPException(400, { message });
|
|
46
|
+
return seconds;
|
|
47
|
+
}
|
|
48
|
+
async function reject(c, message) {
|
|
49
|
+
return c.html(await renderSettingsPage(c, "capture", { globalError: message }) ?? "", 400);
|
|
50
|
+
}
|
|
51
|
+
async function saveCapture(project, form, browsers) {
|
|
52
|
+
const patch = toProjectPatch({
|
|
53
|
+
browsers: browsers.length > 1 ? browsers : null,
|
|
54
|
+
browser: browsers[0],
|
|
55
|
+
modesAllowlist: readAllowlist(form),
|
|
56
|
+
storybookUrlTtlSeconds: readUrlTtl(form)
|
|
57
|
+
});
|
|
58
|
+
await new ProjectModel(getStore().db).update(project.id, patch);
|
|
59
|
+
}
|
|
60
|
+
async function handleCaptureUpdate(c) {
|
|
61
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
62
|
+
const form = await c.req.formData();
|
|
63
|
+
try {
|
|
64
|
+
const browsers = readBrowsers(form);
|
|
65
|
+
await assertTargetsSelectable(browsers, resolveBrowsers(project));
|
|
66
|
+
await saveCapture(project, form, browsers);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof HTTPException) return await reject(c, error.message);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
flash(c, "Capture settings saved");
|
|
72
|
+
return hxRedirect(c, `/projects/${project.slug}/settings/capture`);
|
|
73
|
+
}
|
|
74
|
+
export { registerCaptureSettings };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=settings-capture.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-capture.mjs","names":[],"sources":["../../src/routers/settings-capture.ts"],"sourcesContent":["import { isCaptureTarget, type CaptureTarget } from \"@storyshelf/core/adapter/capture-runner\";\nimport { resolveBrowsers } from \"@storyshelf/core/capture\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { storybookUrlTtlOverrideError } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { assertTargetsSelectable } from \"../targets.ts\";\nimport { toProjectPatch } from \"./capture-config.ts\";\nimport { flash } from \"./flash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, findProject, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Capture settings: browsers (primary + extras), the mode allowlist and the signed URL lifetime. */\nexport function registerCaptureSettings(app: ShelfRouter): void {\n app.get(\"/projects/:slug/settings/capture\", async (c) =>\n c.html(await renderSettingsPage(c, \"capture\")),\n );\n app.post(\"/projects/:slug/settings/capture\", handleCaptureUpdate);\n}\n\nconst EXTRA_PREFIX = \"extra_\";\n\n/** Primary target first, then the ticked extras; throws 400 when the form is malformed. */\nfunction readBrowsers(form: FormData): CaptureTarget[] {\n const primary = asString(form.get(\"browser\")) ?? \"chromium\";\n if (!isCaptureTarget(primary)) throw badBrowser(primary);\n const extras = [...form.entries()].flatMap(([key, value]) =>\n key.startsWith(EXTRA_PREFIX) && value === key.slice(EXTRA_PREFIX.length)\n ? [key.slice(EXTRA_PREFIX.length)]\n : [],\n );\n const bad = extras.find((extra) => !isCaptureTarget(extra));\n if (bad !== undefined) throw badBrowser(bad);\n return [...new Set([primary, ...(extras as CaptureTarget[])])];\n}\n\nfunction badBrowser(value: string): HTTPException {\n return new HTTPException(400, { message: `Unknown browser \"${value}\"` });\n}\n\n/** Mode names, one per line (blank lines and surrounding space ignored). */\nfunction readAllowlist(form: FormData): string[] | null {\n const lines = (asString(form.get(\"modesAllowlist\")) ?? \"\")\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line !== \"\");\n return lines.length > 0 ? lines : null;\n}\n\n/** Seconds of the signed Storybook URL lifetime; empty derives it from the stories (null). */\nfunction readUrlTtl(form: FormData): number | null {\n const raw = asString(form.get(\"storybookUrlTtlSeconds\"));\n if (!raw) return null;\n const seconds = Number(raw);\n const message = storybookUrlTtlOverrideError(seconds);\n if (message !== undefined) {\n throw new HTTPException(400, { message });\n }\n return seconds;\n}\n\nasync function reject(c: Context, message: string): Promise<Response> {\n return c.html((await renderSettingsPage(c, \"capture\", { globalError: message })) ?? \"\", 400);\n}\n\nasync function saveCapture(\n project: Project,\n form: FormData,\n browsers: CaptureTarget[],\n): Promise<void> {\n const patch = toProjectPatch({\n browsers: browsers.length > 1 ? browsers : null,\n browser: browsers[0],\n modesAllowlist: readAllowlist(form),\n storybookUrlTtlSeconds: readUrlTtl(form),\n });\n await new ProjectModel(getStore().db).update(project.id, patch);\n}\n\nasync function handleCaptureUpdate(c: Context): Promise<Response> {\n const project = await findProject(c.req.param(\"slug\") ?? \"\");\n const form = await c.req.formData();\n try {\n const browsers = readBrowsers(form);\n await assertTargetsSelectable(browsers, resolveBrowsers(project));\n await saveCapture(project, form, browsers);\n } catch (error) {\n if (error instanceof HTTPException) return await reject(c, error.message);\n throw error;\n }\n flash(c, \"Capture settings saved\");\n return hxRedirect(c, `/projects/${project.slug}/settings/capture`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,wBAAwB,KAAwB;CAC9D,IAAI,IAAI,oCAAoC,OAAO,MACjD,EAAE,KAAK,MAAM,mBAAmB,GAAG,SAAS,CAAC,CAC/C;CACA,IAAI,KAAK,oCAAoC,mBAAmB;AAClE;AAEA,MAAM,eAAe;;AAGrB,SAAS,aAAa,MAAiC;CACrD,MAAM,UAAU,SAAS,KAAK,IAAI,SAAS,CAAC,KAAK;CACjD,IAAI,CAAC,gBAAgB,OAAO,GAAG,MAAM,WAAW,OAAO;CACvD,MAAM,SAAS,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,WAChD,IAAI,WAAW,YAAY,KAAK,UAAU,IAAI,MAAM,CAAmB,IACnE,CAAC,IAAI,MAAM,CAAmB,CAAC,IAC/B,CAAC,CACP;CACA,MAAM,MAAM,OAAO,MAAM,UAAU,CAAC,gBAAgB,KAAK,CAAC;CAC1D,IAAI,QAAQ,KAAA,GAAW,MAAM,WAAW,GAAG;CAC3C,OAAO,CAAC,mBAAG,IAAI,IAAI,CAAC,SAAS,GAAI,MAA0B,CAAC,CAAC;AAC/D;AAEA,SAAS,WAAW,OAA8B;CAChD,OAAO,IAAI,cAAc,KAAK,EAAE,SAAS,oBAAoB,MAAM,GAAG,CAAC;AACzE;;AAGA,SAAS,cAAc,MAAiC;CACtD,MAAM,SAAS,SAAS,KAAK,IAAI,gBAAgB,CAAC,KAAK,GAAA,CACpD,MAAM,IAAI,CAAC,CACX,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,CAC1B,QAAQ,SAAS,SAAS,EAAE;CAC/B,OAAO,MAAM,SAAS,IAAI,QAAQ;AACpC;;AAGA,SAAS,WAAW,MAA+B;CACjD,MAAM,MAAM,SAAS,KAAK,IAAI,wBAAwB,CAAC;CACvD,IAAI,CAAC,KAAK,OAAO;CACjB,MAAM,UAAU,OAAO,GAAG;CAC1B,MAAM,UAAU,6BAA6B,OAAO;CACpD,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,cAAc,KAAK,EAAE,QAAQ,CAAC;CAE1C,OAAO;AACT;AAEA,eAAe,OAAO,GAAY,SAAoC;CACpE,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,WAAW,EAAE,aAAa,QAAQ,CAAC,KAAM,IAAI,GAAG;AAC7F;AAEA,eAAe,YACb,SACA,MACA,UACe;CACf,MAAM,QAAQ,eAAe;EAC3B,UAAU,SAAS,SAAS,IAAI,WAAW;EAC3C,SAAS,SAAS;EAClB,gBAAgB,cAAc,IAAI;EAClC,wBAAwB,WAAW,IAAI;CACzC,CAAC;CACD,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,KAAK;AAChE;AAEA,eAAe,oBAAoB,GAA+B;CAChE,MAAM,UAAU,MAAM,YAAY,EAAE,IAAI,MAAM,MAAM,KAAK,EAAE;CAC3D,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,IAAI;EACF,MAAM,WAAW,aAAa,IAAI;EAClC,MAAM,wBAAwB,UAAU,gBAAgB,OAAO,CAAC;EAChE,MAAM,YAAY,SAAS,MAAM,QAAQ;CAC3C,SAAS,OAAO;EACd,IAAI,iBAAiB,eAAe,OAAO,MAAM,OAAO,GAAG,MAAM,OAAO;EACxE,MAAM;CACR;CACA,MAAM,GAAG,wBAAwB;CACjC,OAAO,WAAW,GAAG,aAAa,QAAQ,KAAK,kBAAkB;AACnE"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
|
+
import { getStore } from "../store.mjs";
|
|
6
|
+
import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
|
|
7
|
+
import { hxRedirect } from "./htmx.mjs";
|
|
8
|
+
import { flash } from "./flash.mjs";
|
|
9
|
+
import { asString, renderSettingsPage } from "./settings.handlers.mjs";
|
|
10
|
+
import { DesignCredentialModel, DesignModel } from "@storyshelf/core/models";
|
|
11
|
+
import { InvalidDesignUrlError } from "@storyshelf/core/adapter/design";
|
|
12
|
+
// src/routers/settings-design.ts
|
|
13
|
+
/** Design settings (project design files, component mapping, provider credentials). */
|
|
14
|
+
function registerDesignSettings(app) {
|
|
15
|
+
const base = "/projects/:slug/settings/design";
|
|
16
|
+
app.get(base, async (c) => c.html(await renderSettingsPage(c, "design")));
|
|
17
|
+
app.post(`${base}/files`, handleAddFile);
|
|
18
|
+
app.post(`${base}/files/:id/delete`, handleRemoveLink);
|
|
19
|
+
app.post(`${base}/components`, handleMapComponent);
|
|
20
|
+
app.post(`${base}/components/:id/delete`, handleRemoveLink);
|
|
21
|
+
app.post(`${base}/:provider/token`, handleSetToken);
|
|
22
|
+
app.post(`${base}/:provider/token/delete`, handleRemoveToken);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Every design-settings mutation writes credentials or mappings: require a
|
|
26
|
+
* project admin server-side (the forms are only hidden from others, never trusted).
|
|
27
|
+
* With auth disabled the app has no roles and everyone may administer, as elsewhere.
|
|
28
|
+
*/
|
|
29
|
+
async function adminProject(c) {
|
|
30
|
+
return await resolveAuthorizedProject(c, c.req.param("slug") ?? "", "admin");
|
|
31
|
+
}
|
|
32
|
+
function designModel() {
|
|
33
|
+
const { db, design } = getStore();
|
|
34
|
+
return new DesignModel(db, design);
|
|
35
|
+
}
|
|
36
|
+
function credentialModel() {
|
|
37
|
+
const { db, config } = getStore();
|
|
38
|
+
return new DesignCredentialModel(db, credentialKeys(config));
|
|
39
|
+
}
|
|
40
|
+
/** The configured provider addressed by `:provider`, or a 404. */
|
|
41
|
+
function providerOf(c) {
|
|
42
|
+
const provider = getStore().design.find((candidate) => candidate.metadata.kind === c.req.param("provider"));
|
|
43
|
+
if (!provider) notFound("Unknown design provider");
|
|
44
|
+
return provider;
|
|
45
|
+
}
|
|
46
|
+
async function reject(c, errors) {
|
|
47
|
+
return c.html(await renderSettingsPage(c, "design", { errors }) ?? "", 400);
|
|
48
|
+
}
|
|
49
|
+
function done(c, slug, message) {
|
|
50
|
+
flash(c, message);
|
|
51
|
+
return hxRedirect(c, `/projects/${slug}/settings/design`);
|
|
52
|
+
}
|
|
53
|
+
async function handleAddFile(c) {
|
|
54
|
+
const project = await adminProject(c);
|
|
55
|
+
const form = await c.req.formData();
|
|
56
|
+
const label = asString(form.get("label")) ?? "";
|
|
57
|
+
try {
|
|
58
|
+
await designModel().addProjectLink(project.id, {
|
|
59
|
+
label,
|
|
60
|
+
url: asString(form.get("url")) ?? ""
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof InvalidDesignUrlError) return await reject(c, { file: error.message });
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return done(c, project.slug, "Design file added");
|
|
67
|
+
}
|
|
68
|
+
async function handleMapComponent(c) {
|
|
69
|
+
const project = await adminProject(c);
|
|
70
|
+
const form = await c.req.formData();
|
|
71
|
+
const importPath = asString(form.get("importPath")) ?? "";
|
|
72
|
+
if (!importPath) return await reject(c, { component: "Enter the story file path" });
|
|
73
|
+
try {
|
|
74
|
+
await designModel().setComponentLink(project.id, importPath, asString(form.get("url")) ?? "");
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof InvalidDesignUrlError) return await reject(c, { component: error.message });
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
return done(c, project.slug, "Component mapped");
|
|
80
|
+
}
|
|
81
|
+
async function handleRemoveLink(c) {
|
|
82
|
+
const project = await adminProject(c);
|
|
83
|
+
await designModel().removeLink(project.id, c.req.param("id") ?? "");
|
|
84
|
+
return done(c, project.slug, "Link removed");
|
|
85
|
+
}
|
|
86
|
+
async function handleSetToken(c) {
|
|
87
|
+
const project = await adminProject(c);
|
|
88
|
+
const provider = providerOf(c);
|
|
89
|
+
const token = asString((await c.req.formData()).get("token"));
|
|
90
|
+
if (!token) return await reject(c, { [`token:${provider.metadata.kind}`]: "Token is required" });
|
|
91
|
+
await credentialModel().set(project.id, provider.metadata.kind, token);
|
|
92
|
+
return done(c, project.slug, `${provider.metadata.name} token saved`);
|
|
93
|
+
}
|
|
94
|
+
async function handleRemoveToken(c) {
|
|
95
|
+
const project = await adminProject(c);
|
|
96
|
+
const provider = providerOf(c);
|
|
97
|
+
await credentialModel().remove(project.id, provider.metadata.kind);
|
|
98
|
+
return done(c, project.slug, `${provider.metadata.name} token removed`);
|
|
99
|
+
}
|
|
100
|
+
export { registerDesignSettings };
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=settings-design.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-design.mjs","names":[],"sources":["../../src/routers/settings-design.ts"],"sourcesContent":["import { InvalidDesignUrlError, type DesignProvider } from \"@storyshelf/core/adapter/design\";\nimport { DesignCredentialModel, DesignModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { notFound, resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { asString, renderSettingsPage } from \"./settings.handlers.ts\";\n\n/** Design settings (project design files, component mapping, provider credentials). */\nexport function registerDesignSettings(app: ShelfRouter): void {\n const base = \"/projects/:slug/settings/design\";\n app.get(base, async (c) => c.html(await renderSettingsPage(c, \"design\")));\n app.post(`${base}/files`, handleAddFile);\n app.post(`${base}/files/:id/delete`, handleRemoveLink);\n app.post(`${base}/components`, handleMapComponent);\n app.post(`${base}/components/:id/delete`, handleRemoveLink);\n app.post(`${base}/:provider/token`, handleSetToken);\n app.post(`${base}/:provider/token/delete`, handleRemoveToken);\n}\n\n/**\n * Every design-settings mutation writes credentials or mappings: require a\n * project admin server-side (the forms are only hidden from others, never trusted).\n * With auth disabled the app has no roles and everyone may administer, as elsewhere.\n */\nasync function adminProject(c: Context): Promise<Project> {\n return await resolveAuthorizedProject(c, c.req.param(\"slug\") ?? \"\", \"admin\");\n}\n\nfunction designModel(): DesignModel {\n const { db, design } = getStore();\n return new DesignModel(db, design);\n}\n\nfunction credentialModel(): DesignCredentialModel {\n const { db, config } = getStore();\n return new DesignCredentialModel(db, credentialKeys(config));\n}\n\n/** The configured provider addressed by `:provider`, or a 404. */\nfunction providerOf(c: Context): DesignProvider {\n const provider = getStore().design.find(\n (candidate) => candidate.metadata.kind === c.req.param(\"provider\"),\n );\n if (!provider) notFound(\"Unknown design provider\");\n return provider;\n}\n\nasync function reject(c: Context, errors: Record<string, string>): Promise<Response> {\n return c.html((await renderSettingsPage(c, \"design\", { errors })) ?? \"\", 400);\n}\n\nfunction done(c: Context, slug: string, message: string): Response {\n flash(c, message);\n return hxRedirect(c, `/projects/${slug}/settings/design`);\n}\n\nasync function handleAddFile(c: Context): Promise<Response> {\n const project = await adminProject(c);\n const form = await c.req.formData();\n const label = asString(form.get(\"label\")) ?? \"\";\n try {\n await designModel().addProjectLink(project.id, { label, url: asString(form.get(\"url\")) ?? \"\" });\n } catch (error) {\n if (error instanceof InvalidDesignUrlError) return await reject(c, { file: error.message });\n throw error;\n }\n return done(c, project.slug, \"Design file added\");\n}\n\nasync function handleMapComponent(c: Context): Promise<Response> {\n const project = await adminProject(c);\n const form = await c.req.formData();\n const importPath = asString(form.get(\"importPath\")) ?? \"\";\n if (!importPath) return await reject(c, { component: \"Enter the story file path\" });\n try {\n await designModel().setComponentLink(project.id, importPath, asString(form.get(\"url\")) ?? \"\");\n } catch (error) {\n if (error instanceof InvalidDesignUrlError) {\n return await reject(c, { component: error.message });\n }\n throw error;\n }\n return done(c, project.slug, \"Component mapped\");\n}\n\nasync function handleRemoveLink(c: Context): Promise<Response> {\n const project = await adminProject(c);\n await designModel().removeLink(project.id, c.req.param(\"id\") ?? \"\");\n return done(c, project.slug, \"Link removed\");\n}\n\nasync function handleSetToken(c: Context): Promise<Response> {\n const project = await adminProject(c);\n const provider = providerOf(c);\n const token = asString((await c.req.formData()).get(\"token\"));\n if (!token) return await reject(c, { [`token:${provider.metadata.kind}`]: \"Token is required\" });\n await credentialModel().set(project.id, provider.metadata.kind, token);\n return done(c, project.slug, `${provider.metadata.name} token saved`);\n}\n\nasync function handleRemoveToken(c: Context): Promise<Response> {\n const project = await adminProject(c);\n const provider = providerOf(c);\n await credentialModel().remove(project.id, provider.metadata.kind);\n return done(c, project.slug, `${provider.metadata.name} token removed`);\n}\n"],"mappings":"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"}
|
|
@@ -2,18 +2,22 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
|
+
import { resolveAuthorizedProject } from "./helpers.mjs";
|
|
6
|
+
import { assertTargetsSelectable } from "../targets.mjs";
|
|
5
7
|
import { hxRedirect } from "./htmx.mjs";
|
|
6
8
|
import { flash } from "./flash.mjs";
|
|
9
|
+
import { runSwitchOffPass } from "./review-switch-off.mjs";
|
|
7
10
|
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
11
|
+
import { resolveBrowsers } from "@storyshelf/core/capture";
|
|
8
12
|
import { ProjectModel } from "@storyshelf/core/models";
|
|
9
|
-
|
|
10
|
-
//#region src/routers/settings-general.ts
|
|
13
|
+
// src/routers/settings-general.ts
|
|
11
14
|
/** General project settings (name, repo, thresholds) and danger-zone delete. */
|
|
12
15
|
function registerGeneralSettings(app) {
|
|
13
16
|
app.get("/projects/:slug/settings", async (c) => c.html(await renderSettingsPage(c, "general")));
|
|
14
17
|
app.get("/projects/:slug/settings/tests", async (c) => c.html(await renderSettingsPage(c, "tests")));
|
|
15
18
|
app.post("/projects/:slug/settings", handleGeneralUpdate);
|
|
16
19
|
app.post("/projects/:slug/settings/tests", handleTestsUpdate);
|
|
20
|
+
app.post("/projects/:slug/settings/review", handleReviewUpdate);
|
|
17
21
|
app.post("/projects/:slug/delete", handleDeleteProject);
|
|
18
22
|
}
|
|
19
23
|
/** Read the general-settings form fields (name validated by the caller). */
|
|
@@ -33,15 +37,18 @@ async function handleGeneralUpdate(c) {
|
|
|
33
37
|
const form = await c.req.formData();
|
|
34
38
|
const name = form.get("name");
|
|
35
39
|
if (typeof name !== "string" || name.trim() === "") return c.html(await renderSettingsPage(c, "general", { errors: { name: "Name is required" } }) ?? "", 400);
|
|
36
|
-
|
|
40
|
+
return await persistGeneralUpdate(c, project, slug, {
|
|
37
41
|
...readGeneralFields(form),
|
|
38
42
|
name: name.trim()
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Refuse, with 400 and the reason, a browser the runner cannot render (ADR 0007, decision 7). */
|
|
46
|
+
async function assertBrowserChoice(project, browser) {
|
|
47
|
+
if (browser !== void 0) await assertTargetsSelectable([browser], resolveBrowsers(project));
|
|
42
48
|
}
|
|
43
49
|
async function persistGeneralUpdate(c, project, slug, fields) {
|
|
44
50
|
try {
|
|
51
|
+
await assertBrowserChoice(project, fields.browser);
|
|
45
52
|
await new ProjectModel(getStore().db).update(project.id, {
|
|
46
53
|
name: fields.name,
|
|
47
54
|
gitRepository: fields.gitRepository ?? void 0,
|
|
@@ -49,7 +56,8 @@ async function persistGeneralUpdate(c, project, slug, fields) {
|
|
|
49
56
|
pixelThreshold: fields.pixelThreshold ? Number(fields.pixelThreshold) : void 0,
|
|
50
57
|
maxDiffRatio: fields.maxDiffRatio ? Number(fields.maxDiffRatio) : void 0,
|
|
51
58
|
publicBranchRegex: fields.publicBranchRegex === "" ? null : fields.publicBranchRegex ?? void 0,
|
|
52
|
-
browser: fields.browser ?? void 0
|
|
59
|
+
browser: fields.browser ?? void 0,
|
|
60
|
+
...fields.browser && fields.browser !== project.browser ? { browsers: null } : {}
|
|
53
61
|
});
|
|
54
62
|
flash(c, "Project settings saved");
|
|
55
63
|
return hxRedirect(c, `/projects/${slug}/settings`);
|
|
@@ -61,16 +69,22 @@ async function persistGeneralUpdate(c, project, slug, fields) {
|
|
|
61
69
|
async function handleTestsUpdate(c) {
|
|
62
70
|
const slug = c.req.param("slug") ?? "";
|
|
63
71
|
const project = await findProject(slug);
|
|
64
|
-
const { executePlay, playTimeoutMs } = readTestsFields(await c.req.formData());
|
|
72
|
+
const { executePlay, runA11y, runTests, playTimeoutMs } = readTestsFields(await c.req.formData());
|
|
65
73
|
const rangeError = playTimeoutError(playTimeoutMs);
|
|
66
74
|
if (rangeError) return c.html(await renderSettingsPage(c, "tests", { globalError: rangeError }) ?? "", 400);
|
|
67
|
-
return await persistTestsUpdate(c, project, slug,
|
|
75
|
+
return await persistTestsUpdate(c, project, slug, {
|
|
76
|
+
executePlay,
|
|
77
|
+
runA11y,
|
|
78
|
+
runTests
|
|
79
|
+
}, playTimeoutMs);
|
|
68
80
|
}
|
|
69
81
|
/** Read the tests-tab form fields. */
|
|
70
82
|
function readTestsFields(form) {
|
|
71
83
|
const playTimeoutMsRaw = asString(form.get("playTimeoutMs"));
|
|
72
84
|
return {
|
|
73
85
|
executePlay: form.get("executePlay") === "true",
|
|
86
|
+
runA11y: form.get("runA11y") === "true",
|
|
87
|
+
runTests: form.get("runTests") === "true",
|
|
74
88
|
playTimeoutMs: playTimeoutMsRaw ? Number(playTimeoutMsRaw) : void 0
|
|
75
89
|
};
|
|
76
90
|
}
|
|
@@ -79,10 +93,10 @@ function playTimeoutError(playTimeoutMs) {
|
|
|
79
93
|
if (playTimeoutMs !== void 0 && (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1e3 || playTimeoutMs > 3e4)) return "Play timeout must be between 1000 and 30000";
|
|
80
94
|
return null;
|
|
81
95
|
}
|
|
82
|
-
async function persistTestsUpdate(c, project, slug,
|
|
96
|
+
async function persistTestsUpdate(c, project, slug, switches, playTimeoutMs) {
|
|
83
97
|
try {
|
|
84
98
|
await new ProjectModel(getStore().db).update(project.id, {
|
|
85
|
-
|
|
99
|
+
...switches,
|
|
86
100
|
playTimeoutMs: playTimeoutMs ?? void 0
|
|
87
101
|
});
|
|
88
102
|
flash(c, "Test settings saved");
|
|
@@ -98,7 +112,37 @@ async function handleDeleteProject(c) {
|
|
|
98
112
|
flash(c, `Deleted ${project.name}`);
|
|
99
113
|
return hxRedirect(c, "/projects");
|
|
100
114
|
}
|
|
101
|
-
|
|
115
|
+
/** Largest approval count the form and the API accept. */
|
|
116
|
+
const MAX_APPROVALS = 20;
|
|
117
|
+
/**
|
|
118
|
+
* Review settings (ADR 0013, decision 1): the switch and the approvals a build needs. Project
|
|
119
|
+
* admins only. The route is classed `hosting`, not `review`: a review route answers 404 while
|
|
120
|
+
* review is off, and this is the form that turns it back on. Turning review off accepts the
|
|
121
|
+
* project's pending builds that no capture worked on, in one pass.
|
|
122
|
+
*/
|
|
123
|
+
async function handleReviewUpdate(c) {
|
|
124
|
+
const slug = c.req.param("slug") ?? "";
|
|
125
|
+
const project = await resolveAuthorizedProject(c, slug, "admin");
|
|
126
|
+
const form = await c.req.formData();
|
|
127
|
+
const approvals = approvalsOf(form, project);
|
|
128
|
+
if (approvals === null) return c.html(await renderSettingsPage(c, "general", { errors: { reviewApprovals: `Enter a whole number from 1 to ${MAX_APPROVALS}` } }) ?? "", 400);
|
|
129
|
+
const updated = await new ProjectModel(getStore().db).update(project.id, {
|
|
130
|
+
reviewEnabled: form.get("reviewEnabled") === "true",
|
|
131
|
+
reviewApprovals: approvals
|
|
132
|
+
});
|
|
133
|
+
const accepted = await runSwitchOffPass(project, updated);
|
|
134
|
+
flash(c, reviewSavedMessage(accepted));
|
|
135
|
+
return hxRedirect(c, `/projects/${slug}/settings`);
|
|
136
|
+
}
|
|
137
|
+
/** The approvals count of the form (the current one when blank); null when it is not 1 to 20. */
|
|
138
|
+
function approvalsOf(form, project) {
|
|
139
|
+
const raw = asString(form.get("reviewApprovals"));
|
|
140
|
+
const approvals = raw ? Number(raw) : project.reviewApprovals ?? 1;
|
|
141
|
+
return Number.isInteger(approvals) && approvals >= 1 && approvals <= MAX_APPROVALS ? approvals : null;
|
|
142
|
+
}
|
|
143
|
+
function reviewSavedMessage(accepted) {
|
|
144
|
+
return accepted > 0 ? `Review settings saved; accepted ${accepted} pending build${accepted === 1 ? "" : "s"}` : "Review settings saved";
|
|
145
|
+
}
|
|
102
146
|
export { registerGeneralSettings };
|
|
103
147
|
|
|
104
148
|
//# sourceMappingURL=settings-general.mjs.map
|
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"settings-general.mjs","names":[],"sources":["../../src/routers/settings-general.ts"],"sourcesContent":["import { resolveBrowsers } from \"@storyshelf/core/capture\";\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 { assertTargetsSelectable } from \"../targets.ts\";\nimport { flash } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\nimport { runSwitchOffPass } from \"./review-switch-off.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/settings/review\", handleReviewUpdate);\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 return await persistGeneralUpdate(c, project, slug, fields);\n}\n\n/** Refuse, with 400 and the reason, a browser the runner cannot render (ADR 0007, decision 7). */\nasync function assertBrowserChoice(project: Project, browser: string | undefined): Promise<void> {\n if (browser !== undefined) {\n await assertTargetsSelectable([browser], resolveBrowsers(project));\n }\n}\n\nasync function persistGeneralUpdate(\n c: Context,\n project: Project,\n slug: string,\n fields: GeneralFields,\n): Promise<Response> {\n try {\n await assertBrowserChoice(project, fields.browser);\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 // Picking a different single engine returns the project to one-browser capture.\n ...(fields.browser && fields.browser !== project.browser ? { browsers: null } : {}),\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, runA11y, runTests, 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(\n c,\n project,\n slug,\n { executePlay, runA11y, runTests },\n playTimeoutMs,\n );\n}\n\n/** Read the tests-tab form fields. */\nfunction readTestsFields(form: FormData): {\n executePlay: boolean;\n runA11y: boolean;\n runTests: boolean;\n playTimeoutMs: number | undefined;\n} {\n const playTimeoutMsRaw = asString(form.get(\"playTimeoutMs\"));\n return {\n executePlay: form.get(\"executePlay\") === \"true\",\n runA11y: form.get(\"runA11y\") === \"true\",\n runTests: form.get(\"runTests\") === \"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 switches: { executePlay: boolean; runA11y: boolean; runTests: boolean },\n playTimeoutMs: number | undefined,\n): Promise<Response> {\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n ...switches,\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\n/** Largest approval count the form and the API accept. */\nconst MAX_APPROVALS = 20;\n\n/**\n * Review settings (ADR 0013, decision 1): the switch and the approvals a build needs. Project\n * admins only. The route is classed `hosting`, not `review`: a review route answers 404 while\n * review is off, and this is the form that turns it back on. Turning review off accepts the\n * project's pending builds that no capture worked on, in one pass.\n */\nasync function handleReviewUpdate(c: Context): Promise<Response> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const project = await resolveAuthorizedProject(c, slug, \"admin\");\n const form = await c.req.formData();\n const approvals = approvalsOf(form, project);\n if (approvals === null) {\n return c.html(\n (await renderSettingsPage(c, \"general\", {\n errors: { reviewApprovals: `Enter a whole number from 1 to ${MAX_APPROVALS}` },\n })) ?? \"\",\n 400,\n );\n }\n const updated = await new ProjectModel(getStore().db).update(project.id, {\n reviewEnabled: form.get(\"reviewEnabled\") === \"true\",\n reviewApprovals: approvals,\n });\n const accepted = await runSwitchOffPass(project, updated);\n flash(c, reviewSavedMessage(accepted));\n return hxRedirect(c, `/projects/${slug}/settings`);\n}\n\n/** The approvals count of the form (the current one when blank); null when it is not 1 to 20. */\nfunction approvalsOf(form: FormData, project: Project): number | null {\n const raw = asString(form.get(\"reviewApprovals\"));\n const approvals = raw ? Number(raw) : (project.reviewApprovals ?? 1);\n return Number.isInteger(approvals) && approvals >= 1 && approvals <= MAX_APPROVALS\n ? approvals\n : null;\n}\n\nfunction reviewSavedMessage(accepted: number): string {\n return accepted > 0\n ? `Review settings saved; accepted ${accepted} pending build${accepted === 1 ? \"\" : \"s\"}`\n : \"Review settings saved\";\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { flash } from "./flash.mjs";
|
|
6
|
+
import { notificationChannelModel } from "./scoped-models.mjs";
|
|
7
|
+
import { asString, renderSettingsPage } from "./settings.handlers.mjs";
|
|
8
|
+
import { NotificationChannelModel } from "@storyshelf/core/models";
|
|
9
|
+
// src/routers/settings-git-channel.ts
|
|
10
|
+
/** Create a git (`state`) channel from the settings form: repository, status prefix and token. */
|
|
11
|
+
const CONFIG_FIELDS = [
|
|
12
|
+
"owner",
|
|
13
|
+
"repo",
|
|
14
|
+
"host",
|
|
15
|
+
"prefix"
|
|
16
|
+
];
|
|
17
|
+
/** Only the GitHub provider reads these: how it shows a build and the App that posts check runs. */
|
|
18
|
+
const GITHUB_FIELDS = [
|
|
19
|
+
"mode",
|
|
20
|
+
"appId",
|
|
21
|
+
"installationId"
|
|
22
|
+
];
|
|
23
|
+
/** The provider config from the form; blank optional fields are left out. */
|
|
24
|
+
function gitConfig(provider, form) {
|
|
25
|
+
const config = {};
|
|
26
|
+
const fields = provider === "github" ? [...CONFIG_FIELDS, ...GITHUB_FIELDS] : CONFIG_FIELDS;
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
const value = asString(form.get(field));
|
|
29
|
+
if (value) config[field] = value;
|
|
30
|
+
}
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
/** Render the tab again with field errors (HTTP 400). */
|
|
34
|
+
async function formError(c, errors) {
|
|
35
|
+
return c.html(await renderSettingsPage(c, "notifications", { errors }) ?? "", 400);
|
|
36
|
+
}
|
|
37
|
+
/** Map a failed validation to the form fields it concerns. */
|
|
38
|
+
function fieldErrors(secretField, checked) {
|
|
39
|
+
if (checked.field === "secret") return { [secretField]: checked.message };
|
|
40
|
+
if (checked.field === "provider") return { gitProvider: checked.message };
|
|
41
|
+
const errors = {};
|
|
42
|
+
for (const issue of checked.issues ?? []) errors[issue.path] ??= issue.message;
|
|
43
|
+
return Object.keys(errors).length > 0 ? errors : { gitProvider: checked.message };
|
|
44
|
+
}
|
|
45
|
+
/** Validate and store a git channel; the token lands in the encrypted secret column. */
|
|
46
|
+
async function createGitChannel(c, project, provider, form) {
|
|
47
|
+
const config = gitConfig(provider, form);
|
|
48
|
+
const checks = config["mode"] === "checks";
|
|
49
|
+
const token = asString(form.get(checks ? "privateKey" : "token"));
|
|
50
|
+
const problem = gitChannelProblem(project, provider, config, {
|
|
51
|
+
token,
|
|
52
|
+
secretField: checks ? "privateKey" : "token"
|
|
53
|
+
});
|
|
54
|
+
if (problem) return await formError(c, problem);
|
|
55
|
+
await notificationChannelModel().create({
|
|
56
|
+
projectId: project.id,
|
|
57
|
+
provider,
|
|
58
|
+
config,
|
|
59
|
+
secret: token
|
|
60
|
+
});
|
|
61
|
+
flash(c, "Git channel added");
|
|
62
|
+
return c.html(await renderSettingsPage(c, "notifications") ?? "", 201);
|
|
63
|
+
}
|
|
64
|
+
/** The form errors for a git channel input, or null when it is valid. */
|
|
65
|
+
function gitChannelProblem(project, provider, config, secret) {
|
|
66
|
+
const { notifiers } = getStore();
|
|
67
|
+
if (!NotificationChannelModel.isStateKind(notifiers, provider)) return { gitProvider: `Unknown git provider: ${provider}` };
|
|
68
|
+
const checked = NotificationChannelModel.validateInput(notifiers, provider, config, {
|
|
69
|
+
projectId: project.id,
|
|
70
|
+
hasSecret: Boolean(secret.token)
|
|
71
|
+
});
|
|
72
|
+
return checked.ok ? null : fieldErrors(secret.secretField, checked);
|
|
73
|
+
}
|
|
74
|
+
export { createGitChannel };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=settings-git-channel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-git-channel.mjs","names":[],"sources":["../../src/routers/settings-git-channel.ts"],"sourcesContent":["/** Create a git (`state`) channel from the settings form: repository, status prefix and token. */\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { notificationChannelModel } from \"./scoped-models.ts\";\nimport { asString, renderSettingsPage } from \"./settings.handlers.ts\";\n\nconst CONFIG_FIELDS = [\"owner\", \"repo\", \"host\", \"prefix\"] as const;\n/** Only the GitHub provider reads these: how it shows a build and the App that posts check runs. */\nconst GITHUB_FIELDS = [\"mode\", \"appId\", \"installationId\"] as const;\n\n/** The provider config from the form; blank optional fields are left out. */\nfunction gitConfig(provider: string, form: FormData): Record<string, string> {\n const config: Record<string, string> = {};\n const fields = provider === \"github\" ? [...CONFIG_FIELDS, ...GITHUB_FIELDS] : CONFIG_FIELDS;\n for (const field of fields) {\n const value = asString(form.get(field));\n if (value) {\n config[field] = value;\n }\n }\n return config;\n}\n\n/** Render the tab again with field errors (HTTP 400). */\nasync function formError(c: Context, errors: Record<string, string>): Promise<Response> {\n return c.html((await renderSettingsPage(c, \"notifications\", { errors })) ?? \"\", 400);\n}\n\n/** Map a failed validation to the form fields it concerns. */\nfunction fieldErrors(\n secretField: \"token\" | \"privateKey\",\n checked: Extract<ReturnType<typeof NotificationChannelModel.validateInput>, { ok: false }>,\n): Record<string, string> {\n if (checked.field === \"secret\") {\n return { [secretField]: checked.message };\n }\n if (checked.field === \"provider\") {\n return { gitProvider: checked.message };\n }\n const errors: Record<string, string> = {};\n for (const issue of checked.issues ?? []) {\n errors[issue.path] ??= issue.message;\n }\n return Object.keys(errors).length > 0 ? errors : { gitProvider: checked.message };\n}\n\n/** Validate and store a git channel; the token lands in the encrypted secret column. */\nexport async function createGitChannel(\n c: Context,\n project: Project,\n provider: string,\n form: FormData,\n): Promise<Response> {\n const config = gitConfig(provider, form);\n // A GitHub App channel keeps its private key where other channels keep a token.\n const checks = config[\"mode\"] === \"checks\";\n const token = asString(form.get(checks ? \"privateKey\" : \"token\"));\n const problem = gitChannelProblem(project, provider, config, {\n token,\n secretField: checks ? \"privateKey\" : \"token\",\n });\n if (problem) {\n return await formError(c, problem);\n }\n await notificationChannelModel().create({\n projectId: project.id,\n provider,\n config,\n secret: token,\n });\n flash(c, \"Git channel added\");\n return c.html((await renderSettingsPage(c, \"notifications\")) ?? \"\", 201);\n}\n\n/** The form errors for a git channel input, or null when it is valid. */\nfunction gitChannelProblem(\n project: Project,\n provider: string,\n config: Record<string, string>,\n secret: { token: string | undefined; secretField: \"token\" | \"privateKey\" },\n): Record<string, string> | null {\n const { notifiers } = getStore();\n if (!NotificationChannelModel.isStateKind(notifiers, provider)) {\n return { gitProvider: `Unknown git provider: ${provider}` };\n }\n const checked = NotificationChannelModel.validateInput(notifiers, provider, config, {\n projectId: project.id,\n hasSecret: Boolean(secret.token),\n });\n return checked.ok ? null : fieldErrors(secret.secretField, checked);\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,gBAAgB;CAAC;CAAS;CAAQ;CAAQ;AAAQ;;AAExD,MAAM,gBAAgB;CAAC;CAAQ;CAAS;AAAgB;;AAGxD,SAAS,UAAU,UAAkB,MAAwC;CAC3E,MAAM,SAAiC,CAAC;CACxC,MAAM,SAAS,aAAa,WAAW,CAAC,GAAG,eAAe,GAAG,aAAa,IAAI;CAC9E,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,SAAS,KAAK,IAAI,KAAK,CAAC;EACtC,IAAI,OACF,OAAO,SAAS;CAEpB;CACA,OAAO;AACT;;AAGA,eAAe,UAAU,GAAY,QAAmD;CACtF,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,iBAAiB,EAAE,OAAO,CAAC,KAAM,IAAI,GAAG;AACrF;;AAGA,SAAS,YACP,aACA,SACwB;CACxB,IAAI,QAAQ,UAAU,UACpB,OAAO,GAAG,cAAc,QAAQ,QAAQ;CAE1C,IAAI,QAAQ,UAAU,YACpB,OAAO,EAAE,aAAa,QAAQ,QAAQ;CAExC,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,SAAS,QAAQ,UAAU,CAAC,GACrC,OAAO,MAAM,UAAU,MAAM;CAE/B,OAAO,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,SAAS,EAAE,aAAa,QAAQ,QAAQ;AAClF;;AAGA,eAAsB,iBACpB,GACA,SACA,UACA,MACmB;CACnB,MAAM,SAAS,UAAU,UAAU,IAAI;CAEvC,MAAM,SAAS,OAAO,YAAY;CAClC,MAAM,QAAQ,SAAS,KAAK,IAAI,SAAS,eAAe,OAAO,CAAC;CAChE,MAAM,UAAU,kBAAkB,SAAS,UAAU,QAAQ;EAC3D;EACA,aAAa,SAAS,eAAe;CACvC,CAAC;CACD,IAAI,SACF,OAAO,MAAM,UAAU,GAAG,OAAO;CAEnC,MAAM,yBAAyB,CAAC,CAAC,OAAO;EACtC,WAAW,QAAQ;EACnB;EACA;EACA,QAAQ;CACV,CAAC;CACD,MAAM,GAAG,mBAAmB;CAC5B,OAAO,EAAE,KAAM,MAAM,mBAAmB,GAAG,eAAe,KAAM,IAAI,GAAG;AACzE;;AAGA,SAAS,kBACP,SACA,UACA,QACA,QAC+B;CAC/B,MAAM,EAAE,cAAc,SAAS;CAC/B,IAAI,CAAC,yBAAyB,YAAY,WAAW,QAAQ,GAC3D,OAAO,EAAE,aAAa,yBAAyB,WAAW;CAE5D,MAAM,UAAU,yBAAyB,cAAc,WAAW,UAAU,QAAQ;EAClF,WAAW,QAAQ;EACnB,WAAW,QAAQ,OAAO,KAAK;CACjC,CAAC;CACD,OAAO,QAAQ,KAAK,OAAO,YAAY,OAAO,aAAa,OAAO;AACpE"}
|
|
@@ -6,11 +6,12 @@ import { hxRedirect } from "./htmx.mjs";
|
|
|
6
6
|
import { flash } from "./flash.mjs";
|
|
7
7
|
import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
|
|
8
8
|
import { LabelModel } from "@storyshelf/core/models";
|
|
9
|
-
|
|
9
|
+
// src/routers/settings-labels.ts
|
|
10
10
|
/** Label-type settings (create custom types, delete removable ones). */
|
|
11
11
|
function registerLabelSettings(app) {
|
|
12
|
-
app.get("/projects/:slug/settings/labels", async (c) => c.html(await renderSettingsPage(c, "labels")));
|
|
12
|
+
app.get("/projects/:slug/settings/labels", async (c) => c.html(await renderSettingsPage(c, "labels", { editKey: c.req.query("edit") })));
|
|
13
13
|
app.post("/projects/:slug/settings/labels", handleCreateLabelType);
|
|
14
|
+
app.post("/projects/:slug/settings/labels/:key", handleUpdateLabelType);
|
|
14
15
|
app.post("/projects/:slug/settings/labels/:key/delete", handleDeleteLabelType);
|
|
15
16
|
}
|
|
16
17
|
async function handleCreateLabelType(c) {
|
|
@@ -36,6 +37,31 @@ async function persistLabelType(c, projectId, projectSlug, key, labelName, linkT
|
|
|
36
37
|
return c.html(await renderSettingsPage(c, "labels", { globalError: message }) ?? "", 400);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
async function handleUpdateLabelType(c) {
|
|
41
|
+
const project = await findProject(c.req.param("slug") ?? "");
|
|
42
|
+
const key = c.req.param("key") ?? "";
|
|
43
|
+
const form = await c.req.formData();
|
|
44
|
+
const name = asString(form.get("labelName"));
|
|
45
|
+
if (!name) return c.html(await renderSettingsPage(c, "labels", {
|
|
46
|
+
globalError: "Name is required",
|
|
47
|
+
editKey: key
|
|
48
|
+
}) ?? "", 400);
|
|
49
|
+
const template = asString(form.get("linkTemplate"));
|
|
50
|
+
return await persistLabelUpdate(c, project, key, {
|
|
51
|
+
name,
|
|
52
|
+
linkTemplate: template === void 0 || template === "" ? null : template
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function persistLabelUpdate(c, project, key, input) {
|
|
56
|
+
try {
|
|
57
|
+
if (!await new LabelModel(getStore().db).updateType(project.id, key, input)) throw new Error("Label type not found");
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : "Failed to update label";
|
|
60
|
+
return c.html(await renderSettingsPage(c, "labels", { globalError: message }) ?? "", 400);
|
|
61
|
+
}
|
|
62
|
+
flash(c, "Label type saved");
|
|
63
|
+
return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
|
|
64
|
+
}
|
|
39
65
|
async function handleDeleteLabelType(c) {
|
|
40
66
|
const project = await findProject(c.req.param("slug") ?? "");
|
|
41
67
|
try {
|
|
@@ -46,7 +72,6 @@ async function handleDeleteLabelType(c) {
|
|
|
46
72
|
flash(c, "Label type deleted");
|
|
47
73
|
return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
|
|
48
74
|
}
|
|
49
|
-
//#endregion
|
|
50
75
|
export { registerLabelSettings };
|
|
51
76
|
|
|
52
77
|
//# sourceMappingURL=settings-labels.mjs.map
|