diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..51f9b1b99 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,177 @@ +# Dependencies +node_modules +*/node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +bun.lockb* + +# Testing +coverage +test-results +.nyc_output + +# Next.js +.next/ +*/.next/ +out/ +*/out/ + +# Nuxt.js +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Vue.js +dist/ + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage +.grunt + +# Bower dependency directory +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons +build/Release + +# Dependency directories +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables files +.env +.env.test +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Parcel-bundler cache +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# VS Code +.vscode + +# MacOS +.DS_Store + +# Git +.git +.gitignore +README.md + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# IDE files +*.swp +*.swo +*~ + +# OS generated files +Thumbs.db +ehthumbs.db + +# E2E test files +apps/app/e2e/ +apps/app/test-results/ +playwright-report/ +test-results/ + +# Turbo +.turbo +*/.turbo + +# Build artifacts and caches +*/dist/ +**/dist/ +**/build/ +**/.cache/ +**/coverage/ +**/.nyc_output/ + +# Platform specific binaries +**/bin/ +**/*.dylib* +**/*.node +**/*.pack \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..3800275dd --- /dev/null +++ b/.env.example @@ -0,0 +1,45 @@ +# Database Configuration +DATABASE_URL=postgres://postgres:postgres@db:5432/comp + +# Authentication +AUTH_SECRET=your-secret-auth-key-here-min-32-chars +AUTH_GOOGLE_ID=your-google-oauth-client-id +AUTH_GOOGLE_SECRET=your-google-oauth-client-secret +AUTH_GITHUB_ID=your-github-oauth-app-id +AUTH_GITHUB_SECRET=your-github-oauth-app-secret + +# Email Service +RESEND_API_KEY=re_your_resend_api_key_here + +# Application URLs +NEXT_PUBLIC_PORTAL_URL=http://localhost:3001 +NEXT_PUBLIC_VERCEL_URL=http://localhost:3000 + +# Security +REVALIDATION_SECRET=your-revalidation-secret-here + +# Optional - Redis/Upstash (for caching) +UPSTASH_REDIS_REST_URL=your-upstash-redis-url +UPSTASH_REDIS_REST_TOKEN=your-upstash-redis-token + +# Optional - AWS S3 (for file uploads) +APP_AWS_ACCESS_KEY_ID=your-aws-access-key +APP_AWS_SECRET_ACCESS_KEY=your-aws-secret-key +APP_AWS_REGION=us-east-1 +APP_AWS_BUCKET_NAME=your-s3-bucket-name + +# Optional - OpenAI +OPENAI_API_KEY=sk-your-openai-api-key + +# Optional - Analytics +NEXT_PUBLIC_POSTHOG_KEY=your-posthog-key +NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com + +# Optional - External Services +TRIGGER_SECRET_KEY=your-trigger-secret +TRIGGER_API_KEY=your-trigger-api-key +GROQ_API_KEY=your-groq-api-key +FIRECRAWL_API_KEY=your-firecrawl-key + +# Environment +NODE_ENV=production \ No newline at end of file diff --git a/.github/BRANCH_PROTECTION.md b/.github/BRANCH_PROTECTION.md index 9131632d8..63c788f40 100644 --- a/.github/BRANCH_PROTECTION.md +++ b/.github/BRANCH_PROTECTION.md @@ -16,13 +16,11 @@ Go to **Settings → Branches** in your GitHub repository and add a branch prote ### Must Pass Before Merging: 1. **Quick Checks** (Quick Tests) - - Fast smoke tests that run on every PR - Includes type checking, linting, and critical middleware tests - Should complete in < 5 minutes 2. **Unit Tests** (Unit Tests / app) - - Vitest unit tests for the main app - Tests business logic, utilities, and components @@ -103,9 +101,7 @@ Protect matching branches: Status checks that are required: - Quick Checks - Unit Tests (app) - - Unit Tests (framework-editor) - Unit Tests (portal) - - Unit Tests (trust) - E2E Tests - chromium - E2E Tests - firefox - E2E Tests - webkit diff --git a/.github/TESTS_README.md b/.github/TESTS_README.md index 9ffa31500..38bf1da9f 100644 --- a/.github/TESTS_README.md +++ b/.github/TESTS_README.md @@ -62,7 +62,6 @@ See [BRANCH_PROTECTION.md](BRANCH_PROTECTION.md) for setup instructions. ## Workflow Files - **Testing** - - `test-quick.yml` - Type checking, linting, smoke tests - `unit-tests.yml` - Vitest tests for all apps - `e2e-tests.yml` - Playwright tests (multi-browser) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d085ceb0..71f1468f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,10 +51,19 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile # Use --frozen-lockfile in CI + - name: Build packages + run: | + echo "🔨 Building packages before release..." + bun run -F @trycompai/db build + bun run -F @trycompai/email build + bun run -F @trycompai/kv build + bun run -F @trycompai/ui build + bun run -F @trycompai/analytics build + - name: Release env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} - # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Uncomment if publishing to npm + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # Enable npm publishing DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} HUSKY: 0 # Skip husky hooks in CI run: npx semantic-release diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows_disabled/e2e-tests.yml similarity index 100% rename from .github/workflows/e2e-tests.yml rename to .github/workflows_disabled/e2e-tests.yml diff --git a/.github/workflows/quick-tests.yml b/.github/workflows_disabled/quick-tests.yml similarity index 100% rename from .github/workflows/quick-tests.yml rename to .github/workflows_disabled/quick-tests.yml diff --git a/.github/workflows/test-quick.yml b/.github/workflows_disabled/test-quick.yml similarity index 100% rename from .github/workflows/test-quick.yml rename to .github/workflows_disabled/test-quick.yml diff --git a/.github/workflows/unit-tests.yml b/.github/workflows_disabled/unit-tests.yml similarity index 98% rename from .github/workflows/unit-tests.yml rename to .github/workflows_disabled/unit-tests.yml index ada3f01b0..df30e34d6 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows_disabled/unit-tests.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: - app: [app, framework-editor, portal, trust] + app: [app, portal] steps: - name: Checkout diff --git a/.gitignore b/.gitignore index f42185fb3..3659fd5bd 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,6 @@ playwright-report/ playwright/.cache/ debug-setup-page.png -.playground/ \ No newline at end of file +.playground/ + +packages/*/dist \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index aeb37e397..28d539c81 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,4 +27,8 @@ coverage/ **/playwright-report/ **/test-results/ **/playwright/.cache/ -**/.playwright/ \ No newline at end of file +**/.playwright/ + +# Dist +packages/*/dist +dist/* \ No newline at end of file diff --git a/.windsurfrules b/.windsurfrules deleted file mode 100644 index e69de29bb..000000000 diff --git a/CHANGELOG.md b/CHANGELOG.md index bc12a4a5d..007d97d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,215 +14,196 @@ # [1.49.0](https://github.com/trycompai/comp/compare/v1.48.1...v1.49.0) (2025-07-03) - ### Bug Fixes -* **onboarding:** update step index and progress calculation in PostPaymentOnboarding component ([a7b3a13](https://github.com/trycompai/comp/commit/a7b3a1378ab45c361769a7c1dfdb3a2edc9180fc)) - +- **onboarding:** update step index and progress calculation in PostPaymentOnboarding component ([a7b3a13](https://github.com/trycompai/comp/commit/a7b3a1378ab45c361769a7c1dfdb3a2edc9180fc)) ### Features -* **e2e:** add tests for middleware onboarding behavior and split onboarding flow ([cb31f22](https://github.com/trycompai/comp/commit/cb31f2234ea84701b6f4a670d735ecc6a0bddc03)) +- **e2e:** add tests for middleware onboarding behavior and split onboarding flow ([cb31f22](https://github.com/trycompai/comp/commit/cb31f2234ea84701b6f4a670d735ecc6a0bddc03)) ## [1.48.1](https://github.com/trycompai/comp/compare/v1.48.0...v1.48.1) (2025-06-30) - ### Bug Fixes -* fixed an issue with uploading files to comments in a policy ([8a3903c](https://github.com/trycompai/comp/commit/8a3903c291770e4da8aa5b55ff0b973330781430)) +- fixed an issue with uploading files to comments in a policy ([8a3903c](https://github.com/trycompai/comp/commit/8a3903c291770e4da8aa5b55ff0b973330781430)) # [1.48.0](https://github.com/trycompai/comp/compare/v1.47.0...v1.48.0) (2025-06-27) - ### Bug Fixes -* **middleware:** refine onboarding redirect logic to check for explicit false value ([93dfa37](https://github.com/trycompai/comp/commit/93dfa379be2cce444acdd8db85e2decd28da958b)) -* **onboarding:** enhance textarea handling and improve localStorage integration ([64821a3](https://github.com/trycompai/comp/commit/64821a3b4484b613177c7887b0824277270192aa)) - +- **middleware:** refine onboarding redirect logic to check for explicit false value ([93dfa37](https://github.com/trycompai/comp/commit/93dfa379be2cce444acdd8db85e2decd28da958b)) +- **onboarding:** enhance textarea handling and improve localStorage integration ([64821a3](https://github.com/trycompai/comp/commit/64821a3b4484b613177c7887b0824277270192aa)) ### Features -* **onboarding:** enhance post-payment onboarding flow with loading state and step tracking ([6b5f055](https://github.com/trycompai/comp/commit/6b5f055188d729240015e0c7e3e6a40404ec5c8a)) -* **onboarding:** implement split onboarding flow and middleware checks ([7346380](https://github.com/trycompai/comp/commit/73463801f7eff32dcf34cf475ac675ea173aedbf)) +- **onboarding:** enhance post-payment onboarding flow with loading state and step tracking ([6b5f055](https://github.com/trycompai/comp/commit/6b5f055188d729240015e0c7e3e6a40404ec5c8a)) +- **onboarding:** implement split onboarding flow and middleware checks ([7346380](https://github.com/trycompai/comp/commit/73463801f7eff32dcf34cf475ac675ea173aedbf)) # [1.47.0](https://github.com/trycompai/comp/compare/v1.46.0...v1.47.0) (2025-06-27) - ### Features -* **slack-notifications:** add Slack integration for Stripe webhook events ([76511b7](https://github.com/trycompai/comp/commit/76511b720ebdabc1c0d5b0a40b09ec036e2c2560)) -* **tracking:** implement unified tracking for onboarding and purchase events ([29bec28](https://github.com/trycompai/comp/commit/29bec2819f7ea7d5ffb24be5476207ab944ddfc6)) +- **slack-notifications:** add Slack integration for Stripe webhook events ([76511b7](https://github.com/trycompai/comp/commit/76511b720ebdabc1c0d5b0a40b09ec036e2c2560)) +- **tracking:** implement unified tracking for onboarding and purchase events ([29bec28](https://github.com/trycompai/comp/commit/29bec2819f7ea7d5ffb24be5476207ab944ddfc6)) # [1.46.0](https://github.com/trycompai/comp/compare/v1.45.1...v1.46.0) (2025-06-26) - ### Features -* add testimonial section and update pricing card layout ([63f3898](https://github.com/trycompai/comp/commit/63f38982bf0b3a2a6e3b3864652b334a1c539c84)) -* add trial badge to pricing card component ([0e4d92b](https://github.com/trycompai/comp/commit/0e4d92bd7a8cf251b64968e89ffd35425f3725fd)) -* update pricing card component with review link ([2164e5f](https://github.com/trycompai/comp/commit/2164e5f3e432327d41f77d9d80b7d7604969cdd3)) +- add testimonial section and update pricing card layout ([63f3898](https://github.com/trycompai/comp/commit/63f38982bf0b3a2a6e3b3864652b334a1c539c84)) +- add trial badge to pricing card component ([0e4d92b](https://github.com/trycompai/comp/commit/0e4d92bd7a8cf251b64968e89ffd35425f3725fd)) +- update pricing card component with review link ([2164e5f](https://github.com/trycompai/comp/commit/2164e5f3e432327d41f77d9d80b7d7604969cdd3)) ## [1.45.1](https://github.com/trycompai/comp/compare/v1.45.0...v1.45.1) (2025-06-26) - ### Bug Fixes -* improve error handling and logging in deploy-test-results workflow ([62f7696](https://github.com/trycompai/comp/commit/62f7696b5108d780f4bfe3dbddc5956d9057f5f4)) +- improve error handling and logging in deploy-test-results workflow ([62f7696](https://github.com/trycompai/comp/commit/62f7696b5108d780f4bfe3dbddc5956d9057f5f4)) # [1.45.0](https://github.com/trycompai/comp/compare/v1.44.0...v1.45.0) (2025-06-26) - ### Bug Fixes -* correct team size placeholder ([f84e79f](https://github.com/trycompai/comp/commit/f84e79fc6bb66a5180a88861be13253d3b3b39c9)) -* enhance billing page subscription checks and display ([9576af2](https://github.com/trycompai/comp/commit/9576af2099c4d24ac4d6e3250b443f9587dc2c1e)) -* update E2E test command in GitHub Actions workflow ([46597c3](https://github.com/trycompai/comp/commit/46597c3873ab32faf6b0200db4759a89977fcf07)) - +- correct team size placeholder ([f84e79f](https://github.com/trycompai/comp/commit/f84e79fc6bb66a5180a88861be13253d3b3b39c9)) +- enhance billing page subscription checks and display ([9576af2](https://github.com/trycompai/comp/commit/9576af2099c4d24ac4d6e3250b443f9587dc2c1e)) +- update E2E test command in GitHub Actions workflow ([46597c3](https://github.com/trycompai/comp/commit/46597c3873ab32faf6b0200db4759a89977fcf07)) ### Features -* add STARTER subscription type to Organization model and migration ([3f9c373](https://github.com/trycompai/comp/commit/3f9c373c5a8dd9d858dc50aa1e5c585b57cbb4ab)) -* update subscription handling and pricing structure ([b566650](https://github.com/trycompai/comp/commit/b566650656d19bdd9818e05c3159774371276024)) +- add STARTER subscription type to Organization model and migration ([3f9c373](https://github.com/trycompai/comp/commit/3f9c373c5a8dd9d858dc50aa1e5c585b57cbb4ab)) +- update subscription handling and pricing structure ([b566650](https://github.com/trycompai/comp/commit/b566650656d19bdd9818e05c3159774371276024)) # [1.44.0](https://github.com/trycompai/comp/compare/v1.43.3...v1.44.0) (2025-06-25) - ### Bug Fixes -* clarify subscription type comments in test-login route ([6d0ebf3](https://github.com/trycompai/comp/commit/6d0ebf3b0cf7a27fe8c230c8acbe7cd1bb00af90)) -* enhance E2E test for simple authentication flow ([7a00333](https://github.com/trycompai/comp/commit/7a00333dff8098dbd2c2a6f7acab36182c527980)) -* improve E2E test for simple authentication flow ([341df0a](https://github.com/trycompai/comp/commit/341df0a3d1ff61676ce2be9c4c743cc9ed8a98eb)) - +- clarify subscription type comments in test-login route ([6d0ebf3](https://github.com/trycompai/comp/commit/6d0ebf3b0cf7a27fe8c230c8acbe7cd1bb00af90)) +- enhance E2E test for simple authentication flow ([7a00333](https://github.com/trycompai/comp/commit/7a00333dff8098dbd2c2a6f7acab36182c527980)) +- improve E2E test for simple authentication flow ([341df0a](https://github.com/trycompai/comp/commit/341df0a3d1ff61676ce2be9c4c743cc9ed8a98eb)) ### Features -* add GitHub Actions workflow for deploying test results ([7d5fb40](https://github.com/trycompai/comp/commit/7d5fb40924dd1c7e00f9fe6d02a7e2dc34248a9e)) -* add testing framework and middleware tests ([1d022c6](https://github.com/trycompai/comp/commit/1d022c640f92fe45214d92bf073c21d6e4a94259)) -* disable Next button on setup form until current step has valid input ([cffae61](https://github.com/trycompai/comp/commit/cffae6140ed0c28ccd360b9199adc7a177bcd40a)) -* enhance testing infrastructure and add Playwright support ([12509ff](https://github.com/trycompai/comp/commit/12509ff1a2e09911f3b138c4ec8c4ee47b7e27a4)) -* implement E2E test for simple authentication flow ([a68f93f](https://github.com/trycompai/comp/commit/a68f93f1fcaed233b59d737f9331a23a6fe9fcb8)) -* implement MockRedis client for E2E testing in CI ([ed3ed8f](https://github.com/trycompai/comp/commit/ed3ed8f0a729c53f105ff0c96bf6e295165439a1)) -* integrate DotLottie animations into compliance components ([3f8bbed](https://github.com/trycompai/comp/commit/3f8bbedcef3a513b93d8ea8d45770cbfb0df548d)) +- add GitHub Actions workflow for deploying test results ([7d5fb40](https://github.com/trycompai/comp/commit/7d5fb40924dd1c7e00f9fe6d02a7e2dc34248a9e)) +- add testing framework and middleware tests ([1d022c6](https://github.com/trycompai/comp/commit/1d022c640f92fe45214d92bf073c21d6e4a94259)) +- disable Next button on setup form until current step has valid input ([cffae61](https://github.com/trycompai/comp/commit/cffae6140ed0c28ccd360b9199adc7a177bcd40a)) +- enhance testing infrastructure and add Playwright support ([12509ff](https://github.com/trycompai/comp/commit/12509ff1a2e09911f3b138c4ec8c4ee47b7e27a4)) +- implement E2E test for simple authentication flow ([a68f93f](https://github.com/trycompai/comp/commit/a68f93f1fcaed233b59d737f9331a23a6fe9fcb8)) +- implement MockRedis client for E2E testing in CI ([ed3ed8f](https://github.com/trycompai/comp/commit/ed3ed8f0a729c53f105ff0c96bf6e295165439a1)) +- integrate DotLottie animations into compliance components ([3f8bbed](https://github.com/trycompai/comp/commit/3f8bbedcef3a513b93d8ea8d45770cbfb0df548d)) ## [1.43.3](https://github.com/trycompai/comp/compare/v1.43.2...v1.43.3) (2025-06-25) - ### Bug Fixes -* fix issue with not being able to select assignee in policy and fix button text ([2dd6d93](https://github.com/trycompai/comp/commit/2dd6d930695523e65a36cee9bc273ef52c0d08ac)) +- fix issue with not being able to select assignee in policy and fix button text ([2dd6d93](https://github.com/trycompai/comp/commit/2dd6d930695523e65a36cee9bc273ef52c0d08ac)) ## [1.43.2](https://github.com/trycompai/comp/compare/v1.43.1...v1.43.2) (2025-06-25) - ### Bug Fixes -* fix bug with undefined theme ([901fa0c](https://github.com/trycompai/comp/commit/901fa0cff57dce3cb08924dd4efbe9b5e87831eb)) -* fix dark mode rendering for dub referral embed ([f1088d1](https://github.com/trycompai/comp/commit/f1088d1907cca32ececd7e09bb2cbbe52e52945b)) -* use email as fallback for name ([7ba3016](https://github.com/trycompai/comp/commit/7ba3016ab4605d5992849567540d62e17b9b50ea)) -* use taildwind theme instead of hardcoding system ([97d257e](https://github.com/trycompai/comp/commit/97d257e54bb31b8549a0e34afe9715b395c64b36)) +- fix bug with undefined theme ([901fa0c](https://github.com/trycompai/comp/commit/901fa0cff57dce3cb08924dd4efbe9b5e87831eb)) +- fix dark mode rendering for dub referral embed ([f1088d1](https://github.com/trycompai/comp/commit/f1088d1907cca32ececd7e09bb2cbbe52e52945b)) +- use email as fallback for name ([7ba3016](https://github.com/trycompai/comp/commit/7ba3016ab4605d5992849567540d62e17b9b50ea)) +- use taildwind theme instead of hardcoding system ([97d257e](https://github.com/trycompai/comp/commit/97d257e54bb31b8549a0e34afe9715b395c64b36)) ## [1.43.1](https://github.com/trycompai/comp/compare/v1.43.0...v1.43.1) (2025-06-24) - ### Bug Fixes -* update formatting and structure in various files ([939bf07](https://github.com/trycompai/comp/commit/939bf07521131891e9acb731a1d5c15aff56fc78)) +- update formatting and structure in various files ([939bf07](https://github.com/trycompai/comp/commit/939bf07521131891e9acb731a1d5c15aff56fc78)) # [1.43.0](https://github.com/trycompai/comp/compare/v1.42.0...v1.43.0) (2025-06-24) - ### Bug Fixes -* correct URL formatting for logo retrieval in getCachedSites function ([87bd99f](https://github.com/trycompai/comp/commit/87bd99fe0426233ff2b8356b05a44e80c4becd79)) - +- correct URL formatting for logo retrieval in getCachedSites function ([87bd99f](https://github.com/trycompai/comp/commit/87bd99fe0426233ff2b8356b05a44e80c4becd79)) ### Features -* add logo existence check to API endpoint for cached websites ([60d8c95](https://github.com/trycompai/comp/commit/60d8c95318197f17996fca6cf2d1a470373f275f)) +- add logo existence check to API endpoint for cached websites ([60d8c95](https://github.com/trycompai/comp/commit/60d8c95318197f17996fca6cf2d1a470373f275f)) # [1.42.0](https://github.com/trycompai/comp/compare/v1.41.1...v1.42.0) (2025-06-24) - ### Bug Fixes -* add back create fleet lable to create org function ([2f1ca1f](https://github.com/trycompai/comp/commit/2f1ca1f7a6685d56afd72e664cf89e9512eb5aa9)) -* add error handling for null hosts ([c76c0bb](https://github.com/trycompai/comp/commit/c76c0bb3591395def2b05689216e080eb1ef5998)) -* add mdm step to agent instructions ([24b0e83](https://github.com/trycompai/comp/commit/24b0e83b4b577b1025247be94c44c7aa9a20ca21)) -* adjust scale of AnimatedGradientBackground in upgrade page for improved visual consistency ([6f8821e](https://github.com/trycompai/comp/commit/6f8821e7442147722a732bd545d3c290c61c3d35)) -* **booking:** remove unnecessary margin from Phone icon in BookingDialog component ([b39c654](https://github.com/trycompai/comp/commit/b39c6543e283ca9d0b9974ad17c90551bc9f9500)) -* cast stripeCustomerId to string before syncing data ([3887f71](https://github.com/trycompai/comp/commit/3887f71a0cb6c2e1b213b1cc88fb70b32656ebc3)) -* **checkout:** refactor CheckoutCompleteDialog to improve plan type handling ([e91f1f0](https://github.com/trycompai/comp/commit/e91f1f045d8e02fdc6f02b7cf95441b4753e3867)) -* fix issue with s3 url sanitization ([9ab26a0](https://github.com/trycompai/comp/commit/9ab26a0022f213fbd1e796c66ed511d4c3ed75b0)) -* fix issue with sanitizing html ([1acccc6](https://github.com/trycompai/comp/commit/1acccc6ad771c5c067e7449a4efa1023e50e6d00)) -* fix issues with roles while inviting users ([a1e7a7d](https://github.com/trycompai/comp/commit/a1e7a7dba83a0cf513391867549b4ebd2b4ae809)) -* **layout:** adjust navbar height for improved layout consistency ([a61b794](https://github.com/trycompai/comp/commit/a61b794a961425ba1ca9cc557275d4e9357aa7a0)) -* move agent logic to client side instead of server side ([3ac4874](https://github.com/trycompai/comp/commit/3ac48744657717f4b5a9f3b3b395090f1b00d9e9)) -* **RecentAuditLogs:** adjust padding for no activity message display ([98c278a](https://github.com/trycompai/comp/commit/98c278acaf7355dacc8262576de66f71bafb57d4)) -* roadmap link on readme ([66afcbd](https://github.com/trycompai/comp/commit/66afcbde132ddadfdf3e6710463c168912877e6a)) -* start download of installer from the browser ([dde4546](https://github.com/trycompai/comp/commit/dde45460db81e02fe6f610fc93858d7ba6adea6e)) -* try catch fetching the device so there's no error ([20b7f14](https://github.com/trycompai/comp/commit/20b7f14720f2039d9ce8981086b216fe0e16e2df)) -* update Stripe customer ID retrieval to use organization ID instead of user ID ([ce4ef80](https://github.com/trycompai/comp/commit/ce4ef80a0503c55877f3d7374d690ef4a9317bec)) -* update subscription data handling and remove unused checkout components ([3a50c55](https://github.com/trycompai/comp/commit/3a50c559b65bdbfed90294233ca711081dae7e58)) -* validate S3 URL host ([d744f74](https://github.com/trycompai/comp/commit/d744f742d4679c7ea5c8e8649c9ec2d0354cb4b6)) - - -### Features - -* add animated pricing banner to upgrade page ([0599fcc](https://github.com/trycompai/comp/commit/0599fccc1668feb8407c435da01f6b2bbb0531d8)) -* add API endpoint to fetch cached published websites from the database ([4792901](https://github.com/trycompai/comp/commit/479290160e1ed059afb696d1f3194d496cce472d)) -* add dub referral tab and embed ([7a5e166](https://github.com/trycompai/comp/commit/7a5e166d31d96de227b2300a1c068ba35c0a174d)) -* add rage mode and scroll rotation effects to AnimatedGradientBackground ([bd516f7](https://github.com/trycompai/comp/commit/bd516f7975ae5db7915dc75ea0ff26e13360c247)) -* add SelectionIndicator component and enhance pricing card interactions ([04835c6](https://github.com/trycompai/comp/commit/04835c69ed99b1e9cf112ac886585a5afe0c2058)) -* add support for windows agent ([f18e8a2](https://github.com/trycompai/comp/commit/f18e8a263bf213f23d12eaa324e03d407b7bf5d7)) -* add tables support in policy editor ([9afd1ed](https://github.com/trycompai/comp/commit/9afd1edde0c803d0247ee43250e42f9d35e165c5)) -* add tables support in policy editor ([9822d95](https://github.com/trycompai/comp/commit/9822d952b7164cecc3d0065dbdd6691ae26b4b58)) -* **api:** add server directive to stripe session generation ([1c9f957](https://github.com/trycompai/comp/commit/1c9f957c9245ee2a3f06788d2d5fd1e22f5a4f28)) -* **api:** enhance Stripe checkout session handling and logging ([3eff9b4](https://github.com/trycompai/comp/commit/3eff9b480a0ae150758adff131b177a0b43de940)) -* **auth:** add session hook to set active organization ([6de3ded](https://github.com/trycompai/comp/commit/6de3ded2322efa912b2972003b5dbe5c14887e95)) -* **auth:** implement organization access checks in layout components ([a1d3ad7](https://github.com/trycompai/comp/commit/a1d3ad7f787ff9165d948f9cc4e36ed99e012cc9)) -* **auth:** refactor authentication page and components for improved user experience ([fb4fa7d](https://github.com/trycompai/comp/commit/fb4fa7d2e9c1b6dd96862eeb724a297a8a9d713c)) -* **billing:** enhance subscription management and UI components ([1cf32c2](https://github.com/trycompai/comp/commit/1cf32c29f9105fa1eb51f3288f680629468f4bab)) -* **checkout:** add CheckoutCompleteDialog component and integrate confetti animation ([4bd6018](https://github.com/trycompai/comp/commit/4bd6018e3e5b18ecd4fa2aac32775f5cf04c0a6a)) -* **checkout:** enhance CheckoutCompleteDialog with updated features and descriptions ([2182c9e](https://github.com/trycompai/comp/commit/2182c9e1e44df54fbf951fb00d7db546aa0f7ebf)) -* **database:** add nanoid function for generating URL-safe unique IDs ([c3608e1](https://github.com/trycompai/comp/commit/c3608e184c15ccf2222d6ab1557b26de589749fa)) -* enhance AnimatedGradientBackground with pulse and glow effects ([7bd80c0](https://github.com/trycompai/comp/commit/7bd80c06406bed1d58ae8121c3c67a7340da9855)) -* enhance onboarding step input with website field ([7062eb3](https://github.com/trycompai/comp/commit/7062eb3c35a7cf5b28afcc9784d60183fa7e9b08)) -* enhance OnboardingStepInput and SelectPills components ([0079556](https://github.com/trycompai/comp/commit/0079556862b948c364e379381c3b61bee735e1b1)) -* enhance Stripe checkout session handling by validating organization membership and managing customer records ([3901d8d](https://github.com/trycompai/comp/commit/3901d8d59b78c57d0ccedde74ad8ceebb2edd5dd)) -* enhance upgrade page with logo marquee and review widget ([a8c6ff1](https://github.com/trycompai/comp/commit/a8c6ff1702bf86f63e18167bc1bf9efee69ad447)) -* **env:** add new Fleet-related environment variables to configuration ([99eb8b1](https://github.com/trycompai/comp/commit/99eb8b1c6be16607872184320ec61cd7e7c1d565)) -* **env:** add optional Stripe subscription price ID to environment variables ([27a2d3d](https://github.com/trycompai/comp/commit/27a2d3d6a75dfffd2c870887d16159ea35acca61)) -* **env:** add Stripe subscription price ID to environment configuration ([f3da9b6](https://github.com/trycompai/comp/commit/f3da9b69a38207ecf314bdd44531b6714119f929)) -* implement middleware for domain-based routing and organization handling ([7be7b9f](https://github.com/trycompai/comp/commit/7be7b9f395c89a7a680ded944373f5157bbb7cf0)) -* implement self-serve subscription option and update pricing details ([69bd082](https://github.com/trycompai/comp/commit/69bd0820204b0c68a2adf0ddbec5c858f5954cb0)) -* improve scroll handling in AnimatedGradientBackground ([cf98196](https://github.com/trycompai/comp/commit/cf98196b57a1c1c88bf2e1b8aeee187e26a7e9eb)) -* integrate Senja Review Widget into pricing cards ([24ecd81](https://github.com/trycompai/comp/commit/24ecd8188bca7c3d509de12dcb29c7662dff63c1)) -* **invite:** implement invitation handling and redirection ([4dccbbc](https://github.com/trycompai/comp/commit/4dccbbcef21eabde1183917d25ab9d40f442c92f)) -* **layout:** wrap main content in Suspense for improved loading handling ([29c7cf2](https://github.com/trycompai/comp/commit/29c7cf27c5dc29c50541efeba40dc67a70993a89)) -* **login:** add option to use another sign-in method after sending magic link ([957c25f](https://github.com/trycompai/comp/commit/957c25f5244bc1bb9d44ea3581b57496b06431cb)) -* **metadata:** add generateMetadata function for Controls, Tasks, and Cloud Tests pages ([221106c](https://github.com/trycompai/comp/commit/221106c388dfad98bf16f79ed339f14fd6640c1f)) -* **middleware:** allow unauthenticated access to invite routes ([04c64d8](https://github.com/trycompai/comp/commit/04c64d86e2d14e60d1e03f6ffbe413f3d88ebc9d)) -* **middleware:** handle old invite route format for direct visits ([8a47db0](https://github.com/trycompai/comp/commit/8a47db04fcdd628cffc856167df1fdff40ac62dd)) -* **middleware:** implement authentication middleware for route protection ([514e007](https://github.com/trycompai/comp/commit/514e00700d674fdc0b0be2300c7c156cd824392e)) -* **middleware:** implement organization session handling in middleware ([5bd53b9](https://github.com/trycompai/comp/commit/5bd53b9d4732ef4171b9baf244655df7fccb8c4e)) -* **policy:** implement content sanitization to remove unsupported marks ([61bdce4](https://github.com/trycompai/comp/commit/61bdce476a69e9fdbc78d3ed33a5ffc0554a2907)) -* **pricing:** add '12-month minimum term' to paid features list in PricingCards component ([1a45ac4](https://github.com/trycompai/comp/commit/1a45ac46c2c48bed96a3571a41cb68f738c0c728)) -* **pricing:** add BookingDialog component to PricingCards for scheduling calls ([63a68b3](https://github.com/trycompai/comp/commit/63a68b35ef4fac2d4db71be7260633f7f521efef)) -* redo portal tasks to be cleaner ([806f07f](https://github.com/trycompai/comp/commit/806f07fe1c5a01b4aa33addc07e03e3ed5a04ad4)) -* refactor subscription handling and introduce self-serve plan ([9b6adfc](https://github.com/trycompai/comp/commit/9b6adfc0cc525d4b642df3dbc6808d8463ce33f9)) -* **setup:** add SetupHeader component to enhance user experience ([3cadc5b](https://github.com/trycompai/comp/commit/3cadc5ba5fa02648cd5122e4e3a64fd341b29ff2)) -* **setup:** enhance framework selection loading state management ([6106974](https://github.com/trycompai/comp/commit/6106974c259150055bcee7f855cd84f56dae3b42)) -* **setup:** enhance onboarding process and organization setup ([b1fdabf](https://github.com/trycompai/comp/commit/b1fdabfe02fda140455b275d3e3afca9b538754f)) -* **setup:** enhance organization setup and upgrade flow ([e1d57d4](https://github.com/trycompai/comp/commit/e1d57d42dfa7e66e439328f5c26bacd26d1968bb)) -* **setup:** implement organization creation and onboarding enhancements ([229744b](https://github.com/trycompai/comp/commit/229744bbaa9c1757102ea48882c5cba51e7aa7c2)) -* **setup:** implement organization setup flow and subscription checks ([b94e7f3](https://github.com/trycompai/comp/commit/b94e7f348035aacdc6a1df613fb31d018ab474f6)) -* **stripe:** add new Stripe integration features ([3749e78](https://github.com/trycompai/comp/commit/3749e7872c6b455d617b7a3c9c80760fce953823)) -* **stripe:** define subscription data type for Stripe synchronization ([5da6c67](https://github.com/trycompai/comp/commit/5da6c670492de5145957fb08d2f932096c6701df)) -* **stripe:** implement subscription data retrieval and cache management ([47815bc](https://github.com/trycompai/comp/commit/47815bc598eeed24bf68e7cf8b16227b5c76a080)) -* **tests:** add layout, loading, and page components for cloud compliance tests ([9c14c28](https://github.com/trycompai/comp/commit/9c14c28ffa1eef9b27fa99f9c381112f1f08600c)) -* update environment variables for Stripe pricing plans ([c61922f](https://github.com/trycompai/comp/commit/c61922fd1d5393116a7ffef4507809accde07e7f)) -* update OrganizationSetupForm and routing logic ([f0c14b9](https://github.com/trycompai/comp/commit/f0c14b9e11a1d55dc949f4ec2932f0de0cce2ff0)) -* update pricing cards with new features and subscription handling ([3753cc5](https://github.com/trycompai/comp/commit/3753cc5510938558b7177cb05c729f2a1195fd45)) +- add back create fleet lable to create org function ([2f1ca1f](https://github.com/trycompai/comp/commit/2f1ca1f7a6685d56afd72e664cf89e9512eb5aa9)) +- add error handling for null hosts ([c76c0bb](https://github.com/trycompai/comp/commit/c76c0bb3591395def2b05689216e080eb1ef5998)) +- add mdm step to agent instructions ([24b0e83](https://github.com/trycompai/comp/commit/24b0e83b4b577b1025247be94c44c7aa9a20ca21)) +- adjust scale of AnimatedGradientBackground in upgrade page for improved visual consistency ([6f8821e](https://github.com/trycompai/comp/commit/6f8821e7442147722a732bd545d3c290c61c3d35)) +- **booking:** remove unnecessary margin from Phone icon in BookingDialog component ([b39c654](https://github.com/trycompai/comp/commit/b39c6543e283ca9d0b9974ad17c90551bc9f9500)) +- cast stripeCustomerId to string before syncing data ([3887f71](https://github.com/trycompai/comp/commit/3887f71a0cb6c2e1b213b1cc88fb70b32656ebc3)) +- **checkout:** refactor CheckoutCompleteDialog to improve plan type handling ([e91f1f0](https://github.com/trycompai/comp/commit/e91f1f045d8e02fdc6f02b7cf95441b4753e3867)) +- fix issue with s3 url sanitization ([9ab26a0](https://github.com/trycompai/comp/commit/9ab26a0022f213fbd1e796c66ed511d4c3ed75b0)) +- fix issue with sanitizing html ([1acccc6](https://github.com/trycompai/comp/commit/1acccc6ad771c5c067e7449a4efa1023e50e6d00)) +- fix issues with roles while inviting users ([a1e7a7d](https://github.com/trycompai/comp/commit/a1e7a7dba83a0cf513391867549b4ebd2b4ae809)) +- **layout:** adjust navbar height for improved layout consistency ([a61b794](https://github.com/trycompai/comp/commit/a61b794a961425ba1ca9cc557275d4e9357aa7a0)) +- move agent logic to client side instead of server side ([3ac4874](https://github.com/trycompai/comp/commit/3ac48744657717f4b5a9f3b3b395090f1b00d9e9)) +- **RecentAuditLogs:** adjust padding for no activity message display ([98c278a](https://github.com/trycompai/comp/commit/98c278acaf7355dacc8262576de66f71bafb57d4)) +- roadmap link on readme ([66afcbd](https://github.com/trycompai/comp/commit/66afcbde132ddadfdf3e6710463c168912877e6a)) +- start download of installer from the browser ([dde4546](https://github.com/trycompai/comp/commit/dde45460db81e02fe6f610fc93858d7ba6adea6e)) +- try catch fetching the device so there's no error ([20b7f14](https://github.com/trycompai/comp/commit/20b7f14720f2039d9ce8981086b216fe0e16e2df)) +- update Stripe customer ID retrieval to use organization ID instead of user ID ([ce4ef80](https://github.com/trycompai/comp/commit/ce4ef80a0503c55877f3d7374d690ef4a9317bec)) +- update subscription data handling and remove unused checkout components ([3a50c55](https://github.com/trycompai/comp/commit/3a50c559b65bdbfed90294233ca711081dae7e58)) +- validate S3 URL host ([d744f74](https://github.com/trycompai/comp/commit/d744f742d4679c7ea5c8e8649c9ec2d0354cb4b6)) + +### Features + +- add animated pricing banner to upgrade page ([0599fcc](https://github.com/trycompai/comp/commit/0599fccc1668feb8407c435da01f6b2bbb0531d8)) +- add API endpoint to fetch cached published websites from the database ([4792901](https://github.com/trycompai/comp/commit/479290160e1ed059afb696d1f3194d496cce472d)) +- add dub referral tab and embed ([7a5e166](https://github.com/trycompai/comp/commit/7a5e166d31d96de227b2300a1c068ba35c0a174d)) +- add rage mode and scroll rotation effects to AnimatedGradientBackground ([bd516f7](https://github.com/trycompai/comp/commit/bd516f7975ae5db7915dc75ea0ff26e13360c247)) +- add SelectionIndicator component and enhance pricing card interactions ([04835c6](https://github.com/trycompai/comp/commit/04835c69ed99b1e9cf112ac886585a5afe0c2058)) +- add support for windows agent ([f18e8a2](https://github.com/trycompai/comp/commit/f18e8a263bf213f23d12eaa324e03d407b7bf5d7)) +- add tables support in policy editor ([9afd1ed](https://github.com/trycompai/comp/commit/9afd1edde0c803d0247ee43250e42f9d35e165c5)) +- add tables support in policy editor ([9822d95](https://github.com/trycompai/comp/commit/9822d952b7164cecc3d0065dbdd6691ae26b4b58)) +- **api:** add server directive to stripe session generation ([1c9f957](https://github.com/trycompai/comp/commit/1c9f957c9245ee2a3f06788d2d5fd1e22f5a4f28)) +- **api:** enhance Stripe checkout session handling and logging ([3eff9b4](https://github.com/trycompai/comp/commit/3eff9b480a0ae150758adff131b177a0b43de940)) +- **auth:** add session hook to set active organization ([6de3ded](https://github.com/trycompai/comp/commit/6de3ded2322efa912b2972003b5dbe5c14887e95)) +- **auth:** implement organization access checks in layout components ([a1d3ad7](https://github.com/trycompai/comp/commit/a1d3ad7f787ff9165d948f9cc4e36ed99e012cc9)) +- **auth:** refactor authentication page and components for improved user experience ([fb4fa7d](https://github.com/trycompai/comp/commit/fb4fa7d2e9c1b6dd96862eeb724a297a8a9d713c)) +- **billing:** enhance subscription management and UI components ([1cf32c2](https://github.com/trycompai/comp/commit/1cf32c29f9105fa1eb51f3288f680629468f4bab)) +- **checkout:** add CheckoutCompleteDialog component and integrate confetti animation ([4bd6018](https://github.com/trycompai/comp/commit/4bd6018e3e5b18ecd4fa2aac32775f5cf04c0a6a)) +- **checkout:** enhance CheckoutCompleteDialog with updated features and descriptions ([2182c9e](https://github.com/trycompai/comp/commit/2182c9e1e44df54fbf951fb00d7db546aa0f7ebf)) +- **database:** add nanoid function for generating URL-safe unique IDs ([c3608e1](https://github.com/trycompai/comp/commit/c3608e184c15ccf2222d6ab1557b26de589749fa)) +- enhance AnimatedGradientBackground with pulse and glow effects ([7bd80c0](https://github.com/trycompai/comp/commit/7bd80c06406bed1d58ae8121c3c67a7340da9855)) +- enhance onboarding step input with website field ([7062eb3](https://github.com/trycompai/comp/commit/7062eb3c35a7cf5b28afcc9784d60183fa7e9b08)) +- enhance OnboardingStepInput and SelectPills components ([0079556](https://github.com/trycompai/comp/commit/0079556862b948c364e379381c3b61bee735e1b1)) +- enhance Stripe checkout session handling by validating organization membership and managing customer records ([3901d8d](https://github.com/trycompai/comp/commit/3901d8d59b78c57d0ccedde74ad8ceebb2edd5dd)) +- enhance upgrade page with logo marquee and review widget ([a8c6ff1](https://github.com/trycompai/comp/commit/a8c6ff1702bf86f63e18167bc1bf9efee69ad447)) +- **env:** add new Fleet-related environment variables to configuration ([99eb8b1](https://github.com/trycompai/comp/commit/99eb8b1c6be16607872184320ec61cd7e7c1d565)) +- **env:** add optional Stripe subscription price ID to environment variables ([27a2d3d](https://github.com/trycompai/comp/commit/27a2d3d6a75dfffd2c870887d16159ea35acca61)) +- **env:** add Stripe subscription price ID to environment configuration ([f3da9b6](https://github.com/trycompai/comp/commit/f3da9b69a38207ecf314bdd44531b6714119f929)) +- implement middleware for domain-based routing and organization handling ([7be7b9f](https://github.com/trycompai/comp/commit/7be7b9f395c89a7a680ded944373f5157bbb7cf0)) +- implement self-serve subscription option and update pricing details ([69bd082](https://github.com/trycompai/comp/commit/69bd0820204b0c68a2adf0ddbec5c858f5954cb0)) +- improve scroll handling in AnimatedGradientBackground ([cf98196](https://github.com/trycompai/comp/commit/cf98196b57a1c1c88bf2e1b8aeee187e26a7e9eb)) +- integrate Senja Review Widget into pricing cards ([24ecd81](https://github.com/trycompai/comp/commit/24ecd8188bca7c3d509de12dcb29c7662dff63c1)) +- **invite:** implement invitation handling and redirection ([4dccbbc](https://github.com/trycompai/comp/commit/4dccbbcef21eabde1183917d25ab9d40f442c92f)) +- **layout:** wrap main content in Suspense for improved loading handling ([29c7cf2](https://github.com/trycompai/comp/commit/29c7cf27c5dc29c50541efeba40dc67a70993a89)) +- **login:** add option to use another sign-in method after sending magic link ([957c25f](https://github.com/trycompai/comp/commit/957c25f5244bc1bb9d44ea3581b57496b06431cb)) +- **metadata:** add generateMetadata function for Controls, Tasks, and Cloud Tests pages ([221106c](https://github.com/trycompai/comp/commit/221106c388dfad98bf16f79ed339f14fd6640c1f)) +- **middleware:** allow unauthenticated access to invite routes ([04c64d8](https://github.com/trycompai/comp/commit/04c64d86e2d14e60d1e03f6ffbe413f3d88ebc9d)) +- **middleware:** handle old invite route format for direct visits ([8a47db0](https://github.com/trycompai/comp/commit/8a47db04fcdd628cffc856167df1fdff40ac62dd)) +- **middleware:** implement authentication middleware for route protection ([514e007](https://github.com/trycompai/comp/commit/514e00700d674fdc0b0be2300c7c156cd824392e)) +- **middleware:** implement organization session handling in middleware ([5bd53b9](https://github.com/trycompai/comp/commit/5bd53b9d4732ef4171b9baf244655df7fccb8c4e)) +- **policy:** implement content sanitization to remove unsupported marks ([61bdce4](https://github.com/trycompai/comp/commit/61bdce476a69e9fdbc78d3ed33a5ffc0554a2907)) +- **pricing:** add '12-month minimum term' to paid features list in PricingCards component ([1a45ac4](https://github.com/trycompai/comp/commit/1a45ac46c2c48bed96a3571a41cb68f738c0c728)) +- **pricing:** add BookingDialog component to PricingCards for scheduling calls ([63a68b3](https://github.com/trycompai/comp/commit/63a68b35ef4fac2d4db71be7260633f7f521efef)) +- redo portal tasks to be cleaner ([806f07f](https://github.com/trycompai/comp/commit/806f07fe1c5a01b4aa33addc07e03e3ed5a04ad4)) +- refactor subscription handling and introduce self-serve plan ([9b6adfc](https://github.com/trycompai/comp/commit/9b6adfc0cc525d4b642df3dbc6808d8463ce33f9)) +- **setup:** add SetupHeader component to enhance user experience ([3cadc5b](https://github.com/trycompai/comp/commit/3cadc5ba5fa02648cd5122e4e3a64fd341b29ff2)) +- **setup:** enhance framework selection loading state management ([6106974](https://github.com/trycompai/comp/commit/6106974c259150055bcee7f855cd84f56dae3b42)) +- **setup:** enhance onboarding process and organization setup ([b1fdabf](https://github.com/trycompai/comp/commit/b1fdabfe02fda140455b275d3e3afca9b538754f)) +- **setup:** enhance organization setup and upgrade flow ([e1d57d4](https://github.com/trycompai/comp/commit/e1d57d42dfa7e66e439328f5c26bacd26d1968bb)) +- **setup:** implement organization creation and onboarding enhancements ([229744b](https://github.com/trycompai/comp/commit/229744bbaa9c1757102ea48882c5cba51e7aa7c2)) +- **setup:** implement organization setup flow and subscription checks ([b94e7f3](https://github.com/trycompai/comp/commit/b94e7f348035aacdc6a1df613fb31d018ab474f6)) +- **stripe:** add new Stripe integration features ([3749e78](https://github.com/trycompai/comp/commit/3749e7872c6b455d617b7a3c9c80760fce953823)) +- **stripe:** define subscription data type for Stripe synchronization ([5da6c67](https://github.com/trycompai/comp/commit/5da6c670492de5145957fb08d2f932096c6701df)) +- **stripe:** implement subscription data retrieval and cache management ([47815bc](https://github.com/trycompai/comp/commit/47815bc598eeed24bf68e7cf8b16227b5c76a080)) +- **tests:** add layout, loading, and page components for cloud compliance tests ([9c14c28](https://github.com/trycompai/comp/commit/9c14c28ffa1eef9b27fa99f9c381112f1f08600c)) +- update environment variables for Stripe pricing plans ([c61922f](https://github.com/trycompai/comp/commit/c61922fd1d5393116a7ffef4507809accde07e7f)) +- update OrganizationSetupForm and routing logic ([f0c14b9](https://github.com/trycompai/comp/commit/f0c14b9e11a1d55dc949f4ec2932f0de0cce2ff0)) +- update pricing cards with new features and subscription handling ([3753cc5](https://github.com/trycompai/comp/commit/3753cc5510938558b7177cb05c729f2a1195fd45)) ## [1.41.1](https://github.com/trycompai/comp/compare/v1.41.0...v1.41.1) (2025-06-12) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4424ca6e3..6fb3bc3e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,7 +93,6 @@ To develop locally: ``` 5. Set up your `.env` file: - - Duplicate `.env.example` to `.env`. - Use `openssl rand -base64 32` to generate a key and add it under `SECRET_KEY` in the `.env` file. - Setup Trigger.dev diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..4db049d33 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,116 @@ +# ============================================================================= +# STAGE 1: Dependencies - Install and cache workspace dependencies +# ============================================================================= +FROM oven/bun:1.2.8 AS deps + +WORKDIR /app + +# Copy workspace configuration +COPY package.json bun.lock bunfig.toml ./ + +# Copy package.json files for all packages +COPY packages/db/package.json ./packages/db/ +COPY packages/kv/package.json ./packages/kv/ +COPY packages/ui/package.json ./packages/ui/ +COPY packages/email/package.json ./packages/email/ +COPY packages/integrations/package.json ./packages/integrations/ +COPY packages/utils/package.json ./packages/utils/ +COPY packages/tsconfig/package.json ./packages/tsconfig/ +COPY packages/analytics/package.json ./packages/analytics/ + +# Copy app package.json files +COPY apps/app/package.json ./apps/app/ +COPY apps/portal/package.json ./apps/portal/ + +# Install all dependencies +RUN PRISMA_SKIP_POSTINSTALL_GENERATE=true bun install --frozen-lockfile + +# ============================================================================= +# STAGE 2: Ultra-Minimal Migrator - Only Prisma +# ============================================================================= +FROM oven/bun:1.2.8 AS migrator + +WORKDIR /app + +# Copy Prisma schema and migration files +COPY packages/db/prisma ./packages/db/prisma + +# Create minimal package.json for Prisma +RUN echo '{"name":"migrator","type":"module","dependencies":{"prisma":"^6.9.0","@prisma/client":"^6.9.0"}}' > package.json + +# Install ONLY Prisma dependencies +RUN bun install + +# Generate Prisma client +RUN cd packages/db && bunx prisma generate + +# Default command for migrations +CMD ["bunx", "prisma", "migrate", "deploy", "--schema=packages/db/prisma/schema.prisma"] + +# ============================================================================= +# STAGE 3: App Builder +# ============================================================================= +FROM deps AS app-builder + +WORKDIR /app + +# Copy all source code needed for build +COPY packages ./packages +COPY apps/app ./apps/app + +# Generate Prisma client in the full workspace context +RUN cd packages/db && bunx prisma generate + +# Build the app +RUN cd apps/app && SKIP_ENV_VALIDATION=true bun run build + +# ============================================================================= +# STAGE 4: App Production +# ============================================================================= +FROM oven/bun:1.2.8 AS app + +WORKDIR /app + +# Copy the built app and all necessary dependencies from builder +COPY --from=app-builder /app/apps/app/.next ./apps/app/.next +COPY --from=app-builder /app/apps/app/package.json ./apps/app/ +COPY --from=app-builder /app/package.json ./ +COPY --from=app-builder /app/node_modules ./node_modules +COPY --from=app-builder /app/packages ./packages + +EXPOSE 3000 +CMD ["bun", "run", "--cwd", "apps/app", "start"] + +# ============================================================================= +# STAGE 5: Portal Builder +# ============================================================================= +FROM deps AS portal-builder + +WORKDIR /app + +# Copy all source code needed for build +COPY packages ./packages +COPY apps/portal ./apps/portal + +# Generate Prisma client +RUN cd packages/db && bunx prisma generate + +# Build the portal +RUN cd apps/portal && SKIP_ENV_VALIDATION=true bun run build + +# ============================================================================= +# STAGE 6: Portal Production +# ============================================================================= +FROM oven/bun:1.2.8 AS portal + +WORKDIR /app + +# Copy the built portal and all necessary dependencies from builder +COPY --from=portal-builder /app/apps/portal/.next ./apps/portal/.next +COPY --from=portal-builder /app/apps/portal/package.json ./apps/portal/ +COPY --from=portal-builder /app/package.json ./ +COPY --from=portal-builder /app/node_modules ./node_modules +COPY --from=portal-builder /app/packages ./packages + +EXPOSE 3000 +CMD ["bun", "run", "--cwd", "apps/portal", "start"] \ No newline at end of file diff --git a/README.md b/README.md index fe408d401..8080ef75b 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,6 @@ Start and initialize the PostgreSQL database using Docker: ``` 2. Default credentials: - - Database name: `comp` - Username: `postgres` - Password: `postgres` @@ -307,7 +306,6 @@ bun docker:clean If `.env` files don’t load values as expected, you can hard-code the following: - **`comp/packages/kv/src/index.ts`** → Hard-coded Redis client credentials: - - **URL**: The Redis URL needs to start with `https`. Example: ```sh url: "https://default:AXhaAA***MA@charmed-wombat-3**0.upstash.io:6379" @@ -318,7 +316,6 @@ If `.env` files don’t load values as expected, you can hard-code the following ``` - **`comp/packages/db/prisma/schema.prisma`** → Hard-coded `DATABASE_URL`: - - Example: `sh datasource db { @@ -328,7 +325,6 @@ If `.env` files don’t load values as expected, you can hard-code the following ` - **`comp/apps/portal/src/app/lib/auth.ts`** → Hard-coded Google environment variables `clientId`, `clientSecret` under `socialProviders/google`: - - Example: ```js socialProviders: { @@ -379,6 +375,45 @@ Steps to deploy Comp AI on Docker are coming soon. Steps to deploy Comp AI on Vercel are coming soon. +## 📦 Package Publishing + +This repository uses semantic-release to automatically publish packages to npm when merging to the `release` branch. The following packages are published: + +- `@comp/db` - Database utilities with Prisma client +- `@comp/email` - Email templates and components +- `@comp/kv` - Key-value store utilities using Upstash Redis +- `@comp/ui` - UI component library with Tailwind CSS + +### Setup + +1. **NPM Token**: Add your npm token as `NPM_TOKEN` in GitHub repository secrets +2. **Release Branch**: Create and merge PRs into the `release` branch to trigger publishing +3. **Versioning**: Uses conventional commits for automatic version bumping + +### Usage + +```bash +# Install a published package +npm install @comp/ui + +# Use in your project +import { Button } from '@comp/ui/button' +import { client } from '@comp/kv' +``` + +### Development + +```bash +# Build all packages +bun run build + +# Build specific package +bun run -F @comp/ui build + +# Test packages locally +bun run release:packages --dry-run +``` + ## Contributors diff --git a/apps/app/Dockerfile_Backup b/apps/app/Dockerfile_Backup new file mode 100644 index 000000000..0f4929ac8 --- /dev/null +++ b/apps/app/Dockerfile_Backup @@ -0,0 +1,29 @@ +# Use Node.js Alpine for smaller runtime image +FROM node:18-alpine AS runtime + +WORKDIR /app + +# Install curl for health checks +RUN apk add --no-cache curl + +# Set production environment variables +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +ENV NODE_TLS_REJECT_UNAUTHORIZED=0 + +# Copy the complete standalone build from CodeBuild +COPY .next/standalone ./ + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nextjs -u 1001 && \ + chown -R nextjs:nodejs /app + +USER nextjs + +EXPOSE 3000 + +# Use node to run the standalone server - handle monorepo structure +CMD ["sh", "-c", "if [ -f apps/app/server.js ]; then node apps/app/server.js; elif [ -f server.js ]; then node server.js; elif [ -f index.js ]; then node index.js; else echo 'No entry point found' && exit 1; fi"] \ No newline at end of file diff --git a/apps/app/buildspec.yml b/apps/app/buildspec.yml new file mode 100644 index 000000000..39e165825 --- /dev/null +++ b/apps/app/buildspec.yml @@ -0,0 +1,102 @@ +version: 0.2 + +phases: + pre_build: + commands: + - echo "Logging in to Amazon ECR..." + - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com + - REPOSITORY_URI=$ECR_REPOSITORY_URI + - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) + - IMAGE_TAG=${COMMIT_HASH:=latest} + - echo "Installing dependencies..." + - curl -fsSL https://bun.sh/install | bash + + build: + commands: + # Environment setup + - export PATH="/root/.bun/bin:$PATH" + - export PGSSLMODE=require + - export NEXT_TELEMETRY_DISABLED=1 + - export UV_THREADPOOL_SIZE=36 + - export NODE_OPTIONS="--max-old-space-size=65536" + + # Navigate to app directory + - cd apps/$APP_NAME + + # Install dependencies + - echo "Installing dependencies..." + - SKIP_ENV_VALIDATION=true bun install --frozen-lockfile --concurrent 36 || SKIP_ENV_VALIDATION=true bun install --concurrent 36 + - cd ../../ + + # Generate Prisma client + - echo "Generating Prisma client..." + - cd packages/db && bun x prisma generate && cd ../../ + + # Validate environment variables + - echo "Validating environment variables..." + - '[ -n "$NEXT_PUBLIC_PORTAL_URL" ] || { echo "❌ NEXT_PUBLIC_PORTAL_URL is not set"; exit 1; }' + + # Type check + - echo "Type checking..." + - cd apps/$APP_NAME && bun run typecheck && cd ../../ + + # Build Next.js app + - echo "Building Next.js application..." + - cd apps/$APP_NAME + - NODE_TLS_REJECT_UNAUTHORIZED=0 bun run build + + # Prepare standalone build + - echo "Preparing standalone build..." + - echo "DEBUG - Checking what Next.js built..." + - ls -la .next/ + - ls -la .next/standalone/ || echo "No standalone directory" + - echo "DEBUG - Checking if static files exist..." + - ls -la .next/static/ || echo "No static directory found" + - echo "DEBUG - Copying static files to standalone..." + - cp -r public .next/standalone/apps/$APP_NAME/ || echo "No public folder" + - cp -r .next/static .next/standalone/apps/$APP_NAME/.next/ || echo "No .next/static directory" + - echo "DEBUG - Final verification..." + - ls -la .next/standalone/ || echo "Standalone empty" + - find .next/standalone -name "*.css" | head -3 || echo "No CSS files found" + - find .next/standalone -name "*.js" | head -3 || echo "No JS files found" + + # Copy Prisma client + - echo "Copying Prisma client..." + - mkdir -p .next/standalone/node_modules/.prisma .next/standalone/node_modules/@prisma + - if [ -d "../../node_modules/.prisma/client" ]; then + cp -r ../../node_modules/.prisma/client .next/standalone/node_modules/.prisma/; + elif [ -d "node_modules/.prisma/client" ]; then + cp -r node_modules/.prisma/client .next/standalone/node_modules/.prisma/; + fi + - if [ -d "../../node_modules/@prisma/client" ]; then + cp -r "../../node_modules/@prisma/client" ".next/standalone/node_modules/@prisma/"; + elif [ -d "node_modules/@prisma/client" ]; then + cp -r "node_modules/@prisma/client" ".next/standalone/node_modules/@prisma/"; + fi + + # Build Docker image + - echo "Building Docker image..." + - docker build --build-arg BUILDKIT_INLINE_CACHE=1 -f ${DOCKERFILE_PATH:-Dockerfile} -t $ECR_REPOSITORY_URI:$IMAGE_TAG . + - docker tag $ECR_REPOSITORY_URI:$IMAGE_TAG $ECR_REPOSITORY_URI:latest + + post_build: + commands: + - echo "Pushing images to ECR..." + - docker push $ECR_REPOSITORY_URI:$IMAGE_TAG + - docker push $ECR_REPOSITORY_URI:latest + - echo "Updating ECS service..." + - aws ecs update-service --cluster $ECS_CLUSTER_NAME --service $ECS_SERVICE_NAME --force-new-deployment + - 'printf "[{\"name\":\"%s-container\",\"imageUri\":\"%s\"}]" $APP_NAME $ECR_REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json' + +cache: + paths: + - 'node_modules/**/*' + - 'packages/db/node_modules/**/*' + - '/root/.bun/install/cache/**/*' + - '.next/cache/**/*' + - 'bun.lock' + +artifacts: + files: + - imagedefinitions.json + name: ${APP_NAME}-build diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts index 82eaa808b..0a6e9d691 100644 --- a/apps/app/next.config.ts +++ b/apps/app/next.config.ts @@ -27,7 +27,6 @@ const config: NextConfig = { serverActions: { bodySizeLimit: '15mb', }, - nodeMiddleware: true, authInterrupts: true, }, async rewrites() { @@ -49,4 +48,8 @@ const config: NextConfig = { skipTrailingSlashRedirect: true, }; +if (process.env.VERCEL !== '1') { + config.output = 'standalone'; // Required for Docker builds +} + export default config; diff --git a/apps/app/package.json b/apps/app/package.json index 5918f26f5..b7ee36952 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -13,7 +13,6 @@ "@browserbasehq/sdk": "^2.5.0", "@calcom/atoms": "^1.0.102-framer", "@calcom/embed-react": "^1.5.3", - "@comp/db": "workspace:*", "@date-fns/tz": "^1.2.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -33,6 +32,7 @@ "@react-three/drei": "^10.3.0", "@react-three/fiber": "^9.1.2", "@react-three/postprocessing": "^3.0.4", + "@t3-oss/env-nextjs": "^0.13.8", "@tanstack/react-query": "^5.74.4", "@tanstack/react-table": "^8.21.3", "@tiptap/extension-table": "^2.22.3", @@ -41,6 +41,7 @@ "@tiptap/extension-table-row": "^2.22.3", "@trigger.dev/react-hooks": "3.3.17", "@trigger.dev/sdk": "3.3.17", + "@trycompai/db": "workspace:*", "@types/canvas-confetti": "^1.9.0", "@types/three": "^0.177.0", "@uploadthing/react": "^7.3.0", @@ -55,7 +56,7 @@ "framer-motion": "^12.18.1", "geist": "^1.3.1", "motion": "^12.9.2", - "next": "15.4.0-canary.85", + "next": "15.4.2-canary.16", "next-safe-action": "^8.0.3", "next-themes": "^0.4.4", "nuqs": "^2.4.3", @@ -63,6 +64,8 @@ "posthog-js": "^1.236.6", "posthog-node": "^4.14.0", "puppeteer-core": "^24.7.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-email": "^4.0.15", "react-hook-form": "^7.58.1", "react-hotkeys-hook": "^5.1.0", @@ -96,12 +99,10 @@ "@vitejs/plugin-react": "^4.6.0", "@vitest/ui": "^3.2.4", "eslint": "^9", - "eslint-config-next": "15.4.0-canary.85", + "eslint-config-next": "15.4.2-canary.16", "fleetctl": "^4.68.1", "jsdom": "^26.1.0", "postcss": "^8.5.4", - "react": "^19.1.0", - "react-dom": "^19.1.0", "tailwindcss": "^4.1.8", "typescript": "^5.8.3", "vite-tsconfig-paths": "^5.1.4", diff --git a/apps/app/src/actions/comments/deleteComment.ts b/apps/app/src/actions/comments/deleteComment.ts index 0baac2256..30f794577 100644 --- a/apps/app/src/actions/comments/deleteComment.ts +++ b/apps/app/src/actions/comments/deleteComment.ts @@ -76,7 +76,7 @@ export const deleteComment = async (input: z.infer) => { const key = extractS3KeyFromUrl(att.url); await s3Client.send( new DeleteObjectCommand({ - Bucket: process.env.AWS_BUCKET_NAME!, + Bucket: process.env.APP_AWS_BUCKET_NAME!, Key: key, }), ); diff --git a/apps/app/src/actions/schema.ts b/apps/app/src/actions/schema.ts index 721aa29b0..bf7d07325 100644 --- a/apps/app/src/actions/schema.ts +++ b/apps/app/src/actions/schema.ts @@ -36,11 +36,6 @@ export const subdomainAvailabilitySchema = z.object({ }), }); -export const uploadSchema = z.object({ - file: z.instanceof(File), - organizationId: z.string(), -}); - export const deleteOrganizationSchema = z.object({ id: z.string(), organizationId: z.string(), diff --git a/apps/app/src/app/s3.ts b/apps/app/src/app/s3.ts index 52e5125ad..7b942ba5d 100644 --- a/apps/app/src/app/s3.ts +++ b/apps/app/src/app/s3.ts @@ -1,12 +1,12 @@ import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; -const AWS_REGION = process.env.AWS_REGION; -const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; -const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; +const APP_AWS_REGION = process.env.APP_AWS_REGION; +const APP_AWS_ACCESS_KEY_ID = process.env.APP_AWS_ACCESS_KEY_ID; +const APP_AWS_SECRET_ACCESS_KEY = process.env.APP_AWS_SECRET_ACCESS_KEY; export const BUCKET_NAME = process.env.AWS_BUCKET_NAME; -if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY || !BUCKET_NAME || !AWS_REGION) { +if (!APP_AWS_ACCESS_KEY_ID || !APP_AWS_SECRET_ACCESS_KEY || !BUCKET_NAME || !APP_AWS_REGION) { // Log the error in production environments if (process.env.NODE_ENV === 'production') { console.error('AWS S3 credentials or configuration missing in environment variables.'); @@ -21,10 +21,10 @@ if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY || !BUCKET_NAME || !AWS_REGION) // Create a single S3 client instance // Add null checks or assertions if the checks above don't guarantee non-null values export const s3Client = new S3Client({ - region: AWS_REGION!, + region: APP_AWS_REGION!, credentials: { - accessKeyId: AWS_ACCESS_KEY_ID!, - secretAccessKey: AWS_SECRET_ACCESS_KEY!, + accessKeyId: APP_AWS_ACCESS_KEY_ID!, + secretAccessKey: APP_AWS_SECRET_ACCESS_KEY!, }, }); diff --git a/apps/app/src/env.mjs b/apps/app/src/env.mjs index ac5e93e1b..248dba987 100644 --- a/apps/app/src/env.mjs +++ b/apps/app/src/env.mjs @@ -23,10 +23,10 @@ export const env = createEnv({ VERCEL_PROJECT_ID: z.string().optional(), TRUST_PORTAL_PROJECT_ID: z.string().optional(), NODE_ENV: z.string().optional(), - AWS_ACCESS_KEY_ID: z.string().optional(), - AWS_SECRET_ACCESS_KEY: z.string().optional(), - AWS_REGION: z.string().optional(), - AWS_BUCKET_NAME: z.string().optional(), + APP_AWS_ACCESS_KEY_ID: z.string().optional(), + APP_AWS_SECRET_ACCESS_KEY: z.string().optional(), + APP_AWS_REGION: z.string().optional(), + APP_AWS_BUCKET_NAME: z.string().optional(), GROQ_API_KEY: z.string().optional(), NEXT_PUBLIC_PORTAL_URL: z.string(), RESEND_AUDIENCE_ID: z.string().optional(), @@ -78,10 +78,10 @@ export const env = createEnv({ TRUST_PORTAL_PROJECT_ID: process.env.TRUST_PORTAL_PROJECT_ID, NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL, NODE_ENV: process.env.NODE_ENV, - AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, - AWS_REGION: process.env.AWS_REGION, - AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME, + APP_AWS_ACCESS_KEY_ID: process.env.APP_AWS_ACCESS_KEY_ID, + APP_AWS_SECRET_ACCESS_KEY: process.env.APP_AWS_SECRET_ACCESS_KEY, + APP_AWS_REGION: process.env.APP_AWS_REGION, + APP_AWS_BUCKET_NAME: process.env.APP_AWS_BUCKET_NAME, GROQ_API_KEY: process.env.GROQ_API_KEY, NEXT_PUBLIC_PORTAL_URL: process.env.NEXT_PUBLIC_PORTAL_URL, RESEND_AUDIENCE_ID: process.env.RESEND_AUDIENCE_ID, diff --git a/apps/app/src/lib/db/employee.ts b/apps/app/src/lib/db/employee.ts index 1c89fcd64..22235904d 100644 --- a/apps/app/src/lib/db/employee.ts +++ b/apps/app/src/lib/db/employee.ts @@ -2,8 +2,7 @@ import { env } from '@/env.mjs'; import { trainingVideos } from '@/lib/data/training-videos'; import { db } from '@comp/db'; import type { Departments, Member, Role } from '@comp/db/types'; -import { InvitePortalEmail } from '@comp/email/emails/invite-portal'; -import { sendEmail } from '@comp/email/lib/resend'; +import { InvitePortalEmail, sendEmail } from '@comp/email'; import { revalidatePath } from 'next/cache'; if (!env.NEXT_PUBLIC_PORTAL_URL) { diff --git a/apps/app/src/utils/auth.ts b/apps/app/src/utils/auth.ts index 351f29440..f335f212d 100644 --- a/apps/app/src/utils/auth.ts +++ b/apps/app/src/utils/auth.ts @@ -135,7 +135,7 @@ export const auth = betterAuth({ const url = `${protocol}://${domain}/auth`; await sendInviteMemberEmail({ - email: data.email, + inviteeEmail: data.email, inviteLink, organizationName: data.organization.name, }); diff --git a/apps/app/tailwind.config.ts b/apps/app/tailwind.config.ts index 69ed75904..d60f01c16 100644 --- a/apps/app/tailwind.config.ts +++ b/apps/app/tailwind.config.ts @@ -1,5 +1,5 @@ -import baseConfig from '@comp/ui/tailwind.config'; import type { Config } from 'tailwindcss'; +import baseConfig from '../../packages/ui/tailwind.config'; export default { content: [ diff --git a/apps/app/tsconfig.json b/apps/app/tsconfig.json index 77156da9b..7008132a5 100644 --- a/apps/app/tsconfig.json +++ b/apps/app/tsconfig.json @@ -19,7 +19,45 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@comp/db": ["../../packages/db/src/index.ts"], + "@comp/db/*": ["../../packages/db/src/*"], + "@comp/email": ["../../packages/email/index.ts"], + "@comp/email/*": ["../../packages/email/*"], + "@comp/kv": ["../../packages/kv/src/index.ts"], + "@comp/kv/*": ["../../packages/kv/src/*"], + "@comp/ui": ["../../packages/ui/src/components/index.ts"], + "@comp/ui/*": ["../../packages/ui/src/components/*"], + "@comp/ui/hooks": ["../../packages/ui/src/hooks/index.ts"], + "@comp/ui/hooks/*": ["../../packages/ui/src/hooks/*"], + "@comp/ui/utils/*": ["../../packages/ui/src/utils/*"], + "@comp/ui/cn": ["../../packages/ui/src/utils/cn.ts"], + "@comp/ui/truncate": ["../../packages/ui/src/utils/truncate.ts"], + "@comp/ui/globals.css": ["../../packages/ui/src/globals.css"], + "@comp/ui/editor.css": ["../../packages/ui/src/editor.css"], + "@comp/ui/tailwind.config": ["../../packages/ui/tailwind.config.ts"], + "@comp/utils": ["../../packages/utils/src/index.ts"], + "@comp/utils/*": ["../../packages/utils/src/*"], + "@comp/integrations": ["../../packages/integrations/src/index.ts"], + "@comp/integrations/*": ["../../packages/integrations/src/*"], + "@comp/analytics": ["../../packages/analytics/src/index.ts"], + "@comp/analytics/*": ["../../packages/analytics/src/*"], + "@comp/tsconfig": ["../../packages/tsconfig"], + "@comp/tsconfig/*": ["../../packages/tsconfig/*"], + "@trycompai/db": ["../../packages/db/src/index.ts"], + "@trycompai/db/*": ["../../packages/db/src/*"], + "@trycompai/email": ["../../packages/email/index.ts"], + "@trycompai/email/*": ["../../packages/email/*"], + "@trycompai/kv": ["../../packages/kv/src/index.ts"], + "@trycompai/kv/*": ["../../packages/kv/src/*"], + "@trycompai/ui": ["../../packages/ui/src/components/index.ts"], + "@trycompai/ui/*": ["../../packages/ui/src/components/*"], + "@trycompai/utils": ["../../packages/utils/src/index.ts"], + "@trycompai/utils/*": ["../../packages/utils/src/*"], + "@trycompai/analytics": ["../../packages/analytics/src/index.ts"], + "@trycompai/analytics/*": ["../../packages/analytics/src/*"], + "@trycompai/tsconfig": ["../../packages/tsconfig"], + "@trycompai/tsconfig/*": ["../../packages/tsconfig/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "trigger.config.ts"], diff --git a/apps/framework-editor/.env.example b/apps/framework-editor/.env.example deleted file mode 100644 index 305175e60..000000000 --- a/apps/framework-editor/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -DATABASE_URL="postgresql://postgres:postgres@localhost:5432/comp?schema=public" - -# BetterAuth -AUTH_SECRET="" - -# Authentication & Database -AUTH_GOOGLE_ID="" -AUTH_GOOGLE_SECRET="" diff --git a/apps/framework-editor/.eslintrc.json b/apps/framework-editor/.eslintrc.json deleted file mode 100644 index 6a495d57d..000000000 --- a/apps/framework-editor/.eslintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "next/core-web-vitals", - "rules": { - "react/no-unescaped-entities": "off", - "react-hooks/exhaustive-deps": "warn" - } -} diff --git a/apps/framework-editor/.gitignore b/apps/framework-editor/.gitignore deleted file mode 100644 index 3c35968eb..000000000 --- a/apps/framework-editor/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules - -# next.js -/.next/ -/out/ - -# production -/build - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files -.env* -!.env.example - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts \ No newline at end of file diff --git a/apps/framework-editor/app/(pages)/auth/Unauthorized.tsx b/apps/framework-editor/app/(pages)/auth/Unauthorized.tsx deleted file mode 100644 index 1fd927b45..000000000 --- a/apps/framework-editor/app/(pages)/auth/Unauthorized.tsx +++ /dev/null @@ -1,36 +0,0 @@ -'use client'; - -import { authClient } from '@/app/lib/auth-client'; -import { Button } from '@comp/ui/button'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; - -export const Unauthorized = () => { - const router = useRouter(); - const [loading, setLoading] = useState(false); - - const handleSignOut = async () => { - setLoading(true); - await authClient.signOut({ - fetchOptions: { - onSuccess: () => { - router.push('/auth'); - }, - }, - }); - }; - - return ( -
-
-

Oops, you don't belong here

-

- You are not authorized to access this page. Please sign in with a different account. -

- -
-
- ); -}; diff --git a/apps/framework-editor/app/(pages)/auth/button-icon.tsx b/apps/framework-editor/app/(pages)/auth/button-icon.tsx deleted file mode 100644 index 71c40a295..000000000 --- a/apps/framework-editor/app/(pages)/auth/button-icon.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { cn } from '@comp/ui/cn'; -import { motion } from 'framer-motion'; - -export const ButtonIcon = ({ - className, - children, - loading, - isLoading, -}: { - className?: string; - children: React.ReactNode; - loading?: React.ReactNode; - isLoading: boolean; -}) => { - return ( -
- - {children} - - - {loading} - -
- ); -}; diff --git a/apps/framework-editor/app/(pages)/auth/google-sign-in.tsx b/apps/framework-editor/app/(pages)/auth/google-sign-in.tsx deleted file mode 100644 index 01f624438..000000000 --- a/apps/framework-editor/app/(pages)/auth/google-sign-in.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import { authClient } from '@/app/lib/auth-client'; -import { Button } from '@comp/ui/button'; -import { Icons } from '@comp/ui/icons'; -import { Loader2 } from 'lucide-react'; -import { useState } from 'react'; -import { ButtonIcon } from './button-icon'; - -export function GoogleSignIn({ inviteCode }: { inviteCode?: string }) { - const [isLoading, setLoading] = useState(false); - - const handleSignIn = async () => { - setLoading(true); - let redirectTo = '/'; - - if (inviteCode) { - redirectTo = `/invite/${inviteCode}`; - } - - await authClient.signIn.social({ - provider: 'google', - callbackURL: redirectTo, - }); - }; - - return ( - - ); -} diff --git a/apps/framework-editor/app/(pages)/auth/page.tsx b/apps/framework-editor/app/(pages)/auth/page.tsx deleted file mode 100644 index b4997581e..000000000 --- a/apps/framework-editor/app/(pages)/auth/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { auth } from '@/app/lib/auth'; -import type { Metadata } from 'next'; -import { headers } from 'next/headers'; -import { redirect } from 'next/navigation'; -import Balancer from 'react-wrap-balancer'; -import { GoogleSignIn } from './google-sign-in'; -import { Unauthorized } from './Unauthorized'; - -export const metadata: Metadata = { - title: 'Login | Comp AI', -}; - -export default async function Page() { - const session = await auth.api.getSession({ - headers: await headers(), - }); - - const hasSession = session?.user; - const isAllowed = session?.user.email.split('@')[1] === 'trycomp.ai'; - - if (hasSession && !isAllowed) { - return ; - } - - if (hasSession && isAllowed) { - redirect('/frameworks'); - } - - let preferredSignInOption: React.ReactNode; - - if (process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET) { - preferredSignInOption = ( -
- -
- ); - } - - return ( -
-
-
- -

Get Started with Comp AI

-

Sign in to continue

-
- -
{preferredSignInOption}
- -

- By clicking continue, you acknowledge that you have read and agree to the{' '} - - Terms and Conditions - {' '} - and{' '} - - Privacy Policy - - . -

-
-
-
- ); -} diff --git a/apps/framework-editor/app/(pages)/controls/ControlsClientPage.tsx b/apps/framework-editor/app/(pages)/controls/ControlsClientPage.tsx deleted file mode 100644 index 7d31329bb..000000000 --- a/apps/framework-editor/app/(pages)/controls/ControlsClientPage.tsx +++ /dev/null @@ -1,376 +0,0 @@ -'use client'; - -import PageLayout from '@/app/components/PageLayout'; -import { useEffect, useMemo, useState } from 'react'; -import { toast } from 'sonner'; -import { relationalColumn } from '../../components/grid/RelationalCell'; -import { friendlyDateColumnBase } from '../../components/gridUtils'; -import { TableToolbar } from '../../components/TableToolbar'; -import { useTableSearchSort } from '../../hooks/useTableSearchSort'; -import type { SortConfig } from '../../types/common'; -import { - getAllPolicyTemplates, - getAllRequirements, - getAllTaskTemplates, - linkPolicyTemplateToControl, - linkRequirementToControl, - linkTaskTemplateToControl, - unlinkPolicyTemplateFromControl, - unlinkRequirementFromControl, - unlinkTaskTemplateFromControl, -} from './actions'; -import { simpleUUID, useChangeTracking } from './hooks/useChangeTracking'; -import type { - ControlsPageGridData, - ControlsPageSortableColumnKey, - FrameworkEditorControlTemplateWithRelatedData, - SortableColumnOption, -} from './types'; - -import { type Column, DataSheetGrid, keyColumn, textColumn } from 'react-datasheet-grid'; -import 'react-datasheet-grid/dist/style.css'; - -import { Button } from '@comp/ui/button'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; - -interface ControlsClientPageProps { - initialControls: FrameworkEditorControlTemplateWithRelatedData[]; -} - -const sortableColumnsOptions: SortableColumnOption[] = [ - { value: 'name', label: 'Name' }, - { value: 'description', label: 'Description' }, - { value: 'createdAt', label: 'Created At' }, - { value: 'updatedAt', label: 'Updated At' }, -]; - -const controlsSearchableKeys: ControlsPageSortableColumnKey[] = ['name', 'description']; -const controlsSortConfig: SortConfig = { - name: 'string', - description: 'string', - policyTemplatesLength: 'number', - requirementsLength: 'number', - taskTemplatesLength: 'number', - createdAt: 'number', - updatedAt: 'number', -}; - -export function ControlsClientPage({ initialControls }: ControlsClientPageProps) { - const ALL_FRAMEWORKS_OPTION_VALUE = '__ALL_FRAMEWORKS__'; - - const initialGridData: ControlsPageGridData[] = useMemo(() => { - return initialControls.map((control) => { - let cDate: Date | null = null; - if (control.createdAt) { - // Handles if control.createdAt is Date obj or valid date string - const parsedCDate = new Date(control.createdAt); - if (!isNaN(parsedCDate.getTime())) { - // Check if it's a valid date - cDate = parsedCDate; - } - } - - let uDate: Date | null = null; - if (control.updatedAt) { - // Handles if control.updatedAt is Date obj or valid date string - const parsedUDate = new Date(control.updatedAt); - if (!isNaN(parsedUDate.getTime())) { - // Check if it's a valid date - uDate = parsedUDate; - } - } - - return { - id: control.id || simpleUUID(), - name: control.name ?? null, - description: control.description ?? null, - policyTemplates: - control.policyTemplates?.map((pt) => ({ - id: pt.id, - name: pt.name, - })) ?? [], - requirements: - control.requirements?.map((r) => ({ - id: r.id, - name: r.name, - frameworkName: r.framework?.name, - })) ?? [], - taskTemplates: control.taskTemplates?.map((tt) => ({ id: tt.id, name: tt.name })) ?? [], - policyTemplatesLength: control.policyTemplates?.length ?? 0, - requirementsLength: control.requirements?.length ?? 0, - taskTemplatesLength: control.taskTemplates?.length ?? 0, - createdAt: cDate, - updatedAt: uDate, - }; - }); - }, [initialControls]); - - const { - dataForGrid, - handleGridChange, - getRowClassName, - handleCommit, - handleCancel, - isDirty, - createdRowIds, - changesSummaryString, - setDisplayedData, - } = useChangeTracking(initialGridData); - - const [selectedFramework, setSelectedFramework] = useState(''); - - const frameworkOptions = useMemo(() => { - const frameworkNames = new Set(); - initialControls.forEach((control) => { - control.requirements?.forEach((req) => { - if (req.framework?.name) { - frameworkNames.add(req.framework.name); - } - }); - }); - return Array.from(frameworkNames).map((name) => ({ - label: name, - value: name, - })); - }, [initialControls]); - - const filteredDataByFramework = useMemo(() => { - if (selectedFramework === '' || selectedFramework === ALL_FRAMEWORKS_OPTION_VALUE) { - return dataForGrid; - } - return dataForGrid.filter((control) => - control.requirements?.some((req) => req.frameworkName === selectedFramework), - ); - }, [dataForGrid, selectedFramework]); - - const { - searchTerm, - setSearchTerm, - sortColumnKey, - setSortColumnKey, - sortDirection, - toggleSortDirection, - processedData: sortedDataWithPotentialTimestamps, - } = useTableSearchSort( - filteredDataByFramework, - controlsSearchableKeys, - controlsSortConfig, - 'createdAt', - 'asc', - ); - - // Convert timestamps back to Date objects if useTableSearchSort changed them - const dataForDisplay = useMemo(() => { - return sortedDataWithPotentialTimestamps.map((row) => { - const newRow = { ...row }; - // If createdAt/updatedAt became a number (timestamp) after sorting, convert back to Date - if (typeof newRow.createdAt === 'number') { - newRow.createdAt = new Date(newRow.createdAt); - } - if (typeof newRow.updatedAt === 'number') { - newRow.updatedAt = new Date(newRow.updatedAt); - } - return newRow; - }); - }, [sortedDataWithPotentialTimestamps]); - - useEffect(() => { - setDisplayedData(dataForDisplay); - }, [dataForDisplay, setDisplayedData]); - - const columns: Column[] = [ - { ...keyColumn('name', textColumn), title: 'Name', minWidth: 300 }, - { - ...keyColumn('description', textColumn), - title: 'Description', - minWidth: 420, - grow: 1, - }, - { - ...relationalColumn({ - itemsKey: 'policyTemplates', - getAllSearchableItems: getAllPolicyTemplates, - linkItemAction: async (controlId, policyTemplateId) => { - try { - await linkPolicyTemplateToControl(controlId, policyTemplateId); - toast.success('Policy template linked successfully.'); - } catch (error) { - toast.error( - `Failed to link policy template: ${error instanceof Error ? error.message : String(error)}`, - ); - // Do not re-throw, error is handled with a toast - } - }, - unlinkItemAction: async (controlId, policyTemplateId) => { - try { - await unlinkPolicyTemplateFromControl(controlId, policyTemplateId); - toast.success('Policy template unlinked successfully.'); - } catch (error) { - toast.error( - `Failed to unlink policy template: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - itemTypeLabel: 'Policy', - createdRowIds: createdRowIds, - }), - id: 'policyTemplates', - title: 'Linked Policies', - minWidth: 200, - }, - { - id: 'requirements', - title: 'Linked Requirements', - minWidth: 200, - ...relationalColumn({ - itemsKey: 'requirements', - getAllSearchableItems: getAllRequirements, - linkItemAction: async (controlId, requirementId) => { - try { - await linkRequirementToControl(controlId, requirementId); - toast.success('Requirement linked successfully.'); - } catch (error) { - toast.error( - `Failed to link requirement: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - unlinkItemAction: async (controlId, requirementId) => { - try { - await unlinkRequirementFromControl(controlId, requirementId); - toast.success('Requirement unlinked successfully.'); - } catch (error) { - toast.error( - `Failed to unlink requirement: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - itemTypeLabel: 'Requirement', - createdRowIds: createdRowIds, - }), - }, - { - id: 'taskTemplates', - title: 'Linked Tasks', - minWidth: 200, - ...relationalColumn({ - itemsKey: 'taskTemplates', - getAllSearchableItems: getAllTaskTemplates, - linkItemAction: async (controlId, taskTemplateId) => { - try { - await linkTaskTemplateToControl(controlId, taskTemplateId); - toast.success('Task template linked successfully.'); - } catch (error) { - toast.error( - `Failed to link task template: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - unlinkItemAction: async (controlId, taskTemplateId) => { - try { - await unlinkTaskTemplateFromControl(controlId, taskTemplateId); - toast.success('Task template unlinked successfully.'); - } catch (error) { - toast.error( - `Failed to unlink task template: ${error instanceof Error ? error.message : String(error)}`, - ); - } - }, - itemTypeLabel: 'Task Template', - createdRowIds: createdRowIds, - }), - }, - { - ...keyColumn('createdAt', friendlyDateColumnBase), - title: 'Created At', - minWidth: 220, // Adjusted minWidth - disabled: true, - }, - { - ...keyColumn('updatedAt', friendlyDateColumnBase), - title: 'Updated At', - minWidth: 220, // Adjusted minWidth - disabled: true, - }, - { - ...keyColumn('id', textColumn as Partial>), - title: 'ID', - minWidth: 300, - disabled: true, - }, - ]; - - return ( - <> - - {isDirty && ( -
- {changesSummaryString} - - -
- )} - - setSortColumnKey(key as ControlsPageSortableColumnKey | null) - } - sortDirection={sortDirection} - onSortDirectionChange={toggleSortDirection} - > - <> - - - -
- ({ - id: simpleUUID(), - name: 'Control Name', - description: 'Control Description', - policyTemplates: [], - requirements: [], - taskTemplates: [], - policyTemplatesLength: 0, - requirementsLength: 0, - taskTemplatesLength: 0, - createdAt: new Date(), - updatedAt: new Date(), - })} - duplicateRow={({ rowData }) => ({ - ...rowData, - id: simpleUUID(), - createdAt: new Date(), - updatedAt: new Date(), - })} - /> -
-
- - ); -} diff --git a/apps/framework-editor/app/(pages)/controls/actions.ts b/apps/framework-editor/app/(pages)/controls/actions.ts deleted file mode 100644 index 91ea96566..000000000 --- a/apps/framework-editor/app/(pages)/controls/actions.ts +++ /dev/null @@ -1,368 +0,0 @@ -'use server'; - -import type { SearchableItemForLinking } from '@/app/components/SearchAndLinkList'; // Assuming this path is correct -import { db } from '@comp/db'; -import type { FrameworkEditorControlTemplate } from '@prisma/client'; -import { revalidatePath } from 'next/cache'; -import { z } from 'zod'; -import { createControlTemplateSchema } from './schemas'; - -/** - * Fetches all requirements, including their framework name and identifier. - */ -export async function getAllRequirements(): Promise { - try { - const requirements = await db.frameworkEditorRequirement.findMany({ - select: { - id: true, - identifier: true, - name: true, // Descriptive name - framework: { - select: { - name: true, - }, - }, - }, - orderBy: { - // Consider if ordering by identifier or name is preferred. Currently descriptive name. - name: 'asc', - }, - }); - return requirements.map((r) => { - let primaryDisplayLabel = r.identifier; // Start with identifier - if (r.identifier && r.name) { - // If both identifier and descriptive name exist - primaryDisplayLabel = `${r.identifier} - ${r.name}`; - } else if (r.name) { - // Else if only descriptive name exists - primaryDisplayLabel = r.name; - } - // If only identifier exists, it's already set. If neither, fallback. - primaryDisplayLabel = primaryDisplayLabel || 'Unnamed Requirement'; - - return { - id: r.id, - name: primaryDisplayLabel, - sublabel: r.framework?.name ? r.framework.name : undefined, - }; - }); - } catch (error) { - console.error('Error fetching all requirements:', error); - throw new Error('Failed to fetch all requirements.'); - } -} - -/** - * Links a requirement to a control template. - */ -export async function linkRequirementToControl( - controlId: string, - requirementId: string, -): Promise { - if (!controlId || !requirementId) { - throw new Error('Control ID and Requirement ID must be provided.'); - } - try { - await db.frameworkEditorControlTemplate.update({ - where: { id: controlId }, - data: { - requirements: { - connect: { id: requirementId }, - }, - }, - }); - revalidatePath(`/controls/${controlId}`); - revalidatePath('/controls'); // Also revalidate the list page if counts are shown there - } catch (error) { - console.error('Error linking requirement to control:', error); - throw new Error('Failed to link requirement.'); - } -} - -/** - * Unlinks a requirement from a control template. - */ -export async function unlinkRequirementFromControl( - controlId: string, - requirementId: string, -): Promise { - if (!controlId || !requirementId) { - throw new Error('Control ID and Requirement ID must be provided.'); - } - try { - await db.frameworkEditorControlTemplate.update({ - where: { id: controlId }, - data: { - requirements: { - disconnect: { id: requirementId }, - }, - }, - }); - revalidatePath(`/controls/${controlId}`); - revalidatePath('/controls'); - } catch (error) { - console.error('Error unlinking requirement from control:', error); - throw new Error('Failed to unlink requirement.'); - } -} - -// --- Policy Template Actions --- - -/** - * Fetches all policy templates. - */ -export async function getAllPolicyTemplates(): Promise { - try { - const policyTemplates = await db.frameworkEditorPolicyTemplate.findMany({ - select: { - id: true, - name: true, - }, - orderBy: { - name: 'asc', - }, - }); - // Policy templates only have a name, sublabel will be undefined - return policyTemplates.map((pt: { id: string; name: string }) => ({ - id: pt.id, - name: pt.name, - // sublabel will be implicitly undefined - })); - } catch (error) { - console.error('Error fetching all policy templates:', error); - throw new Error('Failed to fetch all policy templates.'); - } -} - -/** - * Links a policy template to a control template. - */ -export async function linkPolicyTemplateToControl( - controlId: string, - policyTemplateId: string, -): Promise { - if (!controlId || !policyTemplateId) { - throw new Error('Control ID and Policy Template ID must be provided.'); - } - try { - await db.frameworkEditorControlTemplate.update({ - where: { id: controlId }, - data: { - policyTemplates: { - // Assuming a 'policyTemplates' relation exists on the control model - connect: { id: policyTemplateId }, - }, - }, - }); - revalidatePath(`/controls/${controlId}`); - revalidatePath('/controls'); - } catch (error) { - console.error('Error linking policy template to control:', error); - throw new Error('Failed to link policy template.'); - } -} - -/** - * Unlinks a policy template from a control template. - */ -export async function unlinkPolicyTemplateFromControl( - controlId: string, - policyTemplateId: string, -): Promise { - if (!controlId || !policyTemplateId) { - throw new Error('Control ID and Policy Template ID must be provided.'); - } - try { - await db.frameworkEditorControlTemplate.update({ - where: { id: controlId }, - data: { - policyTemplates: { - disconnect: { id: policyTemplateId }, - }, - }, - }); - revalidatePath(`/controls/${controlId}`); - revalidatePath('/controls'); - } catch (error) { - console.error('Error unlinking policy template from control:', error); - throw new Error('Failed to unlink policy template.'); - } -} - -// --- Task Template Actions --- - -/** - * Fetches all task templates. - */ -export async function getAllTaskTemplates(): Promise { - try { - const taskTemplates = await db.frameworkEditorTaskTemplate.findMany({ - select: { - id: true, - name: true, - }, - orderBy: { - name: 'asc', - }, - }); - // Task templates only have a name, sublabel will be undefined - return taskTemplates.map((tt: { id: string; name: string }) => ({ - id: tt.id, - name: tt.name, - // sublabel will be implicitly undefined - })); - } catch (error) { - console.error('Error fetching all task templates:', error); - throw new Error('Failed to fetch all task templates.'); - } -} - -/** - * Links a task template to a control template. - */ -export async function linkTaskTemplateToControl( - controlId: string, - taskTemplateId: string, -): Promise { - if (!controlId || !taskTemplateId) { - throw new Error('Control ID and Task Template ID must be provided.'); - } - try { - await db.frameworkEditorControlTemplate.update({ - where: { id: controlId }, - data: { - taskTemplates: { - // Assuming a 'taskTemplates' relation exists on the control model - connect: { id: taskTemplateId }, - }, - }, - }); - revalidatePath(`/controls/${controlId}`); - revalidatePath('/controls'); - } catch (error) { - console.error('Error linking task template to control:', error); - throw new Error('Failed to link task template.'); - } -} - -/** - * Unlinks a task template from a control template. - */ -export async function unlinkTaskTemplateFromControl( - controlId: string, - taskTemplateId: string, -): Promise { - if (!controlId || !taskTemplateId) { - throw new Error('Control ID and Task Template ID must be provided.'); - } - try { - await db.frameworkEditorControlTemplate.update({ - where: { id: controlId }, - data: { - taskTemplates: { - disconnect: { id: taskTemplateId }, - }, - }, - }); - revalidatePath(`/controls/${controlId}`); - revalidatePath('/controls'); - } catch (error) { - console.error('Error unlinking task template from control:', error); - throw new Error('Failed to unlink task template.'); - } -} - -// --- Control Actions --- - -interface UpdateControlPayload { - name: string; - description: string; -} - -/** - * Updates the details of a control template. - */ -export async function updateControlDetails( - controlId: string, - data: UpdateControlPayload, -): Promise { - if (!controlId) { - throw new Error('Control ID must be provided.'); - } - if (!data.name || data.name.trim() === '') { - throw new Error('Control name must be provided.'); - } - - try { - await db.frameworkEditorControlTemplate.update({ - where: { id: controlId }, - data: { - name: data.name, - description: data.description, - }, - }); - revalidatePath(`/controls/${controlId}`); - revalidatePath('/controls'); - } catch (error) { - console.error('Error updating control details:', error); - throw new Error('Failed to update control details.'); - } -} - -/** - * Deletes a control template. - */ -export async function deleteControl(controlId: string): Promise { - if (!controlId) { - throw new Error('Control ID must be provided.'); - } - try { - // Note: Depending on DB constraints, you might need to disconnect relations first - // if there's no onDelete: Cascade or similar set up for related items. - await db.frameworkEditorControlTemplate.delete({ - where: { id: controlId }, - }); - revalidatePath(`/controls/${controlId}`); // Or just /controls if navigating away - revalidatePath('/controls'); - } catch (error) { - console.error('Error deleting control:', error); - // Consider more specific error handling, e.g., if control not found or if relations prevent deletion - throw new Error('Failed to delete control.'); - } -} - -/** - * Creates a new control template. - */ -export async function createControl(rawData: { - name: string | null; - description?: string | null; -}): Promise { - const validationResult = createControlTemplateSchema.safeParse(rawData); - - if (!validationResult.success) { - const errorMessages = validationResult.error.issues - .map((issue: z.ZodIssue) => `${issue.path.join('.')}: ${issue.message}`) - .join(', '); - throw new Error(`Invalid input for creating control: ${errorMessages}`); - } - - const { name, description } = validationResult.data; // name is string, description is string | undefined - - try { - const controlTemplate = await db.frameworkEditorControlTemplate.create({ - data: { - name, - description: description || '', // Ensure description is not undefined for Prisma - }, - }); - - revalidatePath('/controls'); - return controlTemplate; - } catch (error) { - console.error('Failed to create control template:', error); - const errorMessage = - error instanceof Error ? error.message : 'Failed to create control template in the database.'; - throw new Error(`Database error: ${errorMessage}`); - } -} diff --git a/apps/framework-editor/app/(pages)/controls/hooks/useChangeTracking.ts b/apps/framework-editor/app/(pages)/controls/hooks/useChangeTracking.ts deleted file mode 100644 index 6b301b2b6..000000000 --- a/apps/framework-editor/app/(pages)/controls/hooks/useChangeTracking.ts +++ /dev/null @@ -1,448 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -// Import types from the new types.ts file -import { useToast } from '@comp/ui/use-toast'; -import type { FrameworkEditorControlTemplate } from '@prisma/client'; -import { createControl, deleteControl, updateControlDetails } from '../actions'; -import type { ControlsPageGridData, DSGOperation } from '../types'; - -// Define result types for creation operations to help with type inference -type CreationSuccessResult = { - success: true; - tempId: string; - newId: string; - newControl: FrameworkEditorControlTemplate; -}; -type CreationFailureResult = { success: false; tempId: string; error: any }; -type CreationOperationResult = CreationSuccessResult | CreationFailureResult; - -// GridData type definition, co-located as it's tightly coupled with the hook's data -export type GridData = { - id: string; - name: string | null; - description: string | null; - policyTemplatesCount: string | null; - requirementsCount: string | null; - taskTemplatesCount: string | null; -}; - -// simpleUUID can remain here or be moved to a general utils file later if used elsewhere -export const simpleUUID = () => crypto.randomUUID(); - -export const useChangeTracking = (initialData: ControlsPageGridData[]) => { - const [data, setData] = useState(initialData); - const [prevData, setPrevData] = useState(initialData); - const { toast } = useToast(); - - const isMounted = useRef(true); - const displayedDataRef = useRef(initialData); - - const createdRowIds = useMemo(() => new Set(), []); - const updatedRowIds = useMemo(() => new Set(), []); - const deletedRowIds = useMemo(() => new Set(), []); - - useEffect(() => { - setData(initialData); - setPrevData(initialData); - createdRowIds.clear(); - updatedRowIds.clear(); - deletedRowIds.clear(); - }, [initialData]); - - useEffect(() => { - isMounted.current = true; - return () => { - isMounted.current = false; - }; - }, []); - - const setDisplayedData = useCallback((displayedData: ControlsPageGridData[]) => { - displayedDataRef.current = displayedData; - }, []); - - const handleGridChange = useCallback( - (newValue: ControlsPageGridData[], operations: DSGOperation[]) => { - setData((currentDataState) => { - if (!isMounted.current) { - // If the component has unmounted by the time this debounced/async update fires, do nothing. - return currentDataState; // Return the existing state to prevent updates on unmounted component - } - - let workingNewValue = [...newValue]; - const originalDataForDelete = displayedDataRef.current; - - operations.forEach((op) => { - if (op.type === 'CREATE') { - workingNewValue.slice(op.fromRowIndex, op.toRowIndex).forEach((row) => { - if (row.id) createdRowIds.add(row.id); - }); - } else if (op.type === 'UPDATE') { - workingNewValue.slice(op.fromRowIndex, op.toRowIndex).forEach((row) => { - if (row.id && !createdRowIds.has(row.id) && !deletedRowIds.has(row.id)) { - updatedRowIds.add(row.id); - } - }); - } else if (op.type === 'DELETE') { - let keptRowsForDisplay = 0; - originalDataForDelete.slice(op.fromRowIndex, op.toRowIndex).forEach((deletedRow, i) => { - if (!deletedRow.id) return; - updatedRowIds.delete(deletedRow.id); - if (createdRowIds.has(deletedRow.id)) { - createdRowIds.delete(deletedRow.id); - } else { - deletedRowIds.add(deletedRow.id); - workingNewValue.splice( - op.fromRowIndex + keptRowsForDisplay++, - 0, - originalDataForDelete[op.fromRowIndex + i], - ); - } - }); - } - }); - return workingNewValue; - }); - }, - [createdRowIds, updatedRowIds, deletedRowIds], - ); - - const getRowClassName = useCallback( - ({ rowData }: { rowData: ControlsPageGridData }) => { - if (!rowData || !rowData.id) return ''; - if (deletedRowIds.has(rowData.id)) return 'row-deleted'; - if (createdRowIds.has(rowData.id)) return 'row-created'; - if (updatedRowIds.has(rowData.id)) return 'row-updated'; - return ''; - }, - [createdRowIds, updatedRowIds, deletedRowIds], - ); - - const handleCommit = useCallback(async () => { - let workingData = [...data]; - - // --- Handle Creations --- - const creationOps = Array.from(createdRowIds).map((tempId) => { - const row = workingData.find((r) => r.id === tempId); - if (row && row.name) { - return createControl({ name: row.name, description: row.description }) - .then( - (newControl: FrameworkEditorControlTemplate) => - ({ - success: true, - tempId, - newId: newControl.id, - newControl, - }) as CreationSuccessResult, - ) - .catch((error) => { - console.error(`Failed to create control (tempId: ${tempId}):`, error); - return { success: false, tempId, error } as CreationFailureResult; - }); - } - console.warn(`Skipping creation for row with tempId ${tempId} due to missing data or name.`); - return Promise.resolve({ - success: false, - tempId, - error: new Error('Missing data or name for creation'), - } as CreationFailureResult); - }); - const creationResults = await Promise.allSettled(creationOps); - - // --- Handle Updates (excluding newly created or already deleted items) --- - const idsToUpdateActually = Array.from(updatedRowIds).filter( - (id) => !createdRowIds.has(id) && !deletedRowIds.has(id), - ); - const updateOps = idsToUpdateActually.map((id) => { - const row = workingData.find((r) => r.id === id); - if (row && row.name) { - return updateControlDetails(id, { - name: row.name, - description: row.description || '', - }) - .then(() => ({ success: true, id })) - .catch((error) => { - console.error(`Failed to update control (id: ${id}):`, error); - return { success: false, id, error }; - }); - } - console.warn(`Skipping update for row with id ${id} due to missing data or name.`); - return Promise.resolve({ - success: false, - id, - error: new Error('Missing data or name for update'), - }); - }); - const updateResults = await Promise.allSettled(updateOps); - - // --- Handle Deletions --- - const deletionOps = Array.from(deletedRowIds).map((id) => { - if ( - createdRowIds.has(id) && - !creationResults.some( - (res) => - res.status === 'fulfilled' && - (res.value as CreationOperationResult).success && - (res.value as CreationSuccessResult).tempId === id && - (res.value as CreationSuccessResult).newId, - ) - ) { - // If it was a created item that didn't successfully get a server ID (e.g. creation failed or was skipped, or it's still pending somehow) - console.log(`Client-side deletion of temporary item: ${id}`); - return Promise.resolve({ success: true, id, clientSideDelete: true }); - } - return deleteControl(id) - .then(() => ({ success: true, id })) - .catch((error) => { - console.error(`Failed to delete control (id: ${id}):`, error); - return { success: false, id, error }; - }); - }); - const deletionResults = await Promise.allSettled(deletionOps); - - // --- Consolidate Data and Update State --- - // 1. Update IDs for successful creations and collect created data - const serverCreatedRows = new Map(); - creationResults.forEach((res) => { - if (res.status === 'fulfilled') { - const creationValue = res.value as CreationOperationResult; - if (creationValue.success) { - // Now TypeScript knows creationValue is CreationSuccessResult - const { tempId, newId } = creationValue; - const originalRow = workingData.find((r) => r.id === tempId); - if (originalRow) { - serverCreatedRows.set(newId, { - ...originalRow, - id: newId, - // For newly created items, counts are 0 as relations are not established yet. - policyTemplates: [], - requirements: [], - taskTemplates: [], - }); - } - createdRowIds.delete(tempId); - } - } - }); - - // 2. Remove successfully updated items from tracking set - updateResults.forEach((res) => { - if (res.status === 'fulfilled' && res.value.success && res.value.id) { - updatedRowIds.delete(res.value.id); - } - }); - - // 3. Prepare final list of data, handling deletions and incorporating server-created items - const actuallyDeletedIds = new Set(); - deletionResults.forEach((res) => { - if (res.status === 'fulfilled' && res.value.success && res.value.id) { - actuallyDeletedIds.add(res.value.id); - deletedRowIds.delete(res.value.id); - createdRowIds.delete(res.value.id); // If it was created then deleted - updatedRowIds.delete(res.value.id); // If it was updated then deleted - } - }); - - // Rebuild workingData: start with existing, filter deleted, then replace temp IDs with server IDs for created items - let finalProcessedData: ControlsPageGridData[] = []; - workingData.forEach((row) => { - if (actuallyDeletedIds.has(row.id)) return; // Skip if deleted - - // Check if this row corresponds to a successfully created item (identified by its original tempId) - let wasSuccessfullyCreated = false; - for (const res of creationResults) { - if (res.status === 'fulfilled') { - const creationValue = res.value as CreationOperationResult; - if (creationValue.success && creationValue.tempId === row.id && creationValue.newId) { - // This row was a temp row that got created. Its data is on serverCreatedRows map. - if (serverCreatedRows.has(creationValue.newId)) { - finalProcessedData.push(serverCreatedRows.get(creationValue.newId)!); - } - wasSuccessfullyCreated = true; - break; - } - } - } - if (!wasSuccessfullyCreated) { - finalProcessedData.push(row); - } - }); - - if (isMounted.current) { - setData(finalProcessedData); - setPrevData(finalProcessedData); - } - - console.log('Commit completed. Remaining dirty items:', { - created: Array.from(createdRowIds), - updated: Array.from(updatedRowIds), - deleted: Array.from(deletedRowIds), - }); - - if (isMounted.current) { - // Re-filter based on finalProcessedData if necessary for newData - const currentDataAfterFirstUpdate = finalProcessedData; // Assuming finalProcessedData is what we want to filter - const newData = currentDataAfterFirstUpdate.filter( - (row) => !row.id || !actuallyDeletedIds.has(row.id), - ); // Ensure we use actuallyDeletedIds - - setData(newData); - setPrevData(newData); - } - - // --- Display Toast Summary --- - const successes: string[] = []; - const errors: string[] = []; - - creationResults.forEach((res) => { - if (res.status === 'fulfilled') { - const result = res.value as CreationOperationResult; - if (result.success) { - const createdRow = serverCreatedRows.get(result.newId); - successes.push(`Created: ${createdRow?.name || result.newId}`); - } else { - errors.push( - `Failed to create (tempId: ${result.tempId}): ${result.error?.message || 'Unknown error'}`, - ); - } - } else { - // res.reason should be an Error object or contain a message - const reason = res.reason as any; - errors.push( - `Creation operation failed: ${reason?.message || String(reason) || 'Unknown reason'}`, - ); - } - }); - - updateResults.forEach((res) => { - if (res.status === 'fulfilled') { - const result = res.value as { - success: boolean; - id: string; - error?: any; - }; - if (result.success && result.id) { - // Find the name from finalProcessedData as workingData might be stale for updates if ID changed (not in this case but good practice) - const updatedRow = finalProcessedData.find((r) => r.id === result.id); - successes.push(`Updated: ${updatedRow?.name || result.id}`); - } else if (result.id) { - errors.push( - `Failed to update (${result.id}): ${result.error?.message || 'Unknown error'}`, - ); - } - // If result.id is undefined but it was a fulfilled promise, it implies an issue with the op function not returning id - else if (!result.id && !result.success) { - errors.push( - `Failed to update item: ${result.error?.message || 'Unknown error, ID missing'}`, - ); - } - } else { - const reason = res.reason as any; - errors.push( - `Update operation failed: ${reason?.message || String(reason) || 'Unknown reason'}`, - ); - } - }); - - deletionResults.forEach((res) => { - if (res.status === 'fulfilled') { - const result = res.value as { - success: boolean; - id: string; - error?: any; - clientSideDelete?: boolean; - }; - if (result.success && result.id) { - if (result.clientSideDelete) { - successes.push(`Client-side removal of temp item: ${result.id}`); - } else { - // Try to find the name of the deleted item from the prevData or original data if available and needed for toast - // For simplicity, just using ID here. - successes.push(`Deleted ID: ${result.id}`); - } - } else if (result.id) { - errors.push( - `Failed to delete (${result.id}): ${result.error?.message || 'Unknown error'}`, - ); - } - // If result.id is undefined but it was a fulfilled promise, it implies an issue with the op function not returning id - else if (!result.id && !result.success) { - errors.push( - `Failed to delete item: ${result.error?.message || 'Unknown error, ID missing'}`, - ); - } - } else { - const reason = res.reason as any; - errors.push( - `Deletion operation failed: ${reason?.message || String(reason) || 'Unknown reason'}`, - ); - } - }); - - const toastTitle = - errors.length > 0 - ? successes.length > 0 - ? 'Commit Partially Successful' - : 'Commit Failed' - : 'Commit Successful'; - let toastDescription = ''; - - if (successes.length > 0) { - toastDescription += `Successes (${successes.length}):\n${successes.map((s) => ` - ${s}`).join('\n')}`; - } - if (errors.length > 0) { - if (toastDescription) toastDescription += '\n\n'; // Add separator if there were successes - toastDescription += `Errors (${errors.length}):\n${errors.map((e) => ` - ${e}`).join('\n')}`; - } - if (successes.length === 0 && errors.length === 0) { - // This case might occur if handleCommit was called with no pending changes, though isDirty should prevent this. - // Or if all operations were somehow filtered out before being processed. - toastDescription = 'No operations were performed.'; - } - - if (isMounted.current && (successes.length > 0 || errors.length > 0)) { - // Only show toast if there's something to report - toast({ - title: toastTitle, - description: toastDescription, - variant: errors.length > 0 ? 'destructive' : 'default', - duration: errors.length > 0 || successes.length > 5 ? 9000 : 5000, // Longer duration for errors or many successes - }); - } - }, [data, createdRowIds, updatedRowIds, deletedRowIds, toast]); - - const handleCancel = useCallback(() => { - setData(prevData); - createdRowIds.clear(); - updatedRowIds.clear(); - deletedRowIds.clear(); - }, [prevData, createdRowIds, updatedRowIds, deletedRowIds]); - - const isDirty = createdRowIds.size > 0 || updatedRowIds.size > 0 || deletedRowIds.size > 0; - - const changesSummaryString = useMemo(() => { - if (!isDirty) return ''; - - const totalChanges = createdRowIds.size + updatedRowIds.size + deletedRowIds.size; - - if (totalChanges === 0) { - // This case should ideally not be hit if isDirty is true, - // but as a fallback if counts are 0 but something else makes it dirty. - return '(Pending Changes)'; // Or simply '' if the button text itself is enough. - } - - return `(${totalChanges} ${totalChanges === 1 ? 'change' : 'changes'})`; - }, [isDirty, createdRowIds.size, updatedRowIds.size, deletedRowIds.size]); - - return { - dataForGrid: data, - handleGridChange, - getRowClassName, - handleCommit, - handleCancel, - isDirty, - createdRowIds, - updatedRowIds, - deletedRowIds, - changesSummaryString, - setDisplayedData, - }; -}; diff --git a/apps/framework-editor/app/(pages)/controls/page.tsx b/apps/framework-editor/app/(pages)/controls/page.tsx deleted file mode 100644 index d1644eacc..000000000 --- a/apps/framework-editor/app/(pages)/controls/page.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { isAuthorized } from '@/app/lib/utils'; -import { db } from '@comp/db'; -import { redirect } from 'next/navigation'; -import { ControlsClientPage } from './ControlsClientPage'; - -export default async function Page() { - const isAllowed = await isAuthorized(); - - if (!isAllowed) { - redirect('/auth'); - } - - const controls = await db.frameworkEditorControlTemplate.findMany({ - select: { - id: true, - name: true, - description: true, - createdAt: true, - updatedAt: true, - policyTemplates: { - select: { - id: true, - name: true, - }, - }, - requirements: { - select: { - id: true, - name: true, - framework: { - select: { - name: true, - }, - }, - }, - }, - taskTemplates: { - select: { - id: true, - name: true, - }, - }, - }, - orderBy: { - createdAt: 'asc', - }, - }); - - return ; -} diff --git a/apps/framework-editor/app/(pages)/controls/schemas.ts b/apps/framework-editor/app/(pages)/controls/schemas.ts deleted file mode 100644 index 0b74e3609..000000000 --- a/apps/framework-editor/app/(pages)/controls/schemas.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { z } from 'zod'; - -export const createControlTemplateSchema = z.object({ - name: z.string().min(1, { message: 'Name is required' }), - description: z.string().optional(), -}); diff --git a/apps/framework-editor/app/(pages)/controls/types.ts b/apps/framework-editor/app/(pages)/controls/types.ts deleted file mode 100644 index 558a11970..000000000 --- a/apps/framework-editor/app/(pages)/controls/types.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { FrameworkEditorControlTemplate } from '@prisma/client'; -// Import shared types from the common location -import type { SortDirection, SortableColumnOption } from '../../types/common'; - -// Basic item with id and name -export interface ItemWithName { - id: string; - name: string; -} - -// Define a more specific type for requirement items that can include framework info -export interface RequirementItemWithFramework extends ItemWithName { - framework?: { - name: string; - }; -} - -export interface RequirementGridItem extends ItemWithName { - frameworkName?: string; -} - -// Base type for data from the server with potential related entities -export interface FrameworkEditorControlTemplateWithRelatedData - extends FrameworkEditorControlTemplate { - policyTemplates?: ItemWithName[]; - requirements?: RequirementItemWithFramework[]; - taskTemplates?: ItemWithName[]; -} - -// Specific structure for the data displayed in the Controls page grid -export type ControlsPageGridData = { - id: string; - name: string | null; - description: string | null; - // Store the actual arrays of related items - policyTemplates: ItemWithName[]; - requirements: RequirementGridItem[]; - taskTemplates: ItemWithName[]; - // Add separate fields for sorting by count - policyTemplatesLength: number; - requirementsLength: number; - taskTemplatesLength: number; - createdAt: Date | null; - updatedAt: Date | null; -}; - -// react-datasheet-grid operation type -export type DSGOperation = - | { type: 'CREATE'; fromRowIndex: number; toRowIndex: number } - | { type: 'UPDATE'; fromRowIndex: number; toRowIndex: number } - | { type: 'DELETE'; fromRowIndex: number; toRowIndex: number }; - -// General types for sorting and toolbar options are now imported -// export type SortDirection = 'asc' | 'desc'; // Moved to common.ts - -// Specific sortable column keys for the Controls page table -export type ControlsPageSortableColumnKey = - | 'name' - | 'description' - // Update to use length fields for sorting - | 'policyTemplatesLength' - | 'requirementsLength' - | 'taskTemplatesLength' - | 'createdAt' - | 'updatedAt'; - -// Generic type for options in the sort dropdown for the toolbar is now imported -// export interface SortableColumnOption { // Moved to common.ts -// value: string; -// label: string; -// } - -// Re-export for convenience if ControlsClientPage needs them directly from this file -export type { SortDirection, SortableColumnOption }; diff --git a/apps/framework-editor/app/(pages)/frameworks/FrameworksClientPage.tsx b/apps/framework-editor/app/(pages)/frameworks/FrameworksClientPage.tsx deleted file mode 100644 index 8120f6d54..000000000 --- a/apps/framework-editor/app/(pages)/frameworks/FrameworksClientPage.tsx +++ /dev/null @@ -1,47 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -// import { db } from "@comp/db"; -import { DataTable } from '@/app/components/DataTable'; -import PageLayout from '@/app/components/PageLayout'; -import type { FrameworkEditorFramework } from '@prisma/client'; -import { columns } from './components/columns'; -import { CreateFrameworkDialog } from './components/CreateFrameworkDialog'; - -export interface FrameworkWithCounts extends Omit { - requirementsCount: number; - controlsCount: number; -} - -interface FrameworksClientPageProps { - initialFrameworks: FrameworkWithCounts[]; -} - -export function FrameworksClientPage({ initialFrameworks }: FrameworksClientPageProps) { - const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); - const router = useRouter(); - - const handleRowClick = (framework: FrameworkWithCounts) => { - router.push(`/frameworks/${framework.id}`); - }; - - return ( - - setIsCreateDialogOpen(true)} - createButtonLabel="Create New Framework" - onRowClick={handleRowClick} - /> - setIsCreateDialogOpen(false)} - /> - - ); -} diff --git a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/FrameworkRequirementsClientPage.tsx b/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/FrameworkRequirementsClientPage.tsx deleted file mode 100644 index 474bf2084..000000000 --- a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/FrameworkRequirementsClientPage.tsx +++ /dev/null @@ -1,214 +0,0 @@ -'use client'; - -import { DataTable } from '@/app/components/DataTable'; -import PageLayout from '@/app/components/PageLayout'; -import { Badge } from '@comp/ui/badge'; -import { Button } from '@comp/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@comp/ui/card'; -import type { FrameworkEditorFramework, FrameworkEditorRequirement } from '@prisma/client'; -import { FileText, PencilIcon, Trash2 } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { AddRequirementDialog } from './components/AddRequirementDialog'; -import { getColumns } from './components/columns'; -import { DeleteFrameworkDialog } from './components/DeleteFrameworkDialog'; -import { DeleteRequirementDialog } from './components/DeleteRequirementDialog'; -import { EditFrameworkDialog } from './components/EditFrameworkDialog'; -import { EditRequirementDialog } from './components/EditRequirementDialog'; - -interface FrameworkDetails - extends Pick {} - -interface FrameworkRequirementsClientPageProps { - frameworkDetails: FrameworkDetails; - initialRequirements: FrameworkEditorRequirement[]; -} - -interface SelectedRequirementToEdit extends FrameworkEditorRequirement { - frameworkId: string; -} - -export function FrameworkRequirementsClientPage({ - frameworkDetails, - initialRequirements, -}: FrameworkRequirementsClientPageProps) { - const router = useRouter(); - const [isFrameworkEditDialogOpen, setIsFrameworkEditDialogOpen] = useState(false); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isAddRequirementDialogOpen, setIsAddRequirementDialogOpen] = useState(false); - const [isDeleteRequirementDialogOpen, setIsDeleteRequirementDialogOpen] = useState(false); - const [requirementToDelete, setRequirementToDelete] = useState( - null, - ); - - const [requirementsList, setRequirementsList] = - useState(initialRequirements); - - const [isEditRequirementDialogOpen, setIsEditRequirementDialogOpen] = useState(false); - const [selectedRequirement, setSelectedRequirement] = useState( - null, - ); - - useEffect(() => { - setRequirementsList(initialRequirements); - }, [initialRequirements]); - - const handleFrameworkUpdated = () => { - setIsFrameworkEditDialogOpen(false); - router.refresh(); - }; - - const handleOpenEditRequirementDialog = (requirement: FrameworkEditorRequirement) => { - setSelectedRequirement({ - ...requirement, - frameworkId: frameworkDetails.id, - }); - setIsEditRequirementDialogOpen(true); - }; - - const handleRequirementUpdated = ( - updatedRequirement: Pick< - FrameworkEditorRequirement, - 'id' | 'name' | 'description' | 'identifier' - >, - ) => { - setRequirementsList((prevList) => - prevList.map((req) => - req.id === updatedRequirement.id ? { ...req, ...updatedRequirement } : req, - ), - ); - setIsEditRequirementDialogOpen(false); - router.refresh(); - }; - - const handleRequirementAdded = () => { - setIsAddRequirementDialogOpen(false); - router.refresh(); - }; - - const handleOpenDeleteRequirementDialog = (requirement: FrameworkEditorRequirement) => { - setRequirementToDelete(requirement); - setIsDeleteRequirementDialogOpen(true); - }; - - const handleRequirementDeleted = () => { - setRequirementToDelete(null); - setIsDeleteRequirementDialogOpen(false); - router.refresh(); - }; - - const columns = getColumns(handleOpenEditRequirementDialog, handleOpenDeleteRequirementDialog); - - return ( - - - -
-
- - {frameworkDetails.name} - - Version: {frameworkDetails.version} - - - {frameworkDetails.visible ? 'Visible' : 'Hidden'} - - - - {frameworkDetails.description} - -
-
- - -
-
-
- -
- - ID: {frameworkDetails.id} -
-
-
- - setIsAddRequirementDialogOpen(true)} - /> - {isFrameworkEditDialogOpen && ( - - )} - {isDeleteDialogOpen && ( - - )} - {isAddRequirementDialogOpen && ( - - )} - {isEditRequirementDialogOpen && selectedRequirement && ( - - )} - {isDeleteRequirementDialogOpen && requirementToDelete && ( - - )} -
- ); -} diff --git a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/actions/add-requirement-action.ts b/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/actions/add-requirement-action.ts deleted file mode 100644 index bba504663..000000000 --- a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/actions/add-requirement-action.ts +++ /dev/null @@ -1,88 +0,0 @@ -'use server'; - -import { db } from '@comp/db'; -import type { FrameworkEditorRequirement } from '@prisma/client'; -import { revalidatePath } from 'next/cache'; -import { z } from 'zod'; - -// Schema for validating requirement input -const AddRequirementSchema = z.object({ - name: z.string().min(3, { message: 'Requirement name must be at least 3 characters long' }), - description: z.string().optional(), // Description can be optional - identifier: z.string().optional(), // Identifier can be optional - frameworkId: z.string().min(1, { message: 'Framework ID is required' }), -}); - -export interface AddRequirementActionState { - success: boolean; - data?: FrameworkEditorRequirement; // Return the created requirement on success - error?: string; - issues?: z.ZodIssue[]; - message?: string; // Added message property for success/general messages -} - -export async function addRequirementAction( - prevState: AddRequirementActionState | null, - formData: FormData, -): Promise { - const rawInput = { - name: formData.get('name'), - description: formData.get('description'), - identifier: formData.get('identifier'), - frameworkId: formData.get('frameworkId'), - }; - - const validationResult = AddRequirementSchema.safeParse(rawInput); - - if (!validationResult.success) { - return { - success: false, - error: 'Invalid input for requirement.', - issues: validationResult.error.issues, - }; - } - - const { name, description, identifier, frameworkId } = validationResult.data; - - try { - const newRequirement = await db.frameworkEditorRequirement.create({ - data: { - name, - description: description || '', // Ensure description is at least an empty string if optional and not provided - identifier: identifier || '', // Ensure identifier is at least an empty string if optional and not provided - framework: { - connect: { id: frameworkId }, - }, - // Assuming `order` might be a field you want to manage, e.g., for new requirements - // If not, it can be omitted if it has a default or is nullable. - // order: await getNextOrderForFramework(frameworkId) // Example: utility function to determine order - }, - }); - - // Revalidate the path to the framework's requirements page to show the new requirement - revalidatePath(`/frameworks/${frameworkId}`); - - return { - success: true, - data: newRequirement, - message: 'Requirement added successfully.', - }; - } catch (error) { - console.error('Failed to create requirement:', error); - const errorMessage = - error instanceof Error ? error.message : 'An unexpected database error occurred.'; - return { - success: false, - error: `Database error: ${errorMessage}`, - }; - } -} - -// Example utility function (if needed for fields like 'order') -// async function getNextOrderForFramework(frameworkId: string): Promise { -// const lastRequirement = await db.frameworkEditorRequirement.findFirst({ -// where: { frameworkId }, -// orderBy: { order: 'desc' }, -// }); -// return (lastRequirement?.order ?? 0) + 1; -// } diff --git a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/actions/delete-framework.ts b/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/actions/delete-framework.ts deleted file mode 100644 index 2af2b6e5f..000000000 --- a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/actions/delete-framework.ts +++ /dev/null @@ -1,75 +0,0 @@ -'use server'; - -import { db } from '@comp/db'; // Uncommented and assuming this is the correct path to your Prisma client instance -import { revalidatePath } from 'next/cache'; -import { redirect } from 'next/navigation'; -import { z } from 'zod'; - -// Schema for input validation -const DeleteFrameworkSchema = z.object({ - frameworkId: z.string().min(1, { message: 'Framework ID is required' }), -}); - -export interface DeleteFrameworkActionState { - success: boolean; - error?: string; - message?: string; // Optional: for success messages before redirect or more detailed errors - issues?: z.ZodIssue[]; -} - -export async function deleteFrameworkAction( - // prevState is not strictly used here as redirect happens on success, but kept for pattern consistency - prevState: DeleteFrameworkActionState | null, - formData: FormData, -): Promise { - const rawInput = { - frameworkId: formData.get('frameworkId'), - }; - - const validationResult = DeleteFrameworkSchema.safeParse(rawInput); - - if (!validationResult.success) { - return { - success: false, - error: 'Invalid input: Framework ID is missing or invalid.', - issues: validationResult.error.issues, - }; - } - - const { frameworkId } = validationResult.data; - - try { - // Step 1: Delete all related FrameworkEditorRequirement records - // This is important if cascading deletes are not configured or if you want explicit control. - console.log(`Attempting to delete requirements for frameworkId: ${frameworkId}`); - await db.frameworkEditorRequirement.deleteMany({ - where: { frameworkId: frameworkId }, - }); - console.log(`Successfully deleted requirements for frameworkId: ${frameworkId}`); - - // Step 2: Delete the FrameworkEditorFramework itself - console.log(`Attempting to delete framework with id: ${frameworkId}`); - await db.frameworkEditorFramework.delete({ - where: { id: frameworkId }, - }); - console.log(`Successfully deleted framework with id: ${frameworkId}`); - - revalidatePath('/frameworks'); - revalidatePath(`/frameworks/${frameworkId}`); // Revalidate the specific framework page - } catch (error) { - console.error('Failed to delete framework and/or its requirements:', error); - const errorMessage = - error instanceof Error ? error.message : 'An unexpected server error occurred.'; - return { - success: false, - error: `Database operation failed: ${errorMessage}`, - }; - } - - // If deletion logic is successful, redirect to the frameworks list page. - // redirect() will throw a NEXT_REDIRECT error, and Next.js will handle the client-side redirection. - redirect('/frameworks'); - - // This part is effectively unreachable due to redirect(), but can be here for type consistency or if redirect was conditional. - // return { success: true, message: "Framework deleted successfully. Redirecting..." }; -} diff --git a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/actions/delete-requirement-action.ts b/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/actions/delete-requirement-action.ts deleted file mode 100644 index 754be7c5b..000000000 --- a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/actions/delete-requirement-action.ts +++ /dev/null @@ -1,76 +0,0 @@ -'use server'; - -import { db } from '@comp/db'; -import { Prisma } from '@prisma/client'; // Import Prisma for error types -import { revalidatePath } from 'next/cache'; -import { z } from 'zod'; - -const DeleteRequirementSchema = z.object({ - requirementId: z.string().min(1, { message: 'Requirement ID is required' }), - frameworkId: z.string().min(1, { message: 'Framework ID is required for revalidation' }), -}); - -export interface DeleteRequirementActionState { - success: boolean; - error?: string; - issues?: z.ZodIssue[]; -} - -export async function deleteRequirementAction( - prevState: DeleteRequirementActionState | null, - formData: FormData, -): Promise { - const rawInput = { - requirementId: formData.get('requirementId'), - frameworkId: formData.get('frameworkId'), - }; - - const validationResult = DeleteRequirementSchema.safeParse(rawInput); - - if (!validationResult.success) { - return { - success: false, - error: 'Invalid input for deleting requirement.', - issues: validationResult.error.issues, - }; - } - - const { requirementId, frameworkId } = validationResult.data; - - try { - // Before deleting a requirement, ensure any dependent entities are handled - // e.g., if controls are linked to requirements and there's a relation that needs cleanup. - // For a simple delete where Prisma handles cascades or there are no strict FK constraints preventing delete: - await db.frameworkEditorRequirement.delete({ - where: { id: requirementId }, - }); - - revalidatePath(`/frameworks/${frameworkId}`); - - return { - success: true, - }; - } catch (error) { - console.error('Failed to delete requirement:', error); - if (error instanceof Prisma.PrismaClientKnownRequestError) { - // Specific Prisma errors - if (error.code === 'P2025') { - return { - success: false, - error: 'Requirement not found or already deleted.', - }; - } - // Add other Prisma error codes to handle if needed, e.g., P2003 for foreign key constraints - // if (error.code === 'P2003') { - // return { success: false, error: 'Cannot delete requirement due to existing relations (e.g., linked controls).' }; - // } - } - // General error message - const errorMessage = - error instanceof Error ? error.message : 'An unexpected database error occurred.'; - return { - success: false, - error: `Database error: ${errorMessage}`, - }; - } -} diff --git a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/components/AddRequirementDialog.tsx b/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/components/AddRequirementDialog.tsx deleted file mode 100644 index dd60fcc3e..000000000 --- a/apps/framework-editor/app/(pages)/frameworks/[frameworkId]/components/AddRequirementDialog.tsx +++ /dev/null @@ -1,190 +0,0 @@ -'use client'; - -import { Button } from '@comp/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@comp/ui/dialog'; -import { Input } from '@comp/ui/input'; -import { Label } from '@comp/ui/label'; -import { Textarea } from '@comp/ui/textarea'; -import { useToast } from '@comp/ui/use-toast'; -import { useActionState, useEffect, useState } from 'react'; -import { useFormStatus } from 'react-dom'; -import { - addRequirementAction, - type AddRequirementActionState, -} from '../actions/add-requirement-action'; - -const initialFormState: AddRequirementActionState = { - success: false, - error: undefined, - issues: undefined, - data: undefined, - message: undefined, -}; - -interface AddRequirementDialogProps { - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; - frameworkId: string; - onRequirementAdded: () => void; -} - -function SubmitButton() { - const { pending } = useFormStatus(); - return ( - - ); -} - -export function AddRequirementDialog({ - isOpen, - onOpenChange, - frameworkId, - onRequirementAdded, -}: AddRequirementDialogProps) { - const { toast } = useToast(); - const [formKey, setFormKey] = useState(Date.now()); - const [formState, formAction, isPending] = useActionState(addRequirementAction, initialFormState); - const [name, setName] = useState(''); - const [description, setDescription] = useState(''); - const [identifier, setIdentifier] = useState(''); - - useEffect(() => { - if (formState.success && formState.data) { - toast({ - title: 'Success!', - description: formState.message || 'New requirement added successfully.', - }); - onRequirementAdded(); // Close dialog and refresh list - setName(''); // Reset local state - setDescription(''); - setIdentifier(''); - // The form itself will reset due to the key change on next open if desired, or if onOpenChange triggers a reset - } else if (!formState.success && (formState.error || formState.issues)) { - const issueMessages = - formState.issues?.map((i) => `${i.path.join('.')} : ${i.message}`).join('; ') || ''; - toast({ - title: 'Error Adding Requirement', - description: formState.error || issueMessages || 'An unexpected error occurred.', - variant: 'destructive', - }); - } - }, [formState, onRequirementAdded, toast]); - - const handleOpenChange = (open: boolean) => { - if (open) { - // Reset form fields and formState when dialog is opened - setName(''); - setDescription(''); - setIdentifier(''); - setFormKey(Date.now()); // Reset form state by changing key, ensuring useFormState re-initializes - } else { - // If dialog is closed ensure parent knows - onOpenChange(false); - } - // Call onOpenChange to control dialog visibility from parent - // This might be redundant if the above else already calls it, but ensures consistency. - // onOpenChange(open); - }; - - // We only want to call onOpenChange from parent when dialog truly intends to close. - // The Dialog component itself calls its onOpenChange prop for various reasons (escape, click outside). - // So, we manage our internal logic first, then call parent's onOpenChange when appropriate. - - return ( - - - - Add New Requirement - - Fill in the details for the new requirement for this framework. - - -
- {' '} - {/* Add key here */} - -
-
- - setName(e.target.value)} - className="col-span-3" - required - disabled={isPending} - /> - {formState.issues?.find((issue) => issue.path.includes('name')) && ( -

- {formState.issues.find((issue) => issue.path.includes('name'))?.message} -

- )} -
-
- - setIdentifier(e.target.value)} - className="col-span-3" - placeholder="e.g., cc1-1" - disabled={isPending} - /> - {formState.issues?.find((issue) => issue.path.includes('identifier')) && ( -

- {formState.issues.find((issue) => issue.path.includes('identifier'))?.message} -

- )} -
-
- -