@stacksjs/defaults 0.70.206 → 0.70.208
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/TODO.md +21 -0
- package/ai/AGENTS.md +79 -0
- package/ai/README.md +45 -0
- package/ai/claude/launch.json +12 -0
- package/ai/skills/stacks-actions/SKILL.md +128 -0
- package/ai/skills/stacks-ai/SKILL.md +276 -0
- package/ai/skills/stacks-alias/SKILL.md +94 -0
- package/ai/skills/stacks-analytics/SKILL.md +91 -0
- package/ai/skills/stacks-api/SKILL.md +738 -0
- package/ai/skills/stacks-arrays/SKILL.md +277 -0
- package/ai/skills/stacks-auth/SKILL.md +422 -0
- package/ai/skills/stacks-auto-imports/SKILL.md +138 -0
- package/ai/skills/stacks-brandkit/SKILL.md +811 -0
- package/ai/skills/stacks-browse/SKILL.md +127 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +492 -0
- package/ai/skills/stacks-browser/SKILL.md +145 -0
- package/ai/skills/stacks-buddy/SKILL.md +676 -0
- package/ai/skills/stacks-build/SKILL.md +163 -0
- package/ai/skills/stacks-cache/SKILL.md +125 -0
- package/ai/skills/stacks-calendar/SKILL.md +113 -0
- package/ai/skills/stacks-chat/SKILL.md +371 -0
- package/ai/skills/stacks-cli/SKILL.md +112 -0
- package/ai/skills/stacks-cloud/SKILL.md +192 -0
- package/ai/skills/stacks-cms/SKILL.md +217 -0
- package/ai/skills/stacks-collections/SKILL.md +82 -0
- package/ai/skills/stacks-commerce/SKILL.md +114 -0
- package/ai/skills/stacks-composables/SKILL.md +161 -0
- package/ai/skills/stacks-config/SKILL.md +144 -0
- package/ai/skills/stacks-configuration/SKILL.md +154 -0
- package/ai/skills/stacks-cron/SKILL.md +123 -0
- package/ai/skills/stacks-crosswind/SKILL.md +190 -0
- package/ai/skills/stacks-dashboard/SKILL.md +173 -0
- package/ai/skills/stacks-database/SKILL.md +286 -0
- package/ai/skills/stacks-datetime/SKILL.md +269 -0
- package/ai/skills/stacks-dependencies/SKILL.md +111 -0
- package/ai/skills/stacks-deploy/SKILL.md +102 -0
- package/ai/skills/stacks-design-brutalist/SKILL.md +110 -0
- package/ai/skills/stacks-design-minimalist/SKILL.md +115 -0
- package/ai/skills/stacks-design-output/SKILL.md +55 -0
- package/ai/skills/stacks-design-soft/SKILL.md +156 -0
- package/ai/skills/stacks-design-taste/SKILL.md +1012 -0
- package/ai/skills/stacks-design-taste-codex/SKILL.md +294 -0
- package/ai/skills/stacks-desktop/SKILL.md +78 -0
- package/ai/skills/stacks-development/SKILL.md +298 -0
- package/ai/skills/stacks-dns/SKILL.md +149 -0
- package/ai/skills/stacks-docs/SKILL.md +109 -0
- package/ai/skills/stacks-email/SKILL.md +236 -0
- package/ai/skills/stacks-enums/SKILL.md +100 -0
- package/ai/skills/stacks-env/SKILL.md +181 -0
- package/ai/skills/stacks-error-handling/SKILL.md +137 -0
- package/ai/skills/stacks-events/SKILL.md +228 -0
- package/ai/skills/stacks-faker/SKILL.md +158 -0
- package/ai/skills/stacks-git/SKILL.md +158 -0
- package/ai/skills/stacks-guard/SKILL.md +98 -0
- package/ai/skills/stacks-health/SKILL.md +75 -0
- package/ai/skills/stacks-http/SKILL.md +316 -0
- package/ai/skills/stacks-i18n/SKILL.md +198 -0
- package/ai/skills/stacks-image-to-code/SKILL.md +629 -0
- package/ai/skills/stacks-imagegen-mobile/SKILL.md +1474 -0
- package/ai/skills/stacks-imagegen-web/SKILL.md +998 -0
- package/ai/skills/stacks-investigate/SKILL.md +119 -0
- package/ai/skills/stacks-jobs/SKILL.md +136 -0
- package/ai/skills/stacks-lint/SKILL.md +184 -0
- package/ai/skills/stacks-listeners/SKILL.md +99 -0
- package/ai/skills/stacks-logging/SKILL.md +78 -0
- package/ai/skills/stacks-mail/SKILL.md +120 -0
- package/ai/skills/stacks-middleware/SKILL.md +249 -0
- package/ai/skills/stacks-migrations/SKILL.md +105 -0
- package/ai/skills/stacks-models/SKILL.md +267 -0
- package/ai/skills/stacks-new-feature/SKILL.md +206 -0
- package/ai/skills/stacks-notifications/SKILL.md +210 -0
- package/ai/skills/stacks-objects/SKILL.md +205 -0
- package/ai/skills/stacks-office-hours/SKILL.md +134 -0
- package/ai/skills/stacks-orm/SKILL.md +342 -0
- package/ai/skills/stacks-path/SKILL.md +118 -0
- package/ai/skills/stacks-payments/SKILL.md +373 -0
- package/ai/skills/stacks-plan-review/SKILL.md +152 -0
- package/ai/skills/stacks-plugins/SKILL.md +149 -0
- package/ai/skills/stacks-push/SKILL.md +376 -0
- package/ai/skills/stacks-query-builder/SKILL.md +128 -0
- package/ai/skills/stacks-queue/SKILL.md +559 -0
- package/ai/skills/stacks-realtime/SKILL.md +442 -0
- package/ai/skills/stacks-redesign/SKILL.md +182 -0
- package/ai/skills/stacks-registry/SKILL.md +58 -0
- package/ai/skills/stacks-repl/SKILL.md +80 -0
- package/ai/skills/stacks-retro/SKILL.md +122 -0
- package/ai/skills/stacks-review/SKILL.md +135 -0
- package/ai/skills/stacks-router/SKILL.md +231 -0
- package/ai/skills/stacks-routes/SKILL.md +105 -0
- package/ai/skills/stacks-scaffolding/SKILL.md +162 -0
- package/ai/skills/stacks-scheduler/SKILL.md +363 -0
- package/ai/skills/stacks-search-engine/SKILL.md +112 -0
- package/ai/skills/stacks-security/SKILL.md +129 -0
- package/ai/skills/stacks-security-audit/SKILL.md +128 -0
- package/ai/skills/stacks-server/SKILL.md +458 -0
- package/ai/skills/stacks-shell/SKILL.md +62 -0
- package/ai/skills/stacks-slug/SKILL.md +87 -0
- package/ai/skills/stacks-sms/SKILL.md +385 -0
- package/ai/skills/stacks-socials/SKILL.md +303 -0
- package/ai/skills/stacks-storage/SKILL.md +576 -0
- package/ai/skills/stacks-strings/SKILL.md +416 -0
- package/ai/skills/stacks-stx/SKILL.md +176 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +22 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +290 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +4 -0
- package/ai/skills/stacks-technical-diagrams/assets/template.html +1252 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +289 -0
- package/ai/skills/stacks-technical-diagrams/bunfig.toml +1 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +64 -0
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +88 -0
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +75 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +70 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +41 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +62 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +371 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +295 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +91 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +384 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +84 -0
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +257 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +13 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +230 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +88 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +40 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +103 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +481 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +68 -0
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +121 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +43 -0
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +221 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +241 -0
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +186 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +332 -0
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +297 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +28 -0
- package/ai/skills/stacks-testing/SKILL.md +129 -0
- package/ai/skills/stacks-tunnel/SKILL.md +125 -0
- package/ai/skills/stacks-types/SKILL.md +178 -0
- package/ai/skills/stacks-ui/SKILL.md +246 -0
- package/ai/skills/stacks-utils/SKILL.md +444 -0
- package/ai/skills/stacks-validation/SKILL.md +371 -0
- package/ai/skills/stacks-whois/SKILL.md +315 -0
- package/assets/world-map-data.ts +121 -0
- package/bootstrap.ts +114 -0
- package/docs/api-examples.md +46 -0
- package/docs/index.md +24 -0
- package/docs/markdown-examples.md +87 -0
- package/functions/analytics.ts +97 -0
- package/functions/api.ts +143 -0
- package/functions/auth.ts +208 -0
- package/functions/billing/payments.ts +149 -0
- package/functions/blog.ts +120 -0
- package/functions/cms/categorizables.ts +107 -0
- package/functions/cms/pages.ts +108 -0
- package/functions/cms/posts.ts +100 -0
- package/functions/cms/taggables.ts +102 -0
- package/functions/commerce/coupons.ts +128 -0
- package/functions/commerce/customers.ts +128 -0
- package/functions/commerce/gift-cards.ts +119 -0
- package/functions/commerce/orders.ts +112 -0
- package/functions/commerce/payments.ts +119 -0
- package/functions/commerce/products/categories.ts +123 -0
- package/functions/commerce/products/manufacturers.ts +123 -0
- package/functions/commerce/products/products.ts +123 -0
- package/functions/commerce/products/reviews.ts +123 -0
- package/functions/commerce/products/units.ts +123 -0
- package/functions/commerce/products/variants.ts +123 -0
- package/functions/commerce/shippings/delivery-routes.ts +123 -0
- package/functions/commerce/shippings/digital-deliveries.ts +119 -0
- package/functions/commerce/shippings/drivers.ts +123 -0
- package/functions/commerce/shippings/license-keys.ts +123 -0
- package/functions/commerce/shippings/shipping-methods.ts +123 -0
- package/functions/commerce/shippings/shipping-rates.ts +119 -0
- package/functions/commerce/shippings/shipping-zones.ts +119 -0
- package/functions/commerce/taxes.ts +119 -0
- package/functions/commerce/waitlist/products.ts +123 -0
- package/functions/commerce/waitlist/restaurants.ts +123 -0
- package/functions/craft.ts +107 -0
- package/functions/dashboard.ts +153 -0
- package/functions/inbox.ts +95 -0
- package/functions/jobs.ts +86 -0
- package/functions/monitoring/errors.ts +295 -0
- package/functions/queue.ts +111 -0
- package/functions/realtime/websockets.ts +96 -0
- package/functions/realtime.ts +52 -0
- package/functions/releases.ts +49 -0
- package/functions/search.ts +126 -0
- package/functions/toasts.ts +95 -0
- package/functions/uploads.ts +216 -0
- package/ide/.gitignore +4 -0
- package/ide/cursor/rules/auto-imports.mdc +574 -0
- package/ide/cursor/rules/code-style.mdc +12 -0
- package/ide/cursor/rules/documentation.mdc +9 -0
- package/ide/cursor/rules/error-handling-validation.mdc +11 -0
- package/ide/cursor/rules/key-conventions.mdc +11 -0
- package/ide/cursor/rules/project-structure.mdc +169 -0
- package/ide/cursor/rules/readme.mdc +318 -0
- package/ide/cursor/rules/syntax-formatting.mdc +9 -0
- package/ide/cursor/rules/testing.mdc +10 -0
- package/ide/cursor/rules/typescript.mdc +9 -0
- package/ide/cursor/rules/ui-styling.mdc +0 -0
- package/ide/dictionary.txt +397 -0
- package/ide/jetbrains/.fleet/settings.json +4 -0
- package/ide/jetbrains/.idea/.gitignore +5 -0
- package/ide/jetbrains/.idea/.name +1 -0
- package/ide/jetbrains/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/ide/jetbrains/.idea/jsLinters/eslint.xml +7 -0
- package/ide/jetbrains/.idea/modules.xml +8 -0
- package/ide/jetbrains/.idea/stacks.iml +22 -0
- package/ide/jetbrains/.idea/vcs.xml +6 -0
- package/ide/jetbrains/.idea/webResources.xml +14 -0
- package/ide/jetbrains/IntelliJ IDEA Global Settings +0 -0
- package/ide/jetbrains/installed.txt +4 -0
- package/ide/jetbrains/options/github-copilot.xml +5 -0
- package/ide/jetbrains/options/nodejs.xml +7 -0
- package/ide/jetbrains/options/templates.xml +124 -0
- package/ide/jetbrains/templates/JavaScript.xml +6 -0
- package/ide/jetbrains/templates/Stacks.xml +8 -0
- package/ide/vscode/.vscode/README.md +23 -0
- package/ide/vscode/.vscode/extensions.json +5 -0
- package/ide/vscode/.vscode/settings.json +176 -0
- package/ide/vscode/.vscode/stacks.code-snippets +181 -0
- package/ide/vscode/LICENSE.md +21 -0
- package/ide/vscode/README.md +36 -0
- package/ide/vscode/logo.png +0 -0
- package/ide/vscode/package.json +79 -0
- package/ide/zed/.zed/settings.json +139 -0
- package/lang/README.md +28 -0
- package/lang/de.yml +14 -0
- package/lang/en.yml +15 -0
- package/package.json +19 -3
- package/project/bootstrap +161 -0
- package/project/buddy +175 -0
- package/project/storage/framework/tsconfig.app.json +50 -0
- package/project/storage/framework/tsconfig.base.json +58 -0
- package/routes/core.ts +78 -0
- package/routes/dashboard-api.ts +233 -0
- package/routes/dashboard.ts +739 -0
- package/routes/email.ts +129 -0
- package/stores/[name].stx +16 -0
- package/stores/git.ts +71 -0
- package/stores/payment.ts +474 -0
- package/stores/queue.ts +43 -0
- package/stores/user.ts +29 -0
- package/stx-components-plugin.ts +25 -0
- package/styles/design-tokens.css +597 -0
- package/types/dashboard.ts +70 -0
- package/types/defaults.ts +438 -0
- package/vcs/github/CODE_OF_CONDUCT.md +83 -0
- package/vcs/github/CONTRIBUTING.md +187 -0
- package/vcs/github/EXPLANATIONS.md +41 -0
- package/vcs/github/FUNDING.yml +1 -0
- package/vcs/github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/vcs/github/ISSUE_TEMPLATE/config.yml +10 -0
- package/vcs/github/ISSUE_TEMPLATE/feature_request.yml +44 -0
- package/vcs/github/SECURITY.md +70 -0
- package/vcs/github/renovate.json +5 -0
- package/vcs/github/stale.yml +14 -0
- package/vcs/github/workflows/README.md +16 -0
- package/vcs/github/workflows/ci.yml +114 -0
- package/vcs/github/workflows/export-size.yml +25 -0
- package/vcs/github/workflows/labeler.yml +13 -0
- package/vcs/github/workflows/release.yml +219 -0
- package/views/[...all].stx +8 -0
- package/views/about.md +3 -0
- package/views/auth/login.stx +1 -0
- package/views/auth/signup.stx +85 -0
- package/views/dashboard/.discovered-models.json +542 -0
- package/views/dashboard/AUDIT.md +904 -0
- package/views/dashboard/[...all].stx +89 -0
- package/views/dashboard/access-tokens/index.stx +177 -0
- package/views/dashboard/actions/index.stx +129 -0
- package/views/dashboard/analytics/blog/index.stx +356 -0
- package/views/dashboard/analytics/browsers/index.stx +263 -0
- package/views/dashboard/analytics/commerce/sales/index.stx +329 -0
- package/views/dashboard/analytics/commerce/web/index.stx +379 -0
- package/views/dashboard/analytics/countries/index.stx +289 -0
- package/views/dashboard/analytics/devices/index.stx +305 -0
- package/views/dashboard/analytics/events/index.stx +536 -0
- package/views/dashboard/analytics/index.stx +88 -0
- package/views/dashboard/analytics/marketing/index.stx +315 -0
- package/views/dashboard/analytics/pages/index.stx +271 -0
- package/views/dashboard/analytics/referrers/index.stx +319 -0
- package/views/dashboard/analytics/web/index.stx +356 -0
- package/views/dashboard/buddy/index.stx +198 -0
- package/views/dashboard/ci/index.stx +462 -0
- package/views/dashboard/cloud/index.stx +452 -0
- package/views/dashboard/commands/index.stx +295 -0
- package/views/dashboard/commerce/categories/index.stx +406 -0
- package/views/dashboard/commerce/coupons/index.stx +485 -0
- package/views/dashboard/commerce/customers/index.stx +410 -0
- package/views/dashboard/commerce/dashboard/index.stx +287 -0
- package/views/dashboard/commerce/delivery/delivery-routes.stx +305 -0
- package/views/dashboard/commerce/delivery/digital-delivery.stx +332 -0
- package/views/dashboard/commerce/delivery/drivers.stx +397 -0
- package/views/dashboard/commerce/delivery/index.stx +18 -0
- package/views/dashboard/commerce/delivery/license-keys.stx +380 -0
- package/views/dashboard/commerce/delivery/shipping-methods.stx +346 -0
- package/views/dashboard/commerce/delivery/shipping-rates.stx +338 -0
- package/views/dashboard/commerce/delivery/shipping-zones.stx +302 -0
- package/views/dashboard/commerce/delivery/tabs.ts +25 -0
- package/views/dashboard/commerce/gift-cards/index.stx +561 -0
- package/views/dashboard/commerce/manufacturers/index.stx +308 -0
- package/views/dashboard/commerce/orders/index.stx +247 -0
- package/views/dashboard/commerce/payments/index.stx +535 -0
- package/views/dashboard/commerce/pos/index.stx +653 -0
- package/views/dashboard/commerce/printers/devices/index.stx +273 -0
- package/views/dashboard/commerce/printers/receipts/index.stx +308 -0
- package/views/dashboard/commerce/products/detail.stx +563 -0
- package/views/dashboard/commerce/products/index.stx +370 -0
- package/views/dashboard/commerce/reviews/index.stx +656 -0
- package/views/dashboard/commerce/reviews/index.ts +0 -0
- package/views/dashboard/commerce/taxes/index.stx +377 -0
- package/views/dashboard/commerce/units/index.stx +317 -0
- package/views/dashboard/commerce/variants/index.stx +319 -0
- package/views/dashboard/commerce/waitlist/products.stx +942 -0
- package/views/dashboard/commerce/waitlist/restaurant.stx +1024 -0
- package/views/dashboard/components/index.stx +330 -0
- package/views/dashboard/composables/index.ts +19 -0
- package/views/dashboard/composables/useChart.ts +77 -0
- package/views/dashboard/composables/useNavigation.ts +99 -0
- package/views/dashboard/composables/useRole.ts +58 -0
- package/views/dashboard/composables/useTheme.ts +74 -0
- package/views/dashboard/content/authors/index.stx +171 -0
- package/views/dashboard/content/blog/index.stx +493 -0
- package/views/dashboard/content/categories/index.stx +169 -0
- package/views/dashboard/content/comments/index.stx +189 -0
- package/views/dashboard/content/dashboard.stx +383 -0
- package/views/dashboard/content/files/index.stx +706 -0
- package/views/dashboard/content/pages/index.stx +174 -0
- package/views/dashboard/content/posts/index.stx +269 -0
- package/views/dashboard/content/seo/index.stx +240 -0
- package/views/dashboard/content/tags/index.stx +167 -0
- package/views/dashboard/data/activity/index.stx +261 -0
- package/views/dashboard/data/dashboard/index.stx +116 -0
- package/views/dashboard/data/subscribers.stx +270 -0
- package/views/dashboard/data/teams.stx +275 -0
- package/views/dashboard/data/users.stx +279 -0
- package/views/dashboard/dependencies/index.stx +178 -0
- package/views/dashboard/deployments/[id].stx +234 -0
- package/views/dashboard/deployments/index.stx +9 -0
- package/views/dashboard/dns/index.stx +163 -0
- package/views/dashboard/environment/index.stx +283 -0
- package/views/dashboard/errors/index.stx +274 -0
- package/views/dashboard/forgot-password.stx +57 -0
- package/views/dashboard/functions/index.stx +159 -0
- package/views/dashboard/health/index.stx +248 -0
- package/views/dashboard/inbox/activity.stx +350 -0
- package/views/dashboard/inbox/index.stx +452 -0
- package/views/dashboard/inbox/settings.stx +290 -0
- package/views/dashboard/index.stx +252 -0
- package/views/dashboard/insights/index.stx +529 -0
- package/views/dashboard/jobs/[id].stx +430 -0
- package/views/dashboard/jobs/history.stx +222 -0
- package/views/dashboard/jobs/index.stx +214 -0
- package/views/dashboard/kanban/[id].stx +899 -0
- package/views/dashboard/kanban/index.stx +260 -0
- package/views/dashboard/layouts/default.stx +565 -0
- package/views/dashboard/layouts/guest.stx +10 -0
- package/views/dashboard/login.stx +48 -0
- package/views/dashboard/logs/index.stx +540 -0
- package/views/dashboard/mailboxes/index.stx +149 -0
- package/views/dashboard/management/permissions/index.stx +490 -0
- package/views/dashboard/marketing/campaigns/index.stx +604 -0
- package/views/dashboard/marketing/lists/index.stx +514 -0
- package/views/dashboard/marketing/reviews/index.stx +475 -0
- package/views/dashboard/marketing/social-posts/index.stx +560 -0
- package/views/dashboard/models/[model].stx +195 -0
- package/views/dashboard/models/index.stx +159 -0
- package/views/dashboard/monitoring/errors/index.stx +415 -0
- package/views/dashboard/notifications/dashboard.stx +383 -0
- package/views/dashboard/notifications/email.stx +336 -0
- package/views/dashboard/notifications/history.stx +373 -0
- package/views/dashboard/notifications/sms.stx +332 -0
- package/views/dashboard/packages/index.stx +195 -0
- package/views/dashboard/queries/[id].stx +381 -0
- package/views/dashboard/queries/history.stx +227 -0
- package/views/dashboard/queries/index.stx +335 -0
- package/views/dashboard/queries/slow.stx +318 -0
- package/views/dashboard/queue/index.stx +427 -0
- package/views/dashboard/realtime/index.stx +429 -0
- package/views/dashboard/register.stx +46 -0
- package/views/dashboard/releases/index.stx +389 -0
- package/views/dashboard/requests/index.stx +462 -0
- package/views/dashboard/serverless/index.stx +475 -0
- package/views/dashboard/servers/[id].stx +299 -0
- package/views/dashboard/servers/index.stx +483 -0
- package/views/dashboard/settings/billing.stx +50 -0
- package/views/dashboard/settings/index.stx +256 -0
- package/views/dashboard/settings/mail.stx +252 -0
- package/views/dashboard/stores/auth.ts +123 -0
- package/views/dashboard/stores/ci.ts +342 -0
- package/views/dashboard/stores/index.ts +129 -0
- package/views/dashboard/stores/kanban.ts +723 -0
- package/views/dashboard/stores/rbac.ts +350 -0
- package/views/dashboard/stx.config.ts +34 -0
- package/views/dashboard/teams/[id].stx +170 -0
- package/views/forum/category.stx +92 -0
- package/views/forum/index.stx +209 -0
- package/views/forum/new-topic.stx +113 -0
- package/views/forum/profile.stx +1 -0
- package/views/forum/topic.stx +143 -0
- package/views/hello-world/[name].stx +47 -0
- package/views/index.stx +21 -0
- package/views/system-tray/index.stx +147 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import { defineStore, derived, registerStoresClient, state } from '@stacksjs/stx'
|
|
2
|
+
|
|
3
|
+
interface BoardSummary {
|
|
4
|
+
id: number
|
|
5
|
+
uuid: string | null
|
|
6
|
+
name: string
|
|
7
|
+
description: string | null
|
|
8
|
+
icon: string
|
|
9
|
+
color: string
|
|
10
|
+
position: number
|
|
11
|
+
archived: boolean
|
|
12
|
+
cardCount: number
|
|
13
|
+
createdAt: string | null
|
|
14
|
+
updatedAt: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CardLabel { id: number, name: string, color: string }
|
|
18
|
+
interface CardAssignee { userId: number, name: string | null, email: string | null }
|
|
19
|
+
interface CardComment {
|
|
20
|
+
id: number
|
|
21
|
+
uuid: string | null
|
|
22
|
+
userId: number | null
|
|
23
|
+
body: string
|
|
24
|
+
authorName: string | null
|
|
25
|
+
authorEmail: string | null
|
|
26
|
+
createdAt: string | null
|
|
27
|
+
updatedAt: string | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CardRecord {
|
|
31
|
+
id: number
|
|
32
|
+
uuid: string | null
|
|
33
|
+
columnId: number
|
|
34
|
+
boardId: number
|
|
35
|
+
title: string
|
|
36
|
+
description: string | null
|
|
37
|
+
position: number
|
|
38
|
+
createdByUserId: number | null
|
|
39
|
+
dueDate: string | null
|
|
40
|
+
archived: boolean
|
|
41
|
+
createdAt: string | null
|
|
42
|
+
updatedAt: string | null
|
|
43
|
+
// Optional pivot-derived fields. BoardShowAction populates them
|
|
44
|
+
// (Phase 3); CardStoreAction's optimistic insert leaves them empty.
|
|
45
|
+
labels?: CardLabel[]
|
|
46
|
+
assignees?: CardAssignee[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface UserSummary { id: number, name: string | null, email: string | null }
|
|
50
|
+
|
|
51
|
+
interface CardDetail extends CardRecord {
|
|
52
|
+
labels: CardLabel[]
|
|
53
|
+
assignees: CardAssignee[]
|
|
54
|
+
comments: CardComment[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ColumnRecord {
|
|
58
|
+
id: number
|
|
59
|
+
uuid: string | null
|
|
60
|
+
boardId: number
|
|
61
|
+
name: string
|
|
62
|
+
position: number
|
|
63
|
+
cardLimit: number | null
|
|
64
|
+
color: string
|
|
65
|
+
cards: CardRecord[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface LabelRecord {
|
|
69
|
+
id: number
|
|
70
|
+
boardId: number
|
|
71
|
+
name: string
|
|
72
|
+
color: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface BoardDetail {
|
|
76
|
+
id: number
|
|
77
|
+
uuid: string | null
|
|
78
|
+
name: string
|
|
79
|
+
description: string | null
|
|
80
|
+
icon: string
|
|
81
|
+
color: string
|
|
82
|
+
position: number
|
|
83
|
+
archived: boolean
|
|
84
|
+
createdAt: string | null
|
|
85
|
+
updatedAt: string | null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Kanban dashboard store (stacksjs/stacks#1846).
|
|
90
|
+
*
|
|
91
|
+
* Phase 1 surface — list of boards for the index page plus the
|
|
92
|
+
* fetch-by-id helper that the board detail page (Phase 2) will use.
|
|
93
|
+
* Drag-and-drop reorder, write mutations, and label/assignee ops land
|
|
94
|
+
* in Phase 2 / Phase 3.
|
|
95
|
+
*
|
|
96
|
+
* Persistence: only the last-viewed board id is cached in
|
|
97
|
+
* sessionStorage so the user lands back where they left off when they
|
|
98
|
+
* SPA-nav between unrelated dashboard pages. The actual data is always
|
|
99
|
+
* re-fetched.
|
|
100
|
+
*/
|
|
101
|
+
export const kanbanStore = defineStore('kanban', () => {
|
|
102
|
+
const boards = state<BoardSummary[]>([])
|
|
103
|
+
const currentBoard = state<BoardDetail | null>(null)
|
|
104
|
+
const currentColumns = state<ColumnRecord[]>([])
|
|
105
|
+
const currentLabels = state<LabelRecord[]>([])
|
|
106
|
+
const lastViewedBoardId = state<number | null>(null)
|
|
107
|
+
const loading = state(true)
|
|
108
|
+
const loadingBoard = state(false)
|
|
109
|
+
const error = state<string | null>(null)
|
|
110
|
+
const errorBoard = state<string | null>(null)
|
|
111
|
+
|
|
112
|
+
// Phase 3 — card detail modal state. Holding card + comments here
|
|
113
|
+
// (instead of one of column.cards) keeps the modal independent of
|
|
114
|
+
// its source: opening from a card click on the board uses the
|
|
115
|
+
// already-loaded preview; refresh-mid-modal hits /cards/:id and
|
|
116
|
+
// hydrates from the network. Either way the modal reads from
|
|
117
|
+
// `openCard` / `openCardComments`.
|
|
118
|
+
const openCard = state<CardDetail | null>(null)
|
|
119
|
+
const loadingCard = state(false)
|
|
120
|
+
const errorCard = state<string | null>(null)
|
|
121
|
+
const users = state<UserSummary[]>([])
|
|
122
|
+
const loadingUsers = state(false)
|
|
123
|
+
|
|
124
|
+
const hasBoards = derived(() => boards().length > 0)
|
|
125
|
+
|
|
126
|
+
async function loadBoards(): Promise<void> {
|
|
127
|
+
loading.set(true)
|
|
128
|
+
error.set(null)
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch('/api/dashboard/kanban/boards', { headers: { accept: 'application/json' } })
|
|
131
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
132
|
+
const data = await res.json() as { boards: BoardSummary[], error?: string }
|
|
133
|
+
if (data.error) {
|
|
134
|
+
error.set(data.error)
|
|
135
|
+
boards.set([])
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
boards.set(data.boards ?? [])
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
error.set(e instanceof Error ? e.message : String(e))
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
loading.set(false)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function loadBoard(id: number): Promise<void> {
|
|
149
|
+
loadingBoard.set(true)
|
|
150
|
+
errorBoard.set(null)
|
|
151
|
+
lastViewedBoardId.set(id)
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(`/api/dashboard/kanban/boards/${id}`, { headers: { accept: 'application/json' } })
|
|
154
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
155
|
+
const data = await res.json() as {
|
|
156
|
+
board?: BoardDetail
|
|
157
|
+
columns?: ColumnRecord[]
|
|
158
|
+
labels?: LabelRecord[]
|
|
159
|
+
error?: string
|
|
160
|
+
}
|
|
161
|
+
if (data.error || !data.board) {
|
|
162
|
+
errorBoard.set(data.error ?? 'Board not found')
|
|
163
|
+
currentBoard.set(null)
|
|
164
|
+
currentColumns.set([])
|
|
165
|
+
currentLabels.set([])
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
currentBoard.set(data.board)
|
|
169
|
+
currentColumns.set(data.columns ?? [])
|
|
170
|
+
currentLabels.set(data.labels ?? [])
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
errorBoard.set(e instanceof Error ? e.message : String(e))
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
loadingBoard.set(false)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Mutations (Phase 2) ───────────────────────────────────────────
|
|
181
|
+
//
|
|
182
|
+
// Every mutation follows the same shape:
|
|
183
|
+
// 1. Apply optimistically — update the local store immediately so
|
|
184
|
+
// the UI feels snappy.
|
|
185
|
+
// 2. Fire the network request.
|
|
186
|
+
// 3. On failure, restore the pre-mutation snapshot + set an error
|
|
187
|
+
// message. Phase 3 wires a toast helper into the rollback path
|
|
188
|
+
// so the user actually sees the failure.
|
|
189
|
+
//
|
|
190
|
+
// The mutations return the new entity (or null on failure) so
|
|
191
|
+
// callers can chain further work or surface "saved!" toasts.
|
|
192
|
+
|
|
193
|
+
async function createBoard(input: { name: string, description?: string, color?: string, icon?: string }): Promise<BoardSummary | null> {
|
|
194
|
+
const snapshot = boards()
|
|
195
|
+
try {
|
|
196
|
+
const res = await fetch('/api/dashboard/kanban/boards', {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
199
|
+
body: JSON.stringify(input),
|
|
200
|
+
})
|
|
201
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
202
|
+
const data = await res.json() as { board?: BoardSummary, error?: string }
|
|
203
|
+
if (data.error || !data.board) throw new Error(data.error ?? 'Create failed')
|
|
204
|
+
boards.set([...snapshot, data.board])
|
|
205
|
+
return data.board
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
error.set(e instanceof Error ? e.message : String(e))
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function deleteBoard(id: number): Promise<boolean> {
|
|
214
|
+
const snapshot = boards()
|
|
215
|
+
// Optimistic remove.
|
|
216
|
+
boards.set(snapshot.filter(b => b.id !== id))
|
|
217
|
+
try {
|
|
218
|
+
const res = await fetch(`/api/dashboard/kanban/boards/${id}`, { method: 'DELETE', headers: { accept: 'application/json' } })
|
|
219
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
boards.set(snapshot)
|
|
224
|
+
error.set(e instanceof Error ? e.message : String(e))
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function createColumn(input: { boardId: number, name: string, color?: string }): Promise<ColumnRecord | null> {
|
|
230
|
+
const snapshot = currentColumns()
|
|
231
|
+
try {
|
|
232
|
+
const res = await fetch('/api/dashboard/kanban/columns', {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
235
|
+
body: JSON.stringify(input),
|
|
236
|
+
})
|
|
237
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
238
|
+
const data = await res.json() as { column?: ColumnRecord, error?: string }
|
|
239
|
+
if (data.error || !data.column) throw new Error(data.error ?? 'Create failed')
|
|
240
|
+
currentColumns.set([...snapshot, data.column])
|
|
241
|
+
return data.column
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
errorBoard.set(e instanceof Error ? e.message : String(e))
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function deleteColumn(columnId: number): Promise<boolean> {
|
|
250
|
+
const snapshot = currentColumns()
|
|
251
|
+
currentColumns.set(snapshot.filter(c => c.id !== columnId))
|
|
252
|
+
try {
|
|
253
|
+
const res = await fetch(`/api/dashboard/kanban/columns/${columnId}`, { method: 'DELETE', headers: { accept: 'application/json' } })
|
|
254
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
currentColumns.set(snapshot)
|
|
259
|
+
errorBoard.set(e instanceof Error ? e.message : String(e))
|
|
260
|
+
return false
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function createCard(input: { columnId: number, title: string, description?: string }): Promise<CardRecord | null> {
|
|
265
|
+
const snapshot = currentColumns()
|
|
266
|
+
try {
|
|
267
|
+
const res = await fetch('/api/dashboard/kanban/cards', {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
270
|
+
body: JSON.stringify(input),
|
|
271
|
+
})
|
|
272
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
273
|
+
const data = await res.json() as { card?: CardRecord, error?: string }
|
|
274
|
+
if (data.error || !data.card) throw new Error(data.error ?? 'Create failed')
|
|
275
|
+
currentColumns.set(snapshot.map(col =>
|
|
276
|
+
col.id === input.columnId
|
|
277
|
+
? { ...col, cards: [...col.cards, data.card!] }
|
|
278
|
+
: col,
|
|
279
|
+
))
|
|
280
|
+
return data.card
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
errorBoard.set(e instanceof Error ? e.message : String(e))
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function deleteCard(cardId: number): Promise<boolean> {
|
|
289
|
+
const snapshot = currentColumns()
|
|
290
|
+
currentColumns.set(snapshot.map(col => ({
|
|
291
|
+
...col,
|
|
292
|
+
cards: col.cards.filter(c => c.id !== cardId),
|
|
293
|
+
})))
|
|
294
|
+
try {
|
|
295
|
+
const res = await fetch(`/api/dashboard/kanban/cards/${cardId}`, { method: 'DELETE', headers: { accept: 'application/json' } })
|
|
296
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
currentColumns.set(snapshot)
|
|
301
|
+
errorBoard.set(e instanceof Error ? e.message : String(e))
|
|
302
|
+
return false
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Reorder cards across one or more columns of the current board.
|
|
308
|
+
* Called by the drag-and-drop handler in `kanban/[id].stx` after a
|
|
309
|
+
* card move. Sends the full per-column state so neighbour
|
|
310
|
+
* positions never drift — see the CardsReorderAction docstring.
|
|
311
|
+
*
|
|
312
|
+
* @param payload one entry per affected column with that column's
|
|
313
|
+
* new ordered list of card ids
|
|
314
|
+
*/
|
|
315
|
+
async function reorderCards(payload: Array<{ columnId: number, order: number[] }>): Promise<boolean> {
|
|
316
|
+
const snapshot = currentColumns()
|
|
317
|
+
|
|
318
|
+
// Apply optimistically — derive the new column state from the
|
|
319
|
+
// payload + existing cards.
|
|
320
|
+
const cardById = new Map<number, CardRecord>()
|
|
321
|
+
for (const col of snapshot)
|
|
322
|
+
for (const c of col.cards) cardById.set(c.id, c)
|
|
323
|
+
|
|
324
|
+
const next = snapshot.map((col) => {
|
|
325
|
+
const entry = payload.find(p => p.columnId === col.id)
|
|
326
|
+
if (!entry) return col
|
|
327
|
+
const reordered = entry.order
|
|
328
|
+
.map((id, i) => {
|
|
329
|
+
const card = cardById.get(id)
|
|
330
|
+
if (!card) return null
|
|
331
|
+
return { ...card, columnId: col.id, position: i }
|
|
332
|
+
})
|
|
333
|
+
.filter((c): c is CardRecord => c !== null)
|
|
334
|
+
return { ...col, cards: reordered }
|
|
335
|
+
})
|
|
336
|
+
currentColumns.set(next)
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const res = await fetch('/api/dashboard/kanban/cards/reorder', {
|
|
340
|
+
method: 'POST',
|
|
341
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
342
|
+
body: JSON.stringify({ columns: payload }),
|
|
343
|
+
})
|
|
344
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
345
|
+
const data = await res.json() as { moved?: number, error?: string }
|
|
346
|
+
if (data.error) throw new Error(data.error)
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
currentColumns.set(snapshot)
|
|
351
|
+
errorBoard.set(e instanceof Error ? e.message : String(e))
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function reorderColumns(boardId: number, order: number[]): Promise<boolean> {
|
|
357
|
+
const snapshot = currentColumns()
|
|
358
|
+
// Optimistic reorder by sorting against the supplied id list.
|
|
359
|
+
const byId = new Map(snapshot.map(c => [c.id, c] as const))
|
|
360
|
+
const next = order.map((id, i) => {
|
|
361
|
+
const col = byId.get(id)
|
|
362
|
+
return col ? { ...col, position: i } : null
|
|
363
|
+
}).filter((c): c is ColumnRecord => c !== null)
|
|
364
|
+
currentColumns.set(next)
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const res = await fetch('/api/dashboard/kanban/columns/reorder', {
|
|
368
|
+
method: 'POST',
|
|
369
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
370
|
+
body: JSON.stringify({ boardId, order }),
|
|
371
|
+
})
|
|
372
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
373
|
+
const data = await res.json() as { reordered?: number, error?: string }
|
|
374
|
+
if (data.error) throw new Error(data.error)
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
currentColumns.set(snapshot)
|
|
379
|
+
errorBoard.set(e instanceof Error ? e.message : String(e))
|
|
380
|
+
return false
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ─── Phase 3 mutations (card detail) ─────────────────────────────
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Helper: project a CardRecord (board view) onto a partial
|
|
388
|
+
* CardDetail (modal view). Used when the modal opens against a
|
|
389
|
+
* card already loaded into `currentColumns` — we fill in the
|
|
390
|
+
* pivot-derived shape and fire `/cards/:id` in the background to
|
|
391
|
+
* pick up comments.
|
|
392
|
+
*/
|
|
393
|
+
function cardToDetail(c: CardRecord): CardDetail {
|
|
394
|
+
return {
|
|
395
|
+
...c,
|
|
396
|
+
labels: c.labels ?? [],
|
|
397
|
+
assignees: c.assignees ?? [],
|
|
398
|
+
comments: [], // hydrated by openCardDetail
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function findCardInColumns(cardId: number): CardRecord | null {
|
|
403
|
+
for (const col of currentColumns())
|
|
404
|
+
for (const card of col.cards)
|
|
405
|
+
if (card.id === cardId) return card
|
|
406
|
+
return null
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function openCardDetail(cardId: number): Promise<void> {
|
|
410
|
+
loadingCard.set(true)
|
|
411
|
+
errorCard.set(null)
|
|
412
|
+
// Seed from the board view if we have it so the modal renders
|
|
413
|
+
// immediately; the fetch fills in comments and refreshes labels
|
|
414
|
+
// / assignees from the canonical store.
|
|
415
|
+
const seed = findCardInColumns(cardId)
|
|
416
|
+
if (seed)
|
|
417
|
+
openCard.set(cardToDetail(seed))
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const res = await fetch(`/api/dashboard/kanban/cards/${cardId}`, { headers: { accept: 'application/json' } })
|
|
421
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
422
|
+
const data = await res.json() as {
|
|
423
|
+
card?: CardRecord
|
|
424
|
+
labels?: CardLabel[]
|
|
425
|
+
assignees?: CardAssignee[]
|
|
426
|
+
comments?: CardComment[]
|
|
427
|
+
error?: string
|
|
428
|
+
}
|
|
429
|
+
if (data.error || !data.card)
|
|
430
|
+
throw new Error(data.error ?? 'Card not found')
|
|
431
|
+
openCard.set({
|
|
432
|
+
...data.card,
|
|
433
|
+
labels: data.labels ?? [],
|
|
434
|
+
assignees: data.assignees ?? [],
|
|
435
|
+
comments: data.comments ?? [],
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
errorCard.set(e instanceof Error ? e.message : String(e))
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
loadingCard.set(false)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function closeCardDetail(): void {
|
|
447
|
+
openCard.set(null)
|
|
448
|
+
errorCard.set(null)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Apply an in-place patch to the open card AND to the matching card
|
|
453
|
+
* inside `currentColumns` so the board view re-renders alongside
|
|
454
|
+
* the modal. Reduces double-bookkeeping at every callsite.
|
|
455
|
+
*/
|
|
456
|
+
function patchOpenCard(partial: Partial<CardDetail>): void {
|
|
457
|
+
const oc = openCard()
|
|
458
|
+
if (!oc) return
|
|
459
|
+
openCard.set({ ...oc, ...partial })
|
|
460
|
+
|
|
461
|
+
currentColumns.set(currentColumns().map(col => ({
|
|
462
|
+
...col,
|
|
463
|
+
cards: col.cards.map(c => c.id === oc.id ? { ...c, ...partial } : c),
|
|
464
|
+
})))
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function updateCard(input: { title?: string, description?: string | null, dueDate?: string | null, archived?: boolean }): Promise<boolean> {
|
|
468
|
+
const oc = openCard()
|
|
469
|
+
if (!oc) return false
|
|
470
|
+
const snapshot = { ...oc }
|
|
471
|
+
patchOpenCard(input as Partial<CardDetail>)
|
|
472
|
+
try {
|
|
473
|
+
const res = await fetch(`/api/dashboard/kanban/cards/${oc.id}`, {
|
|
474
|
+
method: 'PATCH',
|
|
475
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
476
|
+
body: JSON.stringify(input),
|
|
477
|
+
})
|
|
478
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
479
|
+
const data = await res.json() as { card?: CardRecord, error?: string }
|
|
480
|
+
if (data.error || !data.card) throw new Error(data.error ?? 'Update failed')
|
|
481
|
+
// Server-canonical state wins (handles trimming etc.).
|
|
482
|
+
patchOpenCard(data.card)
|
|
483
|
+
return true
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
// Roll back to the pre-patch snapshot.
|
|
487
|
+
openCard.set(snapshot)
|
|
488
|
+
currentColumns.set(currentColumns().map(col => ({
|
|
489
|
+
...col,
|
|
490
|
+
cards: col.cards.map(c => c.id === snapshot.id ? snapshot : c),
|
|
491
|
+
})))
|
|
492
|
+
errorCard.set(e instanceof Error ? e.message : String(e))
|
|
493
|
+
return false
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function loadUsers(): Promise<void> {
|
|
498
|
+
if (loadingUsers() || users().length > 0) return
|
|
499
|
+
loadingUsers.set(true)
|
|
500
|
+
try {
|
|
501
|
+
const res = await fetch('/api/dashboard/kanban/users', { headers: { accept: 'application/json' } })
|
|
502
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
503
|
+
const data = await res.json() as { users?: UserSummary[] }
|
|
504
|
+
users.set(data.users ?? [])
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
// Soft-fail — assignee picker just shows empty.
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
loadingUsers.set(false)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function syncCardLabels(cardId: number, labelIds: number[]): Promise<boolean> {
|
|
515
|
+
const oc = openCard()
|
|
516
|
+
if (!oc || oc.id !== cardId) return false
|
|
517
|
+
const snapshot = oc.labels
|
|
518
|
+
// Optimistic: keep only the labels in the new set, pulled from
|
|
519
|
+
// the current open card's labels OR the board palette.
|
|
520
|
+
const boardLabels = currentLabels()
|
|
521
|
+
const optimistic = labelIds.map((id) => {
|
|
522
|
+
const fromCard = oc.labels.find(l => l.id === id)
|
|
523
|
+
if (fromCard) return fromCard
|
|
524
|
+
const fromBoard = boardLabels.find(l => l.id === id)
|
|
525
|
+
return fromBoard ? { id: fromBoard.id, name: fromBoard.name, color: fromBoard.color } : null
|
|
526
|
+
}).filter((l): l is CardLabel => l !== null)
|
|
527
|
+
patchOpenCard({ labels: optimistic })
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
const res = await fetch(`/api/dashboard/kanban/cards/${cardId}/labels`, {
|
|
531
|
+
method: 'POST',
|
|
532
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
533
|
+
body: JSON.stringify({ labelIds }),
|
|
534
|
+
})
|
|
535
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
536
|
+
const data = await res.json() as { labels?: CardLabel[], error?: string }
|
|
537
|
+
if (data.error) throw new Error(data.error)
|
|
538
|
+
// Canonical reconciliation.
|
|
539
|
+
patchOpenCard({ labels: data.labels ?? [] })
|
|
540
|
+
return true
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
patchOpenCard({ labels: snapshot })
|
|
544
|
+
errorCard.set(e instanceof Error ? e.message : String(e))
|
|
545
|
+
return false
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function syncCardAssignees(cardId: number, userIds: number[]): Promise<boolean> {
|
|
550
|
+
const oc = openCard()
|
|
551
|
+
if (!oc || oc.id !== cardId) return false
|
|
552
|
+
const snapshot = oc.assignees
|
|
553
|
+
// Optimistic — pull names from the loaded users list when
|
|
554
|
+
// available; fall back to a minimal record so the avatar
|
|
555
|
+
// initial renders something.
|
|
556
|
+
const allUsers = users()
|
|
557
|
+
const optimistic = userIds.map((id) => {
|
|
558
|
+
const u = allUsers.find(x => x.id === id)
|
|
559
|
+
return u
|
|
560
|
+
? { userId: u.id, name: u.name, email: u.email }
|
|
561
|
+
: { userId: id, name: null, email: null }
|
|
562
|
+
})
|
|
563
|
+
patchOpenCard({ assignees: optimistic })
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
const res = await fetch(`/api/dashboard/kanban/cards/${cardId}/assignees`, {
|
|
567
|
+
method: 'POST',
|
|
568
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
569
|
+
body: JSON.stringify({ userIds }),
|
|
570
|
+
})
|
|
571
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
572
|
+
const data = await res.json() as { assignees?: CardAssignee[], error?: string }
|
|
573
|
+
if (data.error) throw new Error(data.error)
|
|
574
|
+
patchOpenCard({ assignees: data.assignees ?? [] })
|
|
575
|
+
return true
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
patchOpenCard({ assignees: snapshot })
|
|
579
|
+
errorCard.set(e instanceof Error ? e.message : String(e))
|
|
580
|
+
return false
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function addComment(body: string): Promise<boolean> {
|
|
585
|
+
const oc = openCard()
|
|
586
|
+
if (!oc) return false
|
|
587
|
+
const trimmed = body.trim()
|
|
588
|
+
if (!trimmed) return false
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const res = await fetch(`/api/dashboard/kanban/cards/${oc.id}/comments`, {
|
|
592
|
+
method: 'POST',
|
|
593
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
594
|
+
body: JSON.stringify({ body: trimmed }),
|
|
595
|
+
})
|
|
596
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
597
|
+
const data = await res.json() as { comment?: CardComment, error?: string }
|
|
598
|
+
if (data.error || !data.comment) throw new Error(data.error ?? 'Comment failed')
|
|
599
|
+
patchOpenCard({ comments: [...oc.comments, data.comment] })
|
|
600
|
+
return true
|
|
601
|
+
}
|
|
602
|
+
catch (e) {
|
|
603
|
+
errorCard.set(e instanceof Error ? e.message : String(e))
|
|
604
|
+
return false
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function deleteComment(commentId: number): Promise<boolean> {
|
|
609
|
+
const oc = openCard()
|
|
610
|
+
if (!oc) return false
|
|
611
|
+
const snapshot = oc.comments
|
|
612
|
+
patchOpenCard({ comments: oc.comments.filter(c => c.id !== commentId) })
|
|
613
|
+
try {
|
|
614
|
+
const res = await fetch(`/api/dashboard/kanban/comments/${commentId}`, { method: 'DELETE', headers: { accept: 'application/json' } })
|
|
615
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
616
|
+
return true
|
|
617
|
+
}
|
|
618
|
+
catch (e) {
|
|
619
|
+
patchOpenCard({ comments: snapshot })
|
|
620
|
+
errorCard.set(e instanceof Error ? e.message : String(e))
|
|
621
|
+
return false
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ─── Label CRUD (board scope) ──────────────────────────────────────
|
|
626
|
+
|
|
627
|
+
async function createLabel(input: { boardId: number, name: string, color?: string }): Promise<LabelRecord | null> {
|
|
628
|
+
try {
|
|
629
|
+
const res = await fetch('/api/dashboard/kanban/labels', {
|
|
630
|
+
method: 'POST',
|
|
631
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
632
|
+
body: JSON.stringify(input),
|
|
633
|
+
})
|
|
634
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
635
|
+
const data = await res.json() as { label?: LabelRecord, error?: string }
|
|
636
|
+
if (data.error || !data.label) throw new Error(data.error ?? 'Create failed')
|
|
637
|
+
currentLabels.set([...currentLabels(), data.label])
|
|
638
|
+
return data.label
|
|
639
|
+
}
|
|
640
|
+
catch (e) {
|
|
641
|
+
errorCard.set(e instanceof Error ? e.message : String(e))
|
|
642
|
+
return null
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function deleteLabel(labelId: number): Promise<boolean> {
|
|
647
|
+
const snapshot = currentLabels()
|
|
648
|
+
currentLabels.set(snapshot.filter(l => l.id !== labelId))
|
|
649
|
+
// Also strip the label from any cards on the board (mirrors what
|
|
650
|
+
// LabelDestroyAction does server-side).
|
|
651
|
+
const colSnapshot = currentColumns()
|
|
652
|
+
currentColumns.set(colSnapshot.map(col => ({
|
|
653
|
+
...col,
|
|
654
|
+
cards: col.cards.map(c => ({
|
|
655
|
+
...c,
|
|
656
|
+
labels: (c.labels ?? []).filter(l => l.id !== labelId),
|
|
657
|
+
})),
|
|
658
|
+
})))
|
|
659
|
+
const oc = openCard()
|
|
660
|
+
if (oc)
|
|
661
|
+
patchOpenCard({ labels: oc.labels.filter(l => l.id !== labelId) })
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
const res = await fetch(`/api/dashboard/kanban/labels/${labelId}`, { method: 'DELETE', headers: { accept: 'application/json' } })
|
|
665
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
666
|
+
return true
|
|
667
|
+
}
|
|
668
|
+
catch (e) {
|
|
669
|
+
currentLabels.set(snapshot)
|
|
670
|
+
currentColumns.set(colSnapshot)
|
|
671
|
+
errorCard.set(e instanceof Error ? e.message : String(e))
|
|
672
|
+
return false
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
boards,
|
|
678
|
+
currentBoard,
|
|
679
|
+
currentColumns,
|
|
680
|
+
currentLabels,
|
|
681
|
+
lastViewedBoardId,
|
|
682
|
+
loading,
|
|
683
|
+
loadingBoard,
|
|
684
|
+
error,
|
|
685
|
+
errorBoard,
|
|
686
|
+
hasBoards,
|
|
687
|
+
loadBoards,
|
|
688
|
+
loadBoard,
|
|
689
|
+
createBoard,
|
|
690
|
+
deleteBoard,
|
|
691
|
+
createColumn,
|
|
692
|
+
deleteColumn,
|
|
693
|
+
createCard,
|
|
694
|
+
deleteCard,
|
|
695
|
+
reorderCards,
|
|
696
|
+
reorderColumns,
|
|
697
|
+
// Phase 3
|
|
698
|
+
openCard,
|
|
699
|
+
loadingCard,
|
|
700
|
+
errorCard,
|
|
701
|
+
users,
|
|
702
|
+
loadingUsers,
|
|
703
|
+
openCardDetail,
|
|
704
|
+
closeCardDetail,
|
|
705
|
+
updateCard,
|
|
706
|
+
loadUsers,
|
|
707
|
+
syncCardLabels,
|
|
708
|
+
syncCardAssignees,
|
|
709
|
+
addComment,
|
|
710
|
+
deleteComment,
|
|
711
|
+
createLabel,
|
|
712
|
+
deleteLabel,
|
|
713
|
+
}
|
|
714
|
+
}, {
|
|
715
|
+
persist: {
|
|
716
|
+
storage: 'sessionStorage',
|
|
717
|
+
key: 'stacks-dashboard-kanban',
|
|
718
|
+
pick: ['lastViewedBoardId'],
|
|
719
|
+
},
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
if (typeof window !== 'undefined')
|
|
723
|
+
registerStoresClient({ kanbanStore })
|