diff --git a/.github/workflows/ci-chrome-extension.yaml b/.github/workflows/ci-chrome-extension.yaml index a78be9415597..6f99f922a94d 100644 --- a/.github/workflows/ci-chrome-extension.yaml +++ b/.github/workflows/ci-chrome-extension.yaml @@ -3,13 +3,9 @@ on: push: branches: - main - paths: - - 'package.json' - - 'packages/twenty-chrome-extension/**' + pull_request: - paths: - - 'package.json' - - 'packages/twenty-chrome-extension/**' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -26,7 +22,25 @@ jobs: with: access_token: ${{ github.token }} - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + package.json + packages/twenty-chrome-extension/** + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Chrome Extension / Run build + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx build twenty-chrome-extension + + - name: Mark as Valid if No Changes + if: steps.changed-files.outputs.changed != 'true' + run: | + echo "No relevant changes detected. Marking as valid." diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index b61506709f93..1aed46afe407 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -3,15 +3,9 @@ on: push: branches: - main - paths: - - 'package.json' - - 'packages/twenty-front/**' - - 'packages/twenty-ui/**' + pull_request: - paths: - - 'package.json' - - 'packages/twenty-front/**' - - 'packages/twenty-ui/**' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -29,21 +23,43 @@ jobs: access_token: ${{ github.token }} - name: Fetch local actions uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + package.json + packages/twenty-front/** + packages/twenty-ui/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Diagnostic disk space issue + if: steps.changed-files.outputs.any_changed == 'true' run: df -h - name: Front / Restore Storybook Task Cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:frontend tasks: storybook:build - name: Front / Write .env + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx reset:env twenty-front - name: Front / Build storybook + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx storybook:build twenty-front front-sb-test: - runs-on: ci-8-cores + runs-on: shipfox-8vcpu-ubuntu-2204 + timeout-minutes: 60 needs: front-sb-build strategy: matrix: @@ -54,35 +70,70 @@ jobs: steps: - name: Fetch local actions uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-front/** + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Install Playwright + if: steps.changed-files.outputs.any_changed == 'true' run: cd packages/twenty-front && npx playwright install - name: Front / Restore Storybook Task Cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:frontend tasks: storybook:build - name: Front / Write .env + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx reset:env twenty-front - name: Run storybook tests + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} front-sb-test-performance: - runs-on: ci-8-cores + runs-on: shipfox-8vcpu-ubuntu-2204 + timeout-minutes: 60 env: REACT_APP_SERVER_BASE_URL: http://localhost:3000 NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 steps: - name: Fetch local actions uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-front/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Install Playwright + if: steps.changed-files.outputs.any_changed == 'true' run: cd packages/twenty-front && npx playwright install - name: Front / Write .env + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx reset:env twenty-front - name: Run storybook tests - run: npx nx storybook:serve-and-test:static:performance twenty-front + if: steps.changed-files.outputs.any_changed == 'true' + run: npx nx run twenty-front:storybook:serve-and-test:static:performance front-chromatic-deployment: if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push' needs: front-sb-build @@ -95,19 +146,35 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-front/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Front / Restore Storybook Task Cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:frontend tasks: storybook:build - name: Front / Write .env + if: steps.changed-files.outputs.any_changed == 'true' run: | cd packages/twenty-front touch .env echo "REACT_APP_SERVER_BASE_URL: $REACT_APP_SERVER_BASE_URL" >> .env - name: Publish to Chromatic + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx run twenty-front:chromatic:ci front-task: runs-on: ubuntu-latest @@ -125,20 +192,35 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-front/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Front / Restore ${{ matrix.task }} task cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:frontend tasks: ${{ matrix.task }} - name: Reset .env + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:frontend tasks: reset:env - name: Run ${{ matrix.task }} task + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:frontend - tasks: ${{ matrix.task }} \ No newline at end of file + tasks: ${{ matrix.task }} diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index 074d63fdda40..b4fb89e4d90a 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -3,15 +3,9 @@ on: push: branches: - main - paths: - - 'package.json' - - 'packages/twenty-server/**' - - 'packages/twenty-emails/**' + pull_request: - paths: - - 'package.json' - - 'packages/twenty-server/**' - - 'packages/twenty-emails/**' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -38,22 +32,38 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + package.json + packages/twenty-server/** + packages/twenty-emails/** + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run lint & typecheck + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend tasks: lint,typecheck - name: Server / Build + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx build twenty-server - name: Server / Write .env + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx reset:env twenty-server - name: Worker / Run + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx run twenty-server:worker:ci server-test: @@ -66,13 +76,26 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + package.json + packages/twenty-server/** + packages/twenty-emails/** + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run Tests + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend @@ -100,17 +123,30 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + package.json + packages/twenty-server/** + packages/twenty-emails/** + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run Integration Tests + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend - tasks: "test:integration" + tasks: "test:integration:with-db-reset" - name: Server / Upload reset-logs file if: always() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/ci-test-docker-compose.yaml b/.github/workflows/ci-test-docker-compose.yaml index 1496425c8511..50277ab7aa31 100644 --- a/.github/workflows/ci-test-docker-compose.yaml +++ b/.github/workflows/ci-test-docker-compose.yaml @@ -1,8 +1,7 @@ name: 'Test Docker Compose' on: pull_request: - paths: - - 'packages/twenty-docker/**' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -13,8 +12,19 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-docker/** + docker-compose.yml + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed != 'true' + run: echo "No relevant changes detected. Marking as valid." - name: Run compose + if: steps.changed-files.outputs.any_changed == 'true' run: | echo "Patching docker-compose.yml..." # change image to localbuild using yq @@ -31,10 +41,7 @@ jobs: cp .env.example .env echo "Generating secrets..." echo "# === Randomly generated secrets ===" >>.env - echo "ACCESS_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env - echo "LOGIN_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env - echo "REFRESH_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env - echo "FILE_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env + echo "APP_SECRET=$(openssl rand -base64 32)" >>.env echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env echo "Starting server..." diff --git a/.github/workflows/ci-tinybird.yaml b/.github/workflows/ci-tinybird.yaml new file mode 100644 index 000000000000..8a83f3e8177b --- /dev/null +++ b/.github/workflows/ci-tinybird.yaml @@ -0,0 +1,34 @@ +name: CI Tinybird +on: + push: + branches: + - main + + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + package.json + packages/twenty-tinybird/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + + - name: Check twenty-tinybird package + uses: tinybirdco/ci/.github/workflows/ci.yml@main + with: + data_project_dir: packages/twenty-tinybird + tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }} + tb_host: https://api.eu-central-1.aws.tinybird.co diff --git a/.github/workflows/ci-website.yaml b/.github/workflows/ci-website.yaml index d79345f3bf53..1b015c2a34ed 100644 --- a/.github/workflows/ci-website.yaml +++ b/.github/workflows/ci-website.yaml @@ -3,13 +3,10 @@ on: push: branches: - main - paths: - - 'package.json' - - 'packages/twenty-website/**' + pull_request: - paths: - - 'package.json' - - 'packages/twenty-website/**' + + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -27,13 +24,29 @@ jobs: - 5432:5432 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: 'package.json, packages/twenty-website/**' + - name: Install dependencies + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/yarn-install + - name: Website / Run migrations + if: steps.changed-files.outputs.changed == 'true' run: npx nx database:migrate twenty-website env: DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default - name: Website / Build Website + if: steps.changed-files.outputs.changed == 'true' run: npx nx build twenty-website env: - DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default \ No newline at end of file + DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default + + - name: Mark as VALID + if: steps.changed-files.outputs.changed != 'true' # If no changes, mark as valid + run: echo "No relevant changes detected. CI is valid." \ No newline at end of file diff --git a/.github/workflows/playwright.yml.bak b/.github/workflows/playwright.yml.bak index cffb50287629..fc45955bc7ab 100644 --- a/.github/workflows/playwright.yml.bak +++ b/.github/workflows/playwright.yml.bak @@ -13,11 +13,27 @@ jobs: - uses: actions/setup-node@v4 with: node-version: lts/* + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/** # Adjust this to your relevant directories + playwright.config.ts # Include any relevant config files + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed != 'true' + run: echo "No relevant changes detected. Marking as valid." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' run: npm install -g yarn && yarn - name: Install Playwright Browsers + if: steps.changed-files.outputs.any_changed == 'true' run: yarn playwright install --with-deps - name: Run Playwright tests + if: steps.changed-files.outputs.any_changed == 'true' run: yarn test:e2e companies - uses: actions/upload-artifact@v4 if: always() diff --git a/.gitignore b/.gitignore index c5bb33e003d1..20f974922280 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ storybook-static .nyc_output test-results/ dump.rdb +.tinyb diff --git a/.vscode/settings.json b/.vscode/settings.json index d63c92973cfc..fde6cdfb1ffb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,5 +45,5 @@ "search.exclude": { "**/.yarn": true, }, - "eslint.debug": true + "eslint.debug": true, } diff --git a/LICENSE b/LICENSE index 0ad25db4bd1d..50a6a10a2861 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,8 @@ + +This project is mostly licensed under the GNU General Public License (GPL) as described below. However, certain files within this project are licensed under a different commercial license. These files are clearly marked with the following comment at the top of the file: /* @license Enterprise */ +Files with this comment are not licensed under the aGPL v3, but instead are subject to the commercial license terms defined later in this file. + + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 @@ -659,3 +664,47 @@ specific requirements. if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . + + + +------------------------------------ + + + + The Twenty.com Commercial License (the “Commercial License”) + Copyright (c) 2023-present Twenty.com, PBC + +With regard to Twenty's Software: + +This part of the software and associated documentation files (the "Software") may only be +used in production, if you (and any entity that you represent) have agreed to, +and are in compliance with, the Terms available +at https://twenty.com/legal/terms, or other agreements governing +the use of the Software, as mutually agreed by you and Twenty.com, PBC ("Twenty"), +and otherwise have a valid Twenty Enterprise Edition subscription +for the correct number of hosts and seats as defined in the Commercial Terms. +Subject to the foregoing sentence, +you are free to modify this Software and publish patches to the Software. You agree +that Twenty and/or its licensors (as applicable) retain all right, title and interest in +and to all such modifications and/or patches, and all such modifications and/or +patches may only be used, copied, modified, displayed, distributed, or otherwise +exploited with a valid Commercial Subscription for the correct number of hosts and seats. +Notwithstanding the foregoing, you may copy and modify the Software for development +and testing purposes, without requiring a subscription. You agree that Twenty.Com and/or +its licensors (as applicable) retain all right, title and interest in and to all such +modifications. You are not granted any other rights beyond what is expressly stated herein. +Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, +and/or sell the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +For all third party components incorporated into the Twenty Software, those +components are licensed under the original license provided by the owner of the +applicable component. \ No newline at end of file diff --git a/Makefile b/Makefile index 764dbd1531d4..af42f19d9fb0 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,12 @@ -docker-dev-build: - make -C packages/twenty-docker dev-build - -docker-dev-up: - make -C packages/twenty-docker dev-up - -docker-dev-start: - make -C packages/twenty-docker dev-start - -docker-dev-stop: - make -C packages/twenty-docker dev-stop - -docker-dev-sh: - make -C packages/twenty-docker dev-sh - postgres-on-docker: - make -C packages/twenty-postgres provision-on-docker - -postgres-on-macos-arm: - make -C packages/twenty-postgres provision-on-macos-arm - -postgres-on-macos-intel: - make -C packages/twenty-postgres provision-on-macos-intel - -postgres-on-linux: - make -C packages/twenty-postgres provision-on-linux + docker run \ + --name twenty_postgres \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=default \ + -v twenty_db_data:/var/lib/postgresql/data \ + -p 5432:5432 \ + twentycrm/twenty-postgres:latest + +redis-on-docker: + docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest \ No newline at end of file diff --git a/README.md b/README.md index 86ed0fe51a6f..d75db65e1504 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,3 @@ -

- - - - - Hacktoberfest - - -


diff --git a/install.sh b/install.sh index 39eb096b8e25..34741b89b908 100755 --- a/install.sh +++ b/install.sh @@ -45,7 +45,7 @@ trap on_exit EXIT # Use environment variables VERSION and BRANCH, with defaults if not set version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)} -branch=${BRANCH:-main} +branch=${BRANCH:-$version} echo "🚀 Using version $version and branch $branch" @@ -72,7 +72,7 @@ done echo "📁 Creating directory '$dir_name'" mkdir -p "$dir_name" && cd "$dir_name" || { echo "❌ Failed to create/access directory '$dir_name'"; exit 1; } -# Copy the twenty/packages/twenty-docker/docker-compose.yml file in it +# Copy twenty/packages/twenty-docker/docker-compose.yml in it echo -e "\t• Copying docker-compose.yml" curl -sLo docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/docker-compose.yml @@ -91,10 +91,7 @@ fi # Generate random strings for secrets echo "# === Randomly generated secrets ===" >>.env -echo "ACCESS_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env -echo "LOGIN_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env -echo "REFRESH_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env -echo "FILE_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env +echo "APP_SECRET=$(openssl rand -base64 32)" >>.env echo "" >>.env echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env diff --git a/nx.json b/nx.json index 35b6e7501700..19c6f5462cce 100644 --- a/nx.json +++ b/nx.json @@ -108,12 +108,11 @@ "storybook:build": { "executor": "nx:run-commands", "cache": true, - "dependsOn": ["^build"], "inputs": ["^default", "excludeTests"], "outputs": ["{projectRoot}/{options.output-dir}"], "options": { "cwd": "{projectRoot}", - "command": "storybook build", + "command": "VITE_DISABLE_ESLINT_CHECKER=true storybook build", "output-dir": "storybook-static", "config-dir": ".storybook" } @@ -192,16 +191,7 @@ "executor": "nx:run-commands", "options": { "commands": [ - "npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port}'" - ], - "port": 6006 - } - }, - "storybook:serve-and-test:static:performance": { - "executor": "nx:run-commands", - "options": { - "commands": [ - "npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:dev {projectName} --configuration=performance --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test:no-coverage {projectName} --port={args.port} --configuration=performance'" + "npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port} --configuration={args.performance}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'" ], "port": 6006 } diff --git a/oss-gg/twenty-content-challenges/1-create-youtube-video-about-20.md b/oss-gg/twenty-content-challenges/1-create-youtube-video-about-20.md deleted file mode 100644 index 455b5e35bae3..000000000000 --- a/oss-gg/twenty-content-challenges/1-create-youtube-video-about-20.md +++ /dev/null @@ -1,21 +0,0 @@ -**Side Quest**: Create a YouTube Video about Twenty showcasing a specific way to use Twenty effectively. -**Points**: 750 Points -**Proof**: Add your oss handle and YouTube video link to the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » YouTube Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) YouTube Link: [YouTube](https://twenty.com/) - ---- \ No newline at end of file diff --git a/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md b/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md deleted file mode 100644 index a4c4e6bee944..000000000000 --- a/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md +++ /dev/null @@ -1,21 +0,0 @@ -**Side Quest**: Write a blog post about sharing your experience using Twenty in a detailed format on any platform. -**Points**: 750 Points -**Proof**: Add your oss handle and blog link to the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » blog Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) blog Link: [blog](https://twenty.com/) - ---- \ No newline at end of file diff --git a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md deleted file mode 100644 index c7352ec430fc..000000000000 --- a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md +++ /dev/null @@ -1,21 +0,0 @@ -**Side Quest**: Write a blog post about self-hosting Twenty in a detailed format on any platform. -**Points**: 750 Points -**Proof**: Add your oss handle and blog link to the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » blog Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) blog Link: [blog](https://twenty.com/) - ---- \ No newline at end of file diff --git a/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md b/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md deleted file mode 100644 index e52cb43a4247..000000000000 --- a/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md +++ /dev/null @@ -1,21 +0,0 @@ -**Side Quest**: Create a promotional video for Twenty and share it on social media. -**Points**: 750 Points -**Proof**: Add your oss handle and video link to the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » video Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) - ---- \ No newline at end of file diff --git a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md deleted file mode 100644 index b61f187117b5..000000000000 --- a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md +++ /dev/null @@ -1,25 +0,0 @@ -**Side Quest**: Design a promotional poster of Twenty and share it on social media. -**Points**: 300 Points -**Proof**: Add your oss handle and poster link to the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » poster Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) poster Link: [poster](https://twenty.com/) - -» 11-October-2024 by [thefool76](https://oss.gg/thefool76) poster Link: [poster](https://drive.google.com/file/d/1cIC1eitvY6zKVTXKq2LnVrS_2Ho9H8-P/view?usp=sharing) - -» 12-October-2024 by [Ionfinisher](https://oss.gg/Ionfinisher) poster Link: [poster](https://x.com/ion_finisher/status/1845168965963628802) - ---- diff --git a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md deleted file mode 100644 index ceee0fa8e4da..000000000000 --- a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md +++ /dev/null @@ -1,28 +0,0 @@ -**Side Quest**: Design/Create new Twenty logo, tweet your design, and mention @twentycrm. -**Points**: 300 Points -**Proof**: Create a logo upload it on any of the platform and add your oss handle and logo link to the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » Logo Link: https://link.to/content » tweet Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 08-October-2024 by [adityadeshlahre](https://oss.gg/adityadeshlahre) Logo Link: [logo](https://drive.google.com/drive/folders/13k22xMnX2fhnWK94vas_hO1t-ImqXcHZ?usp=drive_link) » tweet Link: [tweet](https://x.com/adityadeshlahre/status/1843354963176718374) - -» 11-October-2024 by [thefool76](https://oss.gg/thefool76) Logo Link: [logo](https://drive.google.com/file/d/1DxSwNY_i90kGgWzPQj5SxScBz_6r02l4/view?usp=sharing) » tweet Link: [tweet](https://x.com/thefool1135/status/1844693487067034008) - -» 13-October-2024 by [Atharva_404](https://oss.gg/Atharva-3000) Logo Link: [logo](https://drive.google.com/drive/folders/1XB7ELR7kPA4x7Fx5RQr8wo5etdZAZgcs?usp=drive_link) » tweet Link: [tweet](https://x.com/0x_atharva/status/1845421218914095453) - -» 13-October-2024 by [Ionfinisher](https://oss.gg/Ionfinisher) Logo Link: [logo](https://drive.google.com/file/d/1l9vE8CIjW9KfdioI5WKzxrdmvO8LR4j7/view?usp=drive_link) » tweet Link: [tweet](https://x.com/ion_finisher/status/1845466470429442163) - - ---- diff --git a/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md b/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md deleted file mode 100644 index e51945ea9988..000000000000 --- a/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md +++ /dev/null @@ -1,21 +0,0 @@ -**Side Quest**: Duplicate the Figma file from the main repo and customize the variables to create a unique interface theme for Twenty. -**Points**: 750 Points -**Proof**: Add your oss handle and Figma link to the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » Figma Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) Figma Link: [Figma](https://twenty.com/) - ---- \ No newline at end of file diff --git a/oss-gg/twenty-dev-challenges/1-write-migration-script-other-crm-to-20.md b/oss-gg/twenty-dev-challenges/1-write-migration-script-other-crm-to-20.md deleted file mode 100644 index 249d8e158cfa..000000000000 --- a/oss-gg/twenty-dev-challenges/1-write-migration-script-other-crm-to-20.md +++ /dev/null @@ -1,21 +0,0 @@ -**Side Quest**: Develop a script to facilitate the migration of data from another CRM to Twenty. -**Points**: 750 Points -**Proof**: Add your oss handle and record video and share link to the list below. In video show the working proof of your created script. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » video Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) - ---- \ No newline at end of file diff --git a/oss-gg/twenty-dev-challenges/2-create-raycast-integration-for-20.md b/oss-gg/twenty-dev-challenges/2-create-raycast-integration-for-20.md deleted file mode 100644 index e4793c40d66f..000000000000 --- a/oss-gg/twenty-dev-challenges/2-create-raycast-integration-for-20.md +++ /dev/null @@ -1,21 +0,0 @@ -**Side Quest**: Develop an integration for Raycast that enables users to create records on any object within Twenty directly from Raycast. -**Points**: 1500 Points -**Proof**: Add your oss handle and record video and share link to the list below. In video show the workflow of the your integration created and perform some task. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » video Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) - ---- \ No newline at end of file diff --git a/oss-gg/twenty-no-code-challenges/1-create-n8n-template-integrate-20-API.md b/oss-gg/twenty-no-code-challenges/1-create-n8n-template-integrate-20-API.md deleted file mode 100644 index 6786e5a94553..000000000000 --- a/oss-gg/twenty-no-code-challenges/1-create-n8n-template-integrate-20-API.md +++ /dev/null @@ -1,21 +0,0 @@ -**Side Quest**: Create an n8n workflow that empowers Twenty by connecting it to another tool. -**Points**: 750 Points -**Proof**: Add your oss handle and template link to the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » template Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) template Link: [template](https://twenty.com/) - ---- \ No newline at end of file diff --git a/oss-gg/twenty-no-code-challenges/2-write-selfthost-guide-blog-post-20.md b/oss-gg/twenty-no-code-challenges/2-write-selfthost-guide-blog-post-20.md deleted file mode 100644 index 58fa6de4d8d6..000000000000 --- a/oss-gg/twenty-no-code-challenges/2-write-selfthost-guide-blog-post-20.md +++ /dev/null @@ -1,21 +0,0 @@ -**Side Quest**: Write a comprehensive guide on how to integrate Twenty with marketing automation tool (n8n, Zapier). Include a concrete use case and explain how to leverage AI to write API requests for non-developers and share it. -**Points**: 1500 Points -**Proof**: Add your oss handle and guide link to the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR oss.gg HANDLE » guide Link: https://link.to/content - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) guide Link: [guide](https://twenty.com/) - ---- \ No newline at end of file diff --git a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md deleted file mode 100644 index 168e311b9a32..000000000000 --- a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md +++ /dev/null @@ -1,45 +0,0 @@ -**Side Quest**: Like & Re-Tweet oss.gg Launch Tweet. Quote-tweet it tagging @twentycrm to say you’ll be contributing. -**Points**: 50 Points -**Proof**: Add a screenshot of the retweet to the PR description. Add a link to your retweet in the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR NAME -» Link to Tweet: https://x.com/... - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 13-October-2024 by Vanshika Dargan -» Link to Tweet: https://x.com/VanshikaDargan/status/1845467453108949123 - -» 13-October-2024 by Utsav Bhattarai -» Link to Tweet: https://x.com/utsavdotdev/status/1845417863462649900 - -» 10-October-2024 by Devansh Baghel -» Link to Tweet: https://x.com/DevanshBaghel5/status/1844359648037748954 - -» 11-October-2024 by Bhavesh Mishra -» Link to Tweet: https://x.com/thefool1135/status/1844453425188405326 - -» 11-October-2024 by Chirag Arora -» Link to Tweet: https://x.com/Chirag8023/status/1844689900668682699 - -» 11-October-2024 by Aritra Sadhukhan -» Link to Tweet: https://x.com/AritraDevelops/status/1844670236512878646 - -» 13-October-2024 by Nabhag Motivaras -» Link to Tweet: https://x.com/NabhagMotivaras/status/1845449144695218357 - -» 13-October-2024 by Ali Yar Khan -» Link to Tweet: https://x.com/Mr_Programmer14/status/1845527862549577860 - -» 13-October-2024 by Yash Parmar -» Link to Tweet: https://x.com/yashp3020/status/1845720834716959009 diff --git a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md deleted file mode 100644 index 508210ae5a6e..000000000000 --- a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md +++ /dev/null @@ -1,28 +0,0 @@ -**Side Quest**: Share a tweet about your favorite feature in Twenty. Tweet about your favorite feature in Twenty and mention @twentycrm. -**Points**: 50 Points -**Proof**: Add a screenshot of the tweet to the PR description. Add a link to your tweet in the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR NAME -» Link to Tweet: https://x.com/... - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 10-October-2024 by Devansh Baghel -» Link to Tweet: https://x.com/DevanshBaghel5/status/1844384722119704972 - -» 11-October-2024 by Bhavesh Mishra -» Link to Tweet: https://x.com/thefool1135/status/1844456500380696969 - -» 13-October-2024 by Ali Yar Khan -» Link to Tweet: https://x.com/Mr_Programmer14/status/1845530448245711197 ---- diff --git a/oss-gg/twenty-side-quest/3-bug-report.md b/oss-gg/twenty-side-quest/3-bug-report.md deleted file mode 100644 index d393a2cbeac8..000000000000 --- a/oss-gg/twenty-side-quest/3-bug-report.md +++ /dev/null @@ -1,23 +0,0 @@ -**Side Quest**: Create a bug report. Use the Twenty bug issue template to report a bug in detail, including steps to reproduce it. -**Points**: 50-150 Points -**Proof**: Add a link to your bug report in the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR NAME -» Link to bug report: https://github.com/twentyhq/twenty/issues/... - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 10-October-2024 by Devansh Baghel -» Link to bug report: https://github.com/twentyhq/twenty/issues/7560 - ---- diff --git a/oss-gg/twenty-side-quest/4-meme-magic.md b/oss-gg/twenty-side-quest/4-meme-magic.md deleted file mode 100644 index b5cb3263d218..000000000000 --- a/oss-gg/twenty-side-quest/4-meme-magic.md +++ /dev/null @@ -1,34 +0,0 @@ -**Side Quest**: Meme Magic: Craft a meme where the number twenty plays a role. Tweet it, and tag @twentycrm. -**Points**: 150 Points -**Proof**: Add a screenshot of meme to the PR description. Add a link to your tweet in the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR NAME -» Link to Tweet: https://x.com/... - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 10-October-2024 by Teddy ASSIH -» Link to Tweet: https://x.com/ion_finisher/status/1844389252253299173 - -» 11-October-2024 by Bhavesh Mishra -» Link to Tweet: https://x.com/thefool1135/status/1844458836402503931 - -» 12-October-2024 by Chirag Arora -» Link to Tweet: https://x.com/Chirag8023/status/1845108226527994222 - -» 13-October-2024 by Ali Yar Khan -» Link to Tweet: https://x.com/Mr_Programmer14/status/1845537662587072697 - -» 14-October-2024 by Yash Parmar -» Link to Tweet: [https://x.com/yashp3020/status/1845108226527994222](https://x.com/yashp3020/status/1845720142702842093) ---- diff --git a/oss-gg/twenty-side-quest/5-gif-magic.md b/oss-gg/twenty-side-quest/5-gif-magic.md deleted file mode 100644 index 20467fef4784..000000000000 --- a/oss-gg/twenty-side-quest/5-gif-magic.md +++ /dev/null @@ -1,33 +0,0 @@ -**Side Quest**: Gif Magic: Create a gif related to Twenty. Tweet it, and tag @twentycrm. -**Points**: 150 Points -**Proof**: Add a screenshot of GIF on Giphy to the PR description. Add a link to your GIPHY in the list below. - -Please follow the following schema: - ---- - -» 05-April-2024 by YOUR NAME -» Link to gif: https://giphy.com/... - ---- - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 10-October-2024 by Teddy ASSIH -» Link to gif: https://giphy.com/gifs/oss-crm-twenty-VWDHAIlGTbc6Nqdza9 - -» 11-October-2024 by Bhavesh Mishra -» Link to gif: https://shorturl.at/yln9H - -» 12-October-2024 by Chirag Arora -» Link to gif: https://giphy.com/gifs/yCJIS2MGbBdifbnuj0 - -» 13-October-2024 by Nabhag Motivaras -» Link to gif: https://giphy.com/gifs/twenty-twentycrm-opensourcecrm-wCcsmnJuzzzGrfuf9B - - ---- diff --git a/oss-gg/twenty-side-quest/6-quest-wizard.md b/oss-gg/twenty-side-quest/6-quest-wizard.md deleted file mode 100644 index 9543e3767d6f..000000000000 --- a/oss-gg/twenty-side-quest/6-quest-wizard.md +++ /dev/null @@ -1,19 +0,0 @@ -**Side Quest**: Complete all Twenty side quests -**Points**: 300 Points -**Proof**: Add screenshots for each side quest to the PR description. Add your name to the list below. - -Please follow the following schema: - ---- - - » 05-April-2024 by YOUR NAME - -//////////////////////////// - -Your turn 👇 - -//////////////////////////// - -» 01-October-2024 by X - ---- diff --git a/package.json b/package.json index a4dc90df92ac..c9c7f6f1ef34 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@linaria/core": "^6.2.0", "@linaria/react": "^6.2.1", "@mdx-js/react": "^3.0.0", + "@microsoft/microsoft-graph-client": "^3.0.7", "@nestjs/apollo": "^11.0.5", "@nestjs/axios": "^3.0.1", "@nestjs/cli": "^9.0.0", @@ -49,6 +50,7 @@ "@stoplight/elements": "^8.0.5", "@swc/jest": "^0.2.29", "@tabler/icons-react": "^2.44.0", + "@tiptap/extension-hard-break": "^2.9.1", "@types/dompurify": "^3.0.5", "@types/facepaint": "^1.2.5", "@types/lodash.camelcase": "^4.3.7", @@ -176,6 +178,7 @@ "scroll-into-view": "^1.16.2", "semver": "^7.5.4", "sharp": "^0.32.1", + "slash": "^5.1.0", "stripe": "^14.17.0", "ts-key-enum": "^2.0.12", "tslib": "^2.3.0", @@ -199,6 +202,7 @@ "@graphql-codegen/typescript": "^3.0.4", "@graphql-codegen/typescript-operations": "^3.0.4", "@graphql-codegen/typescript-react-apollo": "^3.3.7", + "@microsoft/microsoft-graph-types": "^2.40.0", "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^9.0.0", @@ -294,6 +298,7 @@ "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prettier": "^5.1.2", + "eslint-plugin-project-structure": "^3.9.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.4", @@ -347,7 +352,7 @@ "version": "0.2.1", "nx": {}, "scripts": { - "start": "npx nx run-many -t start -p twenty-server twenty-front" + "start": "npx concurrently --kill-others 'npx nx run-many -t start -p twenty-server twenty-front' 'npx wait-on tcp:3000 && npx nx run twenty-server:worker'" }, "workspaces": { "packages": [ diff --git a/packages/twenty-chrome-extension/src/options/Loading.tsx b/packages/twenty-chrome-extension/src/options/Loading.tsx index 1fde24f2b9e5..a0543c9c6dad 100644 --- a/packages/twenty-chrome-extension/src/options/Loading.tsx +++ b/packages/twenty-chrome-extension/src/options/Loading.tsx @@ -1,6 +1,5 @@ -import styled from '@emotion/styled'; - import { Loader } from '@/ui/display/loader/components/Loader'; +import styled from '@emotion/styled'; const StyledContainer = styled.div` align-items: center; diff --git a/packages/twenty-docker/.env.example b/packages/twenty-docker/.env.example index 59d8d03f93a7..b10e09876e52 100644 --- a/packages/twenty-docker/.env.example +++ b/packages/twenty-docker/.env.example @@ -3,16 +3,12 @@ TAG=latest # POSTGRES_ADMIN_PASSWORD=replace_me_with_a_strong_password PG_DATABASE_HOST=db:5432 +REDIS_URL=redis://redis:6379 SERVER_URL=http://localhost:3000 -# REDIS_HOST=redis -# REDIS_PORT=6379 # Use openssl rand -base64 32 for each secret -# ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access -# LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login -# REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh -# FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh +# APP_SECRET=replace_me_with_a_random_string SIGN_IN_PREFILLED=true diff --git a/packages/twenty-docker/docker-compose.yml b/packages/twenty-docker/docker-compose.yml index b2efc1a168e4..41d80dabc398 100644 --- a/packages/twenty-docker/docker-compose.yml +++ b/packages/twenty-docker/docker-compose.yml @@ -25,8 +25,7 @@ services: PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default SERVER_URL: ${SERVER_URL} FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL} - REDIS_PORT: ${REDIS_PORT:-6379} - REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_URL: ${REDIS_URL:-redis://localhost:6379} ENABLE_DB_MIGRATIONS: "true" @@ -36,10 +35,7 @@ services: STORAGE_S3_NAME: ${STORAGE_S3_NAME} STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} - ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET} - LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET} - REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET} - FILE_TOKEN_SECRET: ${FILE_TOKEN_SECRET} + APP_SECRET: ${APP_SECRET} depends_on: change-vol-ownership: condition: service_completed_successfully @@ -59,8 +55,7 @@ services: PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default SERVER_URL: ${SERVER_URL} FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL} - REDIS_PORT: ${REDIS_PORT:-6379} - REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_URL: ${REDIS_URL:-redis://localhost:6379} ENABLE_DB_MIGRATIONS: "false" # it already runs on the server @@ -69,10 +64,7 @@ services: STORAGE_S3_NAME: ${STORAGE_S3_NAME} STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} - ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET} - LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET} - REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET} - FILE_TOKEN_SECRET: ${FILE_TOKEN_SECRET} + APP_SECRET: ${APP_SECRET} depends_on: db: condition: service_healthy diff --git a/packages/twenty-docker/k8s/manifests/deployment-server.yaml b/packages/twenty-docker/k8s/manifests/deployment-server.yaml index b1229d649bbb..857729788675 100644 --- a/packages/twenty-docker/k8s/manifests/deployment-server.yaml +++ b/packages/twenty-docker/k8s/manifests/deployment-server.yaml @@ -41,10 +41,8 @@ spec: value: "https://crm.example.com:443" - name: "PG_DATABASE_URL" value: "postgres://twenty:twenty@twenty-db.twentycrm.svc.cluster.local/default" - - name: "REDIS_HOST" - value: "twentycrm-redis.twentycrm.svc.cluster.local" - - name: "REDIS_PORT" - value: 6379 + - name: "REDIS_URL" + value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379" - name: ENABLE_DB_MIGRATIONS value: "true" - name: SIGN_IN_PREFILLED @@ -57,26 +55,11 @@ spec: value: "7d" - name: "LOGIN_TOKEN_EXPIRES_IN" value: "1h" - - name: ACCESS_TOKEN_SECRET + - name: APP_SECRET valueFrom: secretKeyRef: name: tokens key: accessToken - - name: LOGIN_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: loginToken - - name: REFRESH_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: refreshToken - - name: FILE_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: fileToken ports: - containerPort: 3000 name: http-tcp diff --git a/packages/twenty-docker/k8s/manifests/deployment-worker.yaml b/packages/twenty-docker/k8s/manifests/deployment-worker.yaml index b3a7e07a19aa..eb1938ba6dda 100644 --- a/packages/twenty-docker/k8s/manifests/deployment-worker.yaml +++ b/packages/twenty-docker/k8s/manifests/deployment-worker.yaml @@ -40,30 +40,13 @@ spec: value: "bull-mq" - name: "CACHE_STORAGE_TYPE" value: "redis" - - name: "REDIS_HOST" - value: "twentycrm-redis.twentycrm.svc.cluster.local" - - name: "REDIS_PORT" - value: 6379 - - name: ACCESS_TOKEN_SECRET + - name: "REDIS_URL" + value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379" + - name: APP_SECRET valueFrom: secretKeyRef: name: tokens key: accessToken - - name: LOGIN_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: loginToken - - name: REFRESH_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: refreshToken - - name: FILE_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: fileToken command: - yarn - worker:prod diff --git a/packages/twenty-docker/k8s/terraform/deployment-server.tf b/packages/twenty-docker/k8s/terraform/deployment-server.tf index 1868b17624da..5276d574319e 100644 --- a/packages/twenty-docker/k8s/terraform/deployment-server.tf +++ b/packages/twenty-docker/k8s/terraform/deployment-server.tf @@ -61,12 +61,8 @@ resource "kubernetes_deployment" "twentycrm_server" { value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" } env { - name = "REDIS_HOST" - value = "${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local" - } - env { - name = "REDIS_PORT" - value = 6379 + name = "REDIS_URL" + value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379" } env { name = "ENABLE_DB_MIGRATIONS" @@ -95,7 +91,7 @@ resource "kubernetes_deployment" "twentycrm_server" { value = "1h" } env { - name = "ACCESS_TOKEN_SECRET" + name = "APP_SECRET" value_from { secret_key_ref { name = "tokens" @@ -104,36 +100,6 @@ resource "kubernetes_deployment" "twentycrm_server" { } } - env { - name = "LOGIN_TOKEN_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "loginToken" - } - } - } - - env { - name = "REFRESH_TOKEN_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "refreshToken" - } - } - } - - env { - name = "FILE_TOKEN_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "fileToken" - } - } - } - port { container_port = 3000 protocol = "TCP" diff --git a/packages/twenty-docker/k8s/terraform/deployment-worker.tf b/packages/twenty-docker/k8s/terraform/deployment-worker.tf index 78e5ea6dcc1d..aa68fd3af2da 100644 --- a/packages/twenty-docker/k8s/terraform/deployment-worker.tf +++ b/packages/twenty-docker/k8s/terraform/deployment-worker.tf @@ -59,13 +59,8 @@ resource "kubernetes_deployment" "twentycrm_worker" { } env { - name = "REDIS_HOST" - value = "${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local" - } - - env { - name = "REDIS_PORT" - value = 6379 + name = "REDIS_URL" + value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379" } env { @@ -83,7 +78,7 @@ resource "kubernetes_deployment" "twentycrm_worker" { } env { - name = "ACCESS_TOKEN_SECRET" + name = "APP_SECRET" value_from { secret_key_ref { name = "tokens" @@ -92,36 +87,6 @@ resource "kubernetes_deployment" "twentycrm_worker" { } } - env { - name = "LOGIN_TOKEN_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "loginToken" - } - } - } - - env { - name = "REFRESH_TOKEN_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "refreshToken" - } - } - } - - env { - name = "FILE_TOKEN_SECRET" - value_from { - secret_key_ref { - name = "tokens" - key = "fileToken" - } - } - } - resources { requests = { cpu = "250m" diff --git a/packages/twenty-docker/twenty-postgres-spilo/Dockerfile b/packages/twenty-docker/twenty-postgres-spilo/Dockerfile index 21e107c477bb..a87a8a97ec12 100644 --- a/packages/twenty-docker/twenty-postgres-spilo/Dockerfile +++ b/packages/twenty-docker/twenty-postgres-spilo/Dockerfile @@ -1,6 +1,5 @@ ARG POSTGRES_VERSION=15 ARG SPILO_VERSION=3.2-p1 -ARG PG_GRAPHQL_VERSION=1.5.6 ARG WRAPPERS_VERSION=0.2.0 # Build the mysql_fdw extension @@ -38,10 +37,9 @@ WORKDIR /build/openssl RUN ./config && make && make install -# Extend the Spilo image with the pg_graphql and mysql_fdw extensions +# Extend the Spilo image with the mysql_fdw extensions FROM ghcr.io/zalando/spilo-${POSTGRES_VERSION}:${SPILO_VERSION} ARG POSTGRES_VERSION -ARG PG_GRAPHQL_VERSION ARG WRAPPERS_VERSION ARG TARGETARCH @@ -63,14 +61,6 @@ RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_ COPY --from=build-libssl /usr/local/lib/libssl* /usr/local/lib/libcrypto* /usr/lib/ COPY --from=build-libssl /usr/local/lib/engines-1.1 /usr/lib/engines-1.1 -# Copy pg_graphql -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${POSTGRES_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql--${PG_GRAPHQL_VERSION}.sql \ - /usr/share/postgresql/${POSTGRES_VERSION}/extension -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${POSTGRES_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.control \ - /usr/share/postgresql/${POSTGRES_VERSION}/extension -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${POSTGRES_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.so \ - /usr/lib/postgresql/${POSTGRES_VERSION}/lib/pg_graphql.so - # Copy mysql_fdw COPY --from=build-mysql_fdw /mysql_fdw/mysql_fdw.so \ /usr/lib/postgresql/${POSTGRES_VERSION}/lib/mysql_fdw.so diff --git a/packages/twenty-docker/twenty-postgres/Dockerfile b/packages/twenty-docker/twenty-postgres/Dockerfile index 5647a6cd35ac..9c9b96398e66 100644 --- a/packages/twenty-docker/twenty-postgres/Dockerfile +++ b/packages/twenty-docker/twenty-postgres/Dockerfile @@ -3,7 +3,6 @@ ARG IMAGE_TAG='15.5.0-debian-11-r15' FROM bitnami/postgresql:${IMAGE_TAG} ARG PG_MAIN_VERSION=15 -ARG PG_GRAPHQL_VERSION=1.5.6 ARG WRAPPERS_VERSION=0.2.0 ARG TARGETARCH @@ -26,14 +25,6 @@ RUN set -eux; \ RUN apt update && apt install build-essential git curl default-libmysqlclient-dev -y -# Install precompiled pg_graphql extensions -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql--${PG_GRAPHQL_VERSION}.sql \ - /opt/bitnami/postgresql/share/extension/ -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.control \ - /opt/bitnami/postgresql/share/extension/ -COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.so \ - /opt/bitnami/postgresql/lib/ - # Install precompiled supabase wrappers extensions RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_VERSION}/wrappers-v${WRAPPERS_VERSION}-pg${PG_MAIN_VERSION}-${TARGETARCH}-linux-gnu.deb" -o wrappers.deb RUN dpkg --install wrappers.deb diff --git a/packages/twenty-docker/twenty-website/Dockerfile b/packages/twenty-docker/twenty-website/Dockerfile new file mode 100644 index 000000000000..e3b7420ff76a --- /dev/null +++ b/packages/twenty-docker/twenty-website/Dockerfile @@ -0,0 +1,29 @@ +FROM node:18.17.1-alpine as twenty-website-build + + +WORKDIR /app + +COPY ./package.json . +COPY ./yarn.lock . +COPY ./.yarnrc.yml . +COPY ./.yarn/releases /app/.yarn/releases +COPY ./tools/eslint-rules /app/tools/eslint-rules +COPY ./packages/twenty-website/package.json /app/packages/twenty-website/package.json + +RUN yarn + +COPY ./packages/twenty-website /app/packages/twenty-website +RUN npx nx build twenty-website + +FROM node:18.17.1-alpine as twenty-website + +WORKDIR /app/packages/twenty-website + +COPY --from=twenty-website-build /app /app + +WORKDIR /app/packages/twenty-website + +LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty +LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the website." + +CMD ["/bin/sh", "-c", "npx nx start"] \ No newline at end of file diff --git a/packages/twenty-e2e-testing/drivers/env_variables.ts b/packages/twenty-e2e-testing/drivers/env_variables.ts new file mode 100644 index 000000000000..2bb7f57d88fb --- /dev/null +++ b/packages/twenty-e2e-testing/drivers/env_variables.ts @@ -0,0 +1,22 @@ +import * as fs from 'fs'; +import path from 'path'; + +export const envVariables = (variables: string) => { + let payload = ` + PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default + FRONT_BASE_URL=http://localhost:3001 + ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access + LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login + REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh + FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh + REDIS_URL=redis://localhost:6379 + `; + payload = payload.concat(variables); + fs.writeFile( + path.join(__dirname, '..', '..', 'twenty-server', '.env'), + payload, + (err) => { + throw err; + }, + ); +}; diff --git a/packages/twenty-e2e-testing/lib/pom/helper/confirmationModal.ts b/packages/twenty-e2e-testing/lib/pom/helper/confirmationModal.ts new file mode 100644 index 000000000000..225c6733b49c --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/helper/confirmationModal.ts @@ -0,0 +1,36 @@ +import { Locator, Page } from '@playwright/test'; + +export class ConfirmationModal { + private readonly input: Locator; + private readonly cancelButton: Locator; + private readonly confirmButton: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.input = page.getByTestId('confirmation-modal-input'); + this.cancelButton = page.getByRole('button', { name: 'Cancel' }); + this.confirmButton = page.getByTestId('confirmation-modal-confirm-button'); + } + + async typePlaceholderToInput() { + await this.page + .getByTestId('confirmation-modal-input') + .fill( + await this.page + .getByTestId('confirmation-modal-input') + .getAttribute('placeholder'), + ); + } + + async typePhraseToInput(value: string) { + await this.page.getByTestId('confirmation-modal-input').fill(value); + } + + async clickCancelButton() { + await this.page.getByRole('button', { name: 'Cancel' }).click(); + } + + async clickConfirmButton() { + await this.page.getByTestId('confirmation-modal-confirm-button').click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/helper/formatDate.function.ts b/packages/twenty-e2e-testing/lib/pom/helper/formatDate.function.ts new file mode 100644 index 000000000000..bffa490e80f4 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/helper/formatDate.function.ts @@ -0,0 +1,28 @@ +const nth = (d: number) => { + if (d > 3 && d < 21) return 'th'; + switch (d % 10) { + case 1: + return 'st'; + case 2: + return 'nd'; + case 3: + return 'rd'; + default: + return 'th'; + } +}; + +// label looks like this: Choose Wednesday, October 30th, 2024 +// eslint-disable-next-line prefer-arrow/prefer-arrow-functions +export function formatDate(value: string): string { + const date = new Date(value); + return 'Choose '.concat( + date.toLocaleDateString('en-US', { weekday: 'long' }), + ', ', + date.toLocaleDateString('en-US', { month: 'long' }), + ' ', + nth(date.getDate()), + ', ', + date.toLocaleDateString('en-US', { year: 'numeric' }), + ); +} diff --git a/packages/twenty-e2e-testing/lib/pom/helper/googleLogin.ts b/packages/twenty-e2e-testing/lib/pom/helper/googleLogin.ts new file mode 100644 index 000000000000..ca57fd6361f5 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/helper/googleLogin.ts @@ -0,0 +1,6 @@ +import { Locator, Page } from '@playwright/test'; + +export class GoogleLogin { + // TODO: map all things like inputs and buttons + // (what's the correct way for proceeding with Google interaction? log in each time test is performed?) +} diff --git a/packages/twenty-e2e-testing/lib/pom/helper/iconSelect.ts b/packages/twenty-e2e-testing/lib/pom/helper/iconSelect.ts new file mode 100644 index 000000000000..82b30c53c7af --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/helper/iconSelect.ts @@ -0,0 +1,23 @@ +import { Locator, Page } from '@playwright/test'; + +export class IconSelect { + private readonly iconSelectButton: Locator; + private readonly iconSearchInput: Locator; + + constructor(public readonly page: Page) { + this.iconSelectButton = page.getByLabel('Click to select icon ('); + this.iconSearchInput = page.getByPlaceholder('Search icon'); + } + + async selectIcon(name: string) { + await this.iconSelectButton.click(); + await this.iconSearchInput.fill(name); + await this.page.getByTitle(name).click(); + } + + async selectRelationIcon(name: string) { + await this.iconSelectButton.nth(1).click(); + await this.iconSearchInput.fill(name); + await this.page.getByTitle(name).click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/helper/insertFieldData.ts b/packages/twenty-e2e-testing/lib/pom/helper/insertFieldData.ts new file mode 100644 index 000000000000..a052eff68c03 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/helper/insertFieldData.ts @@ -0,0 +1,267 @@ +import { Locator, Page } from '@playwright/test'; +import { formatDate } from './formatDate.function'; + +export class InsertFieldData { + private readonly address1Input: Locator; + private readonly address2Input: Locator; + private readonly cityInput: Locator; + private readonly stateInput: Locator; + private readonly postCodeInput: Locator; + private readonly countrySelect: Locator; + private readonly arrayValueInput: Locator; + private readonly arrayAddValueButton: Locator; + // boolean react after click so no need to write special locator + private readonly currencySelect: Locator; + private readonly currencyAmountInput: Locator; + private readonly monthSelect: Locator; + private readonly yearSelect: Locator; + private readonly previousMonthButton: Locator; + private readonly nextMonthButton: Locator; + private readonly clearDateButton: Locator; + private readonly dateInput: Locator; + private readonly firstNameInput: Locator; + private readonly lastNameInput: Locator; + private readonly addURLButton: Locator; + private readonly setAsPrimaryButton: Locator; + private readonly addPhoneButton: Locator; + private readonly addMailButton: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.address1Input = page.locator( + '//label[contains(., "ADDRESS 1")]/../div[last()]/input', + ); + this.address2Input = page.locator( + '//label[contains(., "ADDRESS 2")]/../div[last()]/input', + ); + this.cityInput = page.locator( + '//label[contains(., "CITY")]/../div[last()]/input', + ); + this.stateInput = page.locator( + '//label[contains(., "STATE")]/../div[last()]/input', + ); + this.postCodeInput = page.locator( + '//label[contains(., "POST CODE")]/../div[last()]/input', + ); + this.countrySelect = page.locator( + '//span[contains(., "COUNTRY")]/../div[last()]/input', + ); + this.arrayValueInput = page.locator("//input[@placeholder='Enter value']"); + this.arrayAddValueButton = page.locator( + "//div[@data-testid='tooltip' and contains(.,'Add item')]", + ); + this.currencySelect = page.locator( + '//body/div[last()]/div/div/div[first()]/div/div', + ); + this.currencyAmountInput = page.locator("//input[@placeholder='Currency']"); + this.monthSelect; // TODO: add once some other attributes are added + this.yearSelect; + this.previousMonthButton; + this.nextMonthButton; + this.clearDateButton = page.locator( + "//div[@data-testid='tooltip' and contains(., 'Clear')]", + ); + this.dateInput = page.locator("//input[@placeholder='Type date and time']"); + this.firstNameInput = page.locator("//input[@placeholder='First name']"); // may fail if placeholder is `F‌‌irst name` instead of `First name` + this.lastNameInput = page.locator("//input[@placeholder='Last name']"); // may fail if placeholder is `L‌‌ast name` instead of `Last name` + this.addURLButton = page.locator( + "//div[@data-testid='tooltip' and contains(., 'Add URL')]", + ); + this.setAsPrimaryButton = page.locator( + "//div[@data-testid='tooltip' and contains(., 'Set as primary')]", + ); + this.addPhoneButton = page.locator( + "//div[@data-testid='tooltip' and contains(., 'Add Phone')]", + ); + this.addMailButton = page.locator( + "//div[@data-testid='tooltip' and contains(., 'Add Email')]", + ); + } + + // address + async typeAddress1(value: string) { + await this.address1Input.fill(value); + } + + async typeAddress2(value: string) { + await this.address2Input.fill(value); + } + + async typeCity(value: string) { + await this.cityInput.fill(value); + } + + async typeState(value: string) { + await this.stateInput.fill(value); + } + + async typePostCode(value: string) { + await this.postCodeInput.fill(value); + } + + async selectCountry(value: string) { + await this.countrySelect.click(); + await this.page + .locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`) + .click(); + } + + // array + async typeArrayValue(value: string) { + await this.arrayValueInput.fill(value); + } + + async clickAddItemButton() { + await this.arrayAddValueButton.click(); + } + + // currency + async selectCurrency(value: string) { + await this.currencySelect.click(); + await this.page + .locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`) + .click(); + } + + async typeCurrencyAmount(value: string) { + await this.currencyAmountInput.fill(value); + } + + // date(-time) + async typeDate(value: string) { + await this.dateInput.fill(value); + } + + async selectMonth(value: string) { + await this.monthSelect.click(); + await this.page + .locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`) + .click(); + } + + async selectYear(value: string) { + await this.yearSelect.click(); + await this.page + .locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`) + .click(); + } + + async clickPreviousMonthButton() { + await this.previousMonthButton.click(); + } + + async clickNextMonthButton() { + await this.nextMonthButton.click(); + } + + async selectDay(value: string) { + await this.page + .locator(`//div[@aria-label='${formatDate(value)}']`) + .click(); + } + + async clearDate() { + await this.clearDateButton.click(); + } + + // email + async typeEmail(value: string) { + await this.page.locator(`//input[@placeholder='Email']`).fill(value); + } + + async clickAddMailButton() { + await this.addMailButton.click(); + } + + // full name + async typeFirstName(name: string) { + await this.firstNameInput.fill(name); + } + + async typeLastName(name: string) { + await this.lastNameInput.fill(name); + } + + // JSON + // placeholder is dependent on the name of field + async typeJSON(placeholder: string, value: string) { + await this.page + .locator(`//input[@placeholder='${placeholder}']`) + .fill(value); + } + + // link + async typeLink(value: string) { + await this.page.locator("//input[@placeholder='URL']").fill(value); + } + + async clickAddURL() { + await this.addURLButton.click(); + } + + // (multi-)select + async selectValue(value: string) { + await this.page + .locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`) + .click(); + } + + // number + // placeholder is dependent on the name of field + async typeNumber(placeholder: string, value: string) { + await this.page + .locator(`//input[@placeholder='${placeholder}']`) + .fill(value); + } + + // phones + async selectCountryPhoneCode(countryCode: string) { + await this.page + .locator( + `//div[@data-testid='tooltip' and contains(., '${countryCode}')]`, + ) + .click(); + } + + async typePhoneNumber(value: string) { + await this.page.locator(`//input[@placeholder='Phone']`).fill(value); + } + + async clickAddPhoneButton() { + await this.addPhoneButton.click(); + } + + // rating + // if adding rating for the first time, hover must be used + async selectRating(rating: number) { + await this.page.locator(`//div[@role='slider']/div[${rating}]`).click(); + } + + // text + // placeholder is dependent on the name of field + async typeText(placeholder: string, value: string) { + await this.page + .locator(`//input[@placeholder='${placeholder}']`) + .fill(value); + } + + async clickSetAsPrimaryButton() { + await this.setAsPrimaryButton.click(); + } + + async searchValue(value: string) { + await this.page.locator(`//div[@placeholder='Search']`).fill(value); + } + + async clickEditButton() { + await this.page + .locator("//div[@data-testid='tooltip' and contains(., 'Edit')]") + .click(); + } + + async clickDeleteButton() { + await this.page + .locator("//div[@data-testid='tooltip' and contains(., 'Delete')]") + .click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/helper/stripePage.ts b/packages/twenty-e2e-testing/lib/pom/helper/stripePage.ts new file mode 100644 index 000000000000..ccef759f916d --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/helper/stripePage.ts @@ -0,0 +1,5 @@ +import { Locator, Page } from '@playwright/test'; + +export class StripePage { + // TODO: implement all necessary methods (staging/sandbox page - does it differ anyhow from normal page?) +} diff --git a/packages/twenty-e2e-testing/lib/pom/helper/uploadImage.ts b/packages/twenty-e2e-testing/lib/pom/helper/uploadImage.ts new file mode 100644 index 000000000000..41493fdc0e19 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/helper/uploadImage.ts @@ -0,0 +1,25 @@ +import { Locator, Page } from '@playwright/test'; + +export class UploadImage { + private readonly imagePreview: Locator; + private readonly uploadButton: Locator; + private readonly removeButton: Locator; + + constructor(public readonly page: Page) { + this.imagePreview = page.locator('.css-6eut39'); //TODO: add attribute to make it independent of theme + this.uploadButton = page.getByRole('button', { name: 'Upload' }); + this.removeButton = page.getByRole('button', { name: 'Remove' }); + } + + async clickImagePreview() { + await this.imagePreview.click(); + } + + async clickUploadButton() { + await this.uploadButton.click(); + } + + async clickRemoveButton() { + await this.removeButton.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/leftMenu.ts b/packages/twenty-e2e-testing/lib/pom/leftMenu.ts new file mode 100644 index 000000000000..c58c3f7f1a02 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/leftMenu.ts @@ -0,0 +1,115 @@ +import { Locator, Page } from '@playwright/test'; + +export class LeftMenu { + private readonly workspaceDropdown: Locator; + private readonly leftMenu: Locator; + private readonly searchSubTab: Locator; + private readonly settingsTab: Locator; + private readonly peopleTab: Locator; + private readonly companiesTab: Locator; + private readonly opportunitiesTab: Locator; + private readonly opportunitiesTabAll: Locator; + private readonly opportunitiesTabByStage: Locator; + private readonly tasksTab: Locator; + private readonly tasksTabAll: Locator; + private readonly tasksTabByStatus: Locator; + private readonly notesTab: Locator; + private readonly rocketsTab: Locator; + private readonly workflowsTab: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.workspaceDropdown = page.getByTestId('workspace-dropdown'); + this.leftMenu = page.getByRole('button').first(); + this.searchSubTab = page.getByText('Search'); + this.settingsTab = page.getByRole('link', { name: 'Settings' }); + this.peopleTab = page.getByRole('link', { name: 'People' }); + this.companiesTab = page.getByRole('link', { name: 'Companies' }); + this.opportunitiesTab = page.getByRole('link', { name: 'Opportunities' }); + this.opportunitiesTabAll = page.getByRole('link', { + name: 'All', + exact: true, + }); + this.opportunitiesTabByStage = page.getByRole('link', { name: 'By Stage' }); + this.tasksTab = page.getByRole('link', { name: 'Tasks' }); + this.tasksTabAll = page.getByRole('link', { name: 'All tasks' }); + this.tasksTabByStatus = page.getByRole('link', { name: 'Notes' }); + this.notesTab = page.getByRole('link', { name: 'Notes' }); + this.rocketsTab = page.getByRole('link', { name: 'Rockets' }); + this.workflowsTab = page.getByRole('link', { name: 'Workflows' }); + } + + async selectWorkspace(workspaceName: string) { + await this.workspaceDropdown.click(); + await this.page + .getByTestId('tooltip') + .filter({ hasText: workspaceName }) + .click(); + } + + async changeLeftMenu() { + await this.leftMenu.click(); + } + + async openSearchTab() { + await this.searchSubTab.click(); + } + + async goToSettings() { + await this.settingsTab.click(); + } + + async goToPeopleTab() { + await this.peopleTab.click(); + } + + async goToCompaniesTab() { + await this.companiesTab.click(); + } + + async goToOpportunitiesTab() { + await this.opportunitiesTab.click(); + } + + async goToOpportunitiesTableView() { + await this.opportunitiesTabAll.click(); + } + + async goToOpportunitiesKanbanView() { + await this.opportunitiesTabByStage.click(); + } + + async goToTasksTab() { + await this.tasksTab.click(); + } + + async goToTasksTableView() { + await this.tasksTabAll.click(); + } + + async goToTasksKanbanView() { + await this.tasksTabByStatus.click(); + } + + async goToNotesTab() { + await this.notesTab.click(); + } + + async goToRocketsTab() { + await this.rocketsTab.click(); + } + + async goToWorkflowsTab() { + await this.workflowsTab.click(); + } + + async goToCustomObject(customObjectName: string) { + await this.page.getByRole('link', { name: customObjectName }).click(); + } + + async goToCustomObjectView(name: string) { + await this.page.getByRole('link', { name: name }).click(); + } +} + +export default LeftMenu; diff --git a/packages/twenty-e2e-testing/lib/pom/loginPage.ts b/packages/twenty-e2e-testing/lib/pom/loginPage.ts new file mode 100644 index 000000000000..dc60d3f7a799 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/loginPage.ts @@ -0,0 +1,187 @@ +import { Locator, Page } from '@playwright/test'; + +export class LoginPage { + private readonly loginWithGoogleButton: Locator; + private readonly loginWithEmailButton: Locator; + private readonly termsOfServiceLink: Locator; + private readonly privacyPolicyLink: Locator; + private readonly emailField: Locator; + private readonly continueButton: Locator; + private readonly forgotPasswordButton: Locator; + private readonly passwordField: Locator; + private readonly revealPasswordButton: Locator; + private readonly signInButton: Locator; + private readonly signUpButton: Locator; + private readonly previewImageButton: Locator; + private readonly uploadImageButton: Locator; + private readonly deleteImageButton: Locator; + private readonly workspaceNameField: Locator; + private readonly firstNameField: Locator; + private readonly lastNameField: Locator; + private readonly syncEverythingWithGoogleRadio: Locator; + private readonly syncSubjectAndMetadataWithGoogleRadio: Locator; + private readonly syncMetadataWithGoogleRadio: Locator; + private readonly syncWithGoogleButton: Locator; + private readonly noSyncButton: Locator; + private readonly inviteLinkField1: Locator; + private readonly inviteLinkField2: Locator; + private readonly inviteLinkField3: Locator; + private readonly copyInviteLink: Locator; + private readonly finishButton: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.loginWithGoogleButton = page.getByRole('button', { + name: 'Continue with Google', + }); + this.loginWithEmailButton = page.getByRole('button', { + name: 'Continue With Email', + }); + this.termsOfServiceLink = page.getByRole('link', { + name: 'Terms of Service', + }); + this.privacyPolicyLink = page.getByRole('link', { name: 'Privacy Policy' }); + this.emailField = page.getByPlaceholder('Email'); + this.continueButton = page.getByRole('button', { + name: 'Continue', + exact: true, + }); + this.forgotPasswordButton = page.getByText('Forgot your password?'); + this.passwordField = page.getByPlaceholder('Password'); + this.revealPasswordButton = page.getByTestId('reveal-password-button'); + this.signInButton = page.getByRole('button', { name: 'Sign in' }); + this.signUpButton = page.getByRole('button', { name: 'Sign up' }); + this.previewImageButton = page.locator('.css-1qzw107'); // TODO: fix + this.uploadImageButton = page.getByRole('button', { name: 'Upload' }); + this.deleteImageButton = page.getByRole('button', { name: 'Remove' }); + this.workspaceNameField = page.getByPlaceholder('Apple'); + this.firstNameField = page.getByPlaceholder('Tim'); + this.lastNameField = page.getByPlaceholder('Cook'); + this.syncEverythingWithGoogleRadio = page.locator( + 'input[value="SHARE_EVERYTHING"]', + ); + this.syncSubjectAndMetadataWithGoogleRadio = page.locator( + 'input[value="SUBJECT"]', + ); + this.syncMetadataWithGoogleRadio = page.locator('input[value="METADATA"]'); + this.syncWithGoogleButton = page.getByRole('button', { + name: 'Sync with Google', + }); + this.noSyncButton = page.getByText('Continue without sync'); + this.inviteLinkField1 = page.getByPlaceholder('tim@apple.dev'); + this.inviteLinkField2 = page.getByPlaceholder('craig@apple.dev'); + this.inviteLinkField3 = page.getByPlaceholder('mike@apple.dev'); + this.copyInviteLink = page.getByRole('button', { + name: 'Copy invitation link', + }); + this.finishButton = page.getByRole('button', { name: 'Finish' }); + } + + async loginWithGoogle() { + await this.loginWithGoogleButton.click(); + } + + async clickLoginWithEmail() { + await this.loginWithEmailButton.click(); + } + + async clickContinueButton() { + await this.continueButton.click(); + } + + async clickTermsLink() { + await this.termsOfServiceLink.click(); + } + + async clickPrivacyPolicyLink() { + await this.privacyPolicyLink.click(); + } + + async typeEmail(email: string) { + await this.emailField.fill(email); + } + + async typePassword(email: string) { + await this.passwordField.fill(email); + } + + async clickSignInButton() { + await this.signInButton.click(); + } + + async clickSignUpButton() { + await this.signUpButton.click(); + } + + async clickForgotPassword() { + await this.forgotPasswordButton.click(); + } + + async revealPassword() { + await this.revealPasswordButton.click(); + } + + async previewImage() { + await this.previewImageButton.click(); + } + + async clickUploadImage() { + await this.uploadImageButton.click(); + } + + async deleteImage() { + await this.deleteImageButton.click(); + } + + async typeWorkspaceName(workspaceName: string) { + await this.workspaceNameField.fill(workspaceName); + } + + async typeFirstName(firstName: string) { + await this.firstNameField.fill(firstName); + } + + async typeLastName(lastName: string) { + await this.lastNameField.fill(lastName); + } + + async clickSyncEverythingWithGoogleRadio() { + await this.syncEverythingWithGoogleRadio.click(); + } + + async clickSyncSubjectAndMetadataWithGoogleRadio() { + await this.syncSubjectAndMetadataWithGoogleRadio.click(); + } + + async clickSyncMetadataWithGoogleRadio() { + await this.syncMetadataWithGoogleRadio.click(); + } + + async clickSyncWithGoogleButton() { + await this.syncWithGoogleButton.click(); + } + + async noSyncWithGoogle() { + await this.noSyncButton.click(); + } + + async typeInviteLink1(email: string) { + await this.inviteLinkField1.fill(email); + } + + async typeInviteLink2(email: string) { + await this.inviteLinkField2.fill(email); + } + + async typeInviteLink3(email: string) { + await this.inviteLinkField3.fill(email); + } + + async clickCopyInviteLink() { + await this.copyInviteLink.click(); + } + + async clickFinishButton() { + await this.finishButton.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/mainPage.ts b/packages/twenty-e2e-testing/lib/pom/mainPage.ts new file mode 100644 index 000000000000..a28e133b575c --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/mainPage.ts @@ -0,0 +1,196 @@ +import { Locator, Page } from '@playwright/test'; + +export class MainPage { + // TODO: add missing elements (advanced filters, import/export popups) + private readonly tableViews: Locator; + private readonly addViewButton: Locator; + private readonly viewIconSelect: Locator; + private readonly viewNameInput: Locator; + private readonly viewTypeSelect: Locator; + private readonly createViewButton: Locator; + private readonly deleteViewButton: Locator; + private readonly filterButton: Locator; + private readonly searchFieldInput: Locator; + private readonly advancedFilterButton: Locator; + private readonly addFilterButton: Locator; + private readonly resetFilterButton: Locator; + private readonly saveFilterAsViewButton: Locator; + private readonly sortButton: Locator; + private readonly sortOrderButton: Locator; + private readonly optionsButton: Locator; + private readonly fieldsButton: Locator; + private readonly goBackButton: Locator; + private readonly hiddenFieldsButton: Locator; + private readonly editFieldsButton: Locator; + private readonly importButton: Locator; + private readonly exportButton: Locator; + private readonly deletedRecordsButton: Locator; + private readonly createNewRecordButton: Locator; + private readonly addToFavoritesButton: Locator; + private readonly deleteFromFavoritesButton: Locator; + private readonly exportBottomBarButton: Locator; + private readonly deleteRecordsButton: Locator; + + constructor(public readonly page: Page) { + this.tableViews = page.getByText('·'); + this.addViewButton = page + .getByTestId('tooltip') + .filter({ hasText: /^Add view$/ }); + this.viewIconSelect = page.getByLabel('Click to select icon ('); + this.viewNameInput; // can be selected using only actual value + this.viewTypeSelect = page.locator( + "//span[contains(., 'View type')]/../div", + ); + this.createViewButton = page.getByRole('button', { name: 'Create' }); + this.deleteViewButton = page.getByRole('button', { name: 'Delete' }); + this.filterButton = page.getByText('Filter'); + this.searchFieldInput = page.getByPlaceholder('Search fields'); + this.advancedFilterButton = page + .getByTestId('tooltip') + .filter({ hasText: /^Advanced filter$/ }); + this.addFilterButton = page.getByRole('button', { name: 'Add Filter' }); + this.resetFilterButton = page.getByTestId('cancel-button'); + this.saveFilterAsViewButton = page.getByRole('button', { + name: 'Save as new view', + }); + this.sortButton = page.getByText('Sort'); + this.sortOrderButton = page.locator('//li'); + this.optionsButton = page.getByText('Options'); + this.fieldsButton = page.getByText('Fields'); + this.goBackButton = page.getByTestId('dropdown-menu-header-end-icon'); + this.hiddenFieldsButton = page + .getByTestId('tooltip') + .filter({ hasText: /^Hidden Fields$/ }); + this.editFieldsButton = page + .getByTestId('tooltip') + .filter({ hasText: /^Edit Fields$/ }); + this.importButton = page + .getByTestId('tooltip') + .filter({ hasText: /^Import$/ }); + this.exportButton = page + .getByTestId('tooltip') + .filter({ hasText: /^Export$/ }); + this.deletedRecordsButton = page + .getByTestId('tooltip') + .filter({ hasText: /^Deleted */ }); + this.createNewRecordButton = page.getByTestId('add-button'); + this.addToFavoritesButton = page.getByText('Add to favorites'); + this.deleteFromFavoritesButton = page.getByText('Delete from favorites'); + this.exportBottomBarButton = page.getByText('Export'); + this.deleteRecordsButton = page.getByText('Delete'); + } + + async clickTableViews() { + await this.tableViews.click(); + } + + async clickAddViewButton() { + await this.addViewButton.click(); + } + + async typeViewName(name: string) { + await this.viewNameInput.clear(); + await this.viewNameInput.fill(name); + } + + // name can be either be 'Table' or 'Kanban' + async selectViewType(name: string) { + await this.viewTypeSelect.click(); + await this.page.getByTestId('tooltip').filter({ hasText: name }).click(); + } + + async createView() { + await this.createViewButton.click(); + } + + async deleteView() { + await this.deleteViewButton.click(); + } + + async clickFilterButton() { + await this.filterButton.click(); + } + + async searchFields(name: string) { + await this.searchFieldInput.clear(); + await this.searchFieldInput.fill(name); + } + + async clickAdvancedFilterButton() { + await this.advancedFilterButton.click(); + } + + async addFilter() { + await this.addFilterButton.click(); + } + + async resetFilter() { + await this.resetFilterButton.click(); + } + + async saveFilterAsView() { + await this.saveFilterAsViewButton.click(); + } + + async clickSortButton() { + await this.sortButton.click(); + } + + //can be Ascending or Descending + async setSortOrder(name: string) { + await this.sortOrderButton.click(); + await this.page.getByTestId('tooltip').filter({ hasText: name }).click(); + } + + async clickOptionsButton() { + await this.optionsButton.click(); + } + + async clickFieldsButton() { + await this.fieldsButton.click(); + } + + async clickBackButton() { + await this.goBackButton.click(); + } + + async clickHiddenFieldsButton() { + await this.hiddenFieldsButton.click(); + } + + async clickEditFieldsButton() { + await this.editFieldsButton.click(); + } + + async clickImportButton() { + await this.importButton.click(); + } + + async clickExportButton() { + await this.exportButton.click(); + } + + async clickDeletedRecordsButton() { + await this.deletedRecordsButton.click(); + } + + async clickCreateNewRecordButton() { + await this.createNewRecordButton.click(); + } + + async clickAddToFavoritesButton() { + await this.addToFavoritesButton.click(); + } + + async clickDeleteFromFavoritesButton() { + await this.deleteFromFavoritesButton.click(); + } + + async clickExportBottomBarButton() { + await this.exportBottomBarButton.click(); + } + + async clickDeleteRecordsButton() { + await this.deleteRecordsButton.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/recordDetails.ts b/packages/twenty-e2e-testing/lib/pom/recordDetails.ts new file mode 100644 index 000000000000..f22dd8a459b5 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/recordDetails.ts @@ -0,0 +1,150 @@ +import { Locator, Page } from '@playwright/test'; + +export class RecordDetails { + // TODO: add missing components in tasks, notes, files, emails, calendar tabs + private readonly closeRecordButton: Locator; + private readonly previousRecordButton: Locator; + private readonly nextRecordButton: Locator; + private readonly favoriteRecordButton: Locator; + private readonly addShowPageButton: Locator; + private readonly moreOptionsButton: Locator; + private readonly deleteButton: Locator; + private readonly uploadProfileImageButton: Locator; + private readonly timelineTab: Locator; + private readonly tasksTab: Locator; + private readonly notesTab: Locator; + private readonly filesTab: Locator; + private readonly emailsTab: Locator; + private readonly calendarTab: Locator; + private readonly detachRelationButton: Locator; + + constructor(public readonly page: Page) { + this.page = page; + } + + async clickCloseRecordButton() { + await this.closeRecordButton.click(); + } + + async clickPreviousRecordButton() { + await this.previousRecordButton.click(); + } + + async clickNextRecordButton() { + await this.nextRecordButton.click(); + } + + async clickFavoriteRecordButton() { + await this.favoriteRecordButton.click(); + } + + async createRelatedNote() { + await this.addShowPageButton.click(); + await this.page + .locator('//div[@data-testid="tooltip" and contains(., "Note")]') + .click(); + } + + async createRelatedTask() { + await this.addShowPageButton.click(); + await this.page + .locator('//div[@data-testid="tooltip" and contains(., "Task")]') + .click(); + } + + async clickMoreOptionsButton() { + await this.moreOptionsButton.click(); + } + + async clickDeleteRecordButton() { + await this.deleteButton.click(); + } + + async clickUploadProfileImageButton() { + await this.uploadProfileImageButton.click(); + } + + async goToTimelineTab() { + await this.timelineTab.click(); + } + + async goToTasksTab() { + await this.tasksTab.click(); + } + + async goToNotesTab() { + await this.notesTab.click(); + } + + async goToFilesTab() { + await this.filesTab.click(); + } + + async goToEmailsTab() { + await this.emailsTab.click(); + } + + async goToCalendarTab() { + await this.calendarTab.click(); + } + + async clickField(name: string) { + await this.page + .locator( + `//div[@data-testid='tooltip' and contains(., '${name}']/../../../div[last()]/div/div`, + ) + .click(); + } + + async clickFieldWithButton(name: string) { + await this.page + .locator( + `//div[@data-testid='tooltip' and contains(., '${name}']/../../../div[last()]/div/div`, + ) + .hover(); + await this.page + .locator( + `//div[@data-testid='tooltip' and contains(., '${name}']/../../../div[last()]/div/div[last()]/div/button`, + ) + .click(); + } + + async clickRelationEditButton(name: string) { + await this.page.getByRole('heading').filter({ hasText: name }).hover(); + await this.page + .locator(`//header[contains(., "${name}")]/div[last()]/div/button`) + .click(); + } + + async detachRelation(name: string) { + await this.page.locator(`//a[contains(., "${name}")]`).hover(); + await this.page + .locator(`, //a[contains(., "${name}")]/../div[last()]/div/div/button`) + .hover(); + await this.detachRelationButton.click(); + } + + async deleteRelationRecord(name: string) { + await this.page.locator(`//a[contains(., "${name}")]`).hover(); + await this.page + .locator(`, //a[contains(., "${name}")]/../div[last()]/div/div/button`) + .hover(); + await this.deleteButton.click(); + } + + async selectRelationRecord(name: string) { + await this.page + .locator(`//div[@data-testid="tooltip" and contains(., "${name}")]`) + .click(); + } + + async searchRelationRecord(name: string) { + await this.page.getByPlaceholder('Search').fill(name); + } + + async createNewRelationRecord() { + await this.page + .locator('//div[@data-testid="tooltip" and contains(., "Add New")]') + .click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/accountsSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/accountsSection.ts new file mode 100644 index 000000000000..703cdffa6ed6 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/accountsSection.ts @@ -0,0 +1,54 @@ +import { Locator, Page } from '@playwright/test'; + +export class AccountsSection { + private readonly addAccountButton: Locator; + private readonly deleteAccountButton: Locator; + private readonly addBlocklistField: Locator; + private readonly addBlocklistButton: Locator; + private readonly connectWithGoogleButton: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.addAccountButton = page.getByRole('button', { name: 'Add account' }); + this.deleteAccountButton = page + .getByTestId('tooltip') + .getByText('Remove account'); + this.addBlocklistField = page.getByPlaceholder( + 'eddy@gmail.com, @apple.com', + ); + this.addBlocklistButton = page.getByRole('button', { + name: 'Add to blocklist', + }); + this.connectWithGoogleButton = page.getByRole('button', { + name: 'Connect with Google', + }); + } + + async clickAddAccount() { + await this.addAccountButton.click(); + } + + async deleteAccount(email: string) { + await this.page + .locator(`//span[contains(., "${email}")]/../div/div/div/button`) + .click(); + await this.deleteAccountButton.click(); + } + + async addToBlockList(domain: string) { + await this.addBlocklistField.fill(domain); + await this.addBlocklistButton.click(); + } + + async removeFromBlocklist(domain: string) { + await this.page + .locator( + `//div[@data-testid='tooltip' and contains(., '${domain}')]/../../div[last()]/button`, + ) + .click(); + } + + async linkGoogleAccount() { + await this.connectWithGoogleButton.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/calendarSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/calendarSection.ts new file mode 100644 index 000000000000..98ccba0d06f8 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/calendarSection.ts @@ -0,0 +1,30 @@ +import { Locator, Page } from '@playwright/test'; + +export class CalendarSection { + private readonly eventVisibilityEverythingOption: Locator; + private readonly eventVisibilityMetadataOption: Locator; + private readonly contactAutoCreation: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.eventVisibilityEverythingOption = page.locator( + 'input[value="SHARE_EVERYTHING"]', + ); + this.eventVisibilityMetadataOption = page.locator( + 'input[value="METADATA"]', + ); + this.contactAutoCreation = page.getByRole('checkbox').nth(1); + } + + async changeVisibilityToEverything() { + await this.eventVisibilityEverythingOption.click(); + } + + async changeVisibilityToMetadata() { + await this.eventVisibilityMetadataOption.click(); + } + + async toggleAutoCreation() { + await this.contactAutoCreation.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/dataModelSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/dataModelSection.ts new file mode 100644 index 000000000000..0e36bd351d56 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/dataModelSection.ts @@ -0,0 +1,189 @@ +import { Locator, Page } from '@playwright/test'; + +export class DataModelSection { + private readonly searchObjectInput: Locator; + private readonly addObjectButton: Locator; + private readonly objectSingularNameInput: Locator; + private readonly objectPluralNameInput: Locator; + private readonly objectDescription: Locator; + private readonly synchronizeLabelAPIToggle: Locator; + private readonly objectAPISingularNameInput: Locator; + private readonly objectAPIPluralNameInput: Locator; + private readonly objectMoreOptionsButton: Locator; + private readonly editObjectButton: Locator; + private readonly deleteObjectButton: Locator; + private readonly activeSection: Locator; + private readonly inactiveSection: Locator; + private readonly searchFieldInput: Locator; + private readonly addFieldButton: Locator; + private readonly viewFieldDetailsMoreOptionsButton: Locator; + private readonly nameFieldInput: Locator; + private readonly descriptionFieldInput: Locator; + private readonly deactivateMoreOptionsButton: Locator; + private readonly activateMoreOptionsButton: Locator; + private readonly deactivateButton: Locator; // TODO: add attribute to make it one button + private readonly activateButton: Locator; + private readonly cancelButton: Locator; + private readonly saveButton: Locator; + + constructor(public readonly page: Page) { + this.searchObjectInput = page.getByPlaceholder('Search an object...'); + this.addObjectButton = page.getByRole('button', { name: 'Add object' }); + this.objectSingularNameInput = page.getByPlaceholder('Listing', { + exact: true, + }); + this.objectPluralNameInput = page.getByPlaceholder('Listings', { + exact: true, + }); + this.objectDescription = page.getByPlaceholder('Write a description'); + this.synchronizeLabelAPIToggle = page.getByRole('checkbox').nth(1); + this.objectAPISingularNameInput = page.getByPlaceholder('listing', { + exact: true, + }); + this.objectAPIPluralNameInput = page.getByPlaceholder('listings', { + exact: true, + }); + this.objectMoreOptionsButton = page.getByLabel('Object Options'); + this.editObjectButton = page.getByTestId('tooltip').getByText('Edit'); + this.deactivateMoreOptionsButton = page + .getByTestId('tooltip') + .getByText('Deactivate'); + this.activateMoreOptionsButton = page + .getByTestId('tooltip') + .getByText('Activate'); + this.deleteObjectButton = page.getByTestId('tooltip').getByText('Delete'); + this.activeSection = page.getByText('Active', { exact: true }); + this.inactiveSection = page.getByText('Inactive'); + this.searchFieldInput = page.getByPlaceholder('Search a field...'); + this.addFieldButton = page.getByRole('button', { name: 'Add field' }); + this.viewFieldDetailsMoreOptionsButton = page + .getByTestId('tooltip') + .getByText('View'); + this.nameFieldInput = page.getByPlaceholder('Employees'); + this.descriptionFieldInput = page.getByPlaceholder('Write a description'); + this.deactivateButton = page.getByRole('button', { name: 'Deactivate' }); + this.activateButton = page.getByRole('button', { name: 'Activate' }); + this.cancelButton = page.getByRole('button', { name: 'Cancel' }); + this.saveButton = page.getByRole('button', { name: 'Save' }); + } + + async searchObject(name: string) { + await this.searchObjectInput.fill(name); + } + + async clickAddObjectButton() { + await this.addObjectButton.click(); + } + + async typeObjectSingularName(name: string) { + await this.objectSingularNameInput.fill(name); + } + + async typeObjectPluralName(name: string) { + await this.objectPluralNameInput.fill(name); + } + + async typeObjectDescription(name: string) { + await this.objectDescription.fill(name); + } + + async toggleSynchronizeLabelAPI() { + await this.synchronizeLabelAPIToggle.click(); + } + + async typeObjectSingularAPIName(name: string) { + await this.objectAPISingularNameInput.fill(name); + } + + async typeObjectPluralAPIName(name: string) { + await this.objectAPIPluralNameInput.fill(name); + } + + async checkObjectDetails(name: string) { + await this.page.getByRole('link').filter({ hasText: name }).click(); + } + + async activateInactiveObject(name: string) { + await this.page + .locator(`//div[@title="${name}"]/../../div[last()]`) + .click(); + await this.activateButton.click(); + } + + // object can be deleted only if is custom and inactive + async deleteInactiveObject(name: string) { + await this.page + .locator(`//div[@title="${name}"]/../../div[last()]`) + .click(); + await this.deleteObjectButton.click(); + } + + async editObjectDetails() { + await this.objectMoreOptionsButton.click(); + await this.editObjectButton.click(); + } + + async deactivateObjectWithMoreOptions() { + await this.objectMoreOptionsButton.click(); + await this.deactivateButton.click(); + } + + async searchField(name: string) { + await this.searchFieldInput.fill(name); + } + + async checkFieldDetails(name: string) { + await this.page.locator(`//div[@title="${name}"]`).click(); + } + + async checkFieldDetailsWithButton(name: string) { + await this.page + .locator(`//div[@title="${name}"]/../../div[last()]`) + .click(); + await this.viewFieldDetailsMoreOptionsButton.click(); + } + + async deactivateFieldWithButton(name: string) { + await this.page + .locator(`//div[@title="${name}"]/../../div[last()]`) + .click(); + await this.deactivateMoreOptionsButton.click(); + } + + async activateFieldWithButton(name: string) { + await this.page + .locator(`//div[@title="${name}"]/../../div[last()]`) + .click(); + await this.activateMoreOptionsButton.click(); + } + + async clickAddFieldButton() { + await this.addFieldButton.click(); + } + + async typeFieldName(name: string) { + await this.nameFieldInput.clear(); + await this.nameFieldInput.fill(name); + } + + async typeFieldDescription(description: string) { + await this.descriptionFieldInput.clear(); + await this.descriptionFieldInput.fill(description); + } + + async clickInactiveSection() { + await this.inactiveSection.click(); + } + + async clickActiveSection() { + await this.activeSection.click(); + } + + async clickCancelButton() { + await this.cancelButton.click(); + } + + async clickSaveButton() { + await this.saveButton.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/developersSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/developersSection.ts new file mode 100644 index 000000000000..0f7fec96f697 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/developersSection.ts @@ -0,0 +1,123 @@ +import { Locator, Page } from '@playwright/test'; + +export class DevelopersSection { + private readonly readDocumentationButton: Locator; + private readonly createAPIKeyButton: Locator; + private readonly regenerateAPIKeyButton: Locator; + private readonly nameOfAPIKeyInput: Locator; + private readonly expirationDateAPIKeySelect: Locator; + private readonly createWebhookButton: Locator; + private readonly webhookURLInput: Locator; + private readonly webhookDescription: Locator; + private readonly webhookFilterObjectSelect: Locator; + private readonly webhookFilterActionSelect: Locator; + private readonly cancelButton: Locator; + private readonly saveButton: Locator; + private readonly deleteButton: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.readDocumentationButton = page.getByRole('link', { + name: 'Read documentation', + }); + this.createAPIKeyButton = page.getByRole('link', { + name: 'Create API Key', + }); + this.createWebhookButton = page.getByRole('link', { + name: 'Create Webhook', + }); + this.nameOfAPIKeyInput = page + .getByPlaceholder('E.g. backoffice integration') + .first(); + this.expirationDateAPIKeySelect = page + .locator('div') + .filter({ hasText: /^Never$/ }) + .nth(3); // good enough if expiration date will change only 1 time + this.regenerateAPIKeyButton = page.getByRole('button', { + name: 'Regenerate Key', + }); + this.webhookURLInput = page.getByPlaceholder('URL'); + this.webhookDescription = page.getByPlaceholder('Write a description'); + this.webhookFilterObjectSelect = page + .locator('div') + .filter({ hasText: /^All Objects$/ }) + .nth(3); // works only for first filter + this.webhookFilterActionSelect = page + .locator('div') + .filter({ hasText: /^All Actions$/ }) + .nth(3); // works only for first filter + this.cancelButton = page.getByRole('button', { name: 'Cancel' }); + this.saveButton = page.getByRole('button', { name: 'Save' }); + this.deleteButton = page.getByRole('button', { name: 'Delete' }); + } + + async openDocumentation() { + await this.readDocumentationButton.click(); + } + + async createAPIKey() { + await this.createAPIKeyButton.click(); + } + + async typeAPIKeyName(name: string) { + await this.nameOfAPIKeyInput.clear(); + await this.nameOfAPIKeyInput.fill(name); + } + + async selectAPIExpirationDate(date: string) { + await this.expirationDateAPIKeySelect.click(); + await this.page.getByText(date, { exact: true }).click(); + } + + async regenerateAPIKey() { + await this.regenerateAPIKeyButton.click(); + } + + async deleteAPIKey() { + await this.deleteButton.click(); + } + + async deleteWebhook() { + await this.deleteButton.click(); + } + + async createWebhook() { + await this.createWebhookButton.click(); + } + + async typeWebhookURL(url: string) { + await this.webhookURLInput.fill(url); + } + + async typeWebhookDescription(description: string) { + await this.webhookDescription.fill(description); + } + + async selectWebhookObject(object: string) { + // TODO: finish + } + + async selectWebhookAction(action: string) { + // TODO: finish + } + + async deleteWebhookFilter() { + // TODO: finish + } + + async clickCancelButton() { + await this.cancelButton.click(); + } + + async clickSaveButton() { + await this.saveButton.click(); + } + + async checkAPIKeyDetails(name: string) { + await this.page.locator(`//a/div[contains(.,'${name}')][first()]`).click(); + } + + async checkWebhookDetails(name: string) { + await this.page.locator(`//a/div[contains(.,'${name}')][first()]`).click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/emailsSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/emailsSection.ts new file mode 100644 index 000000000000..23a83f8db07e --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/emailsSection.ts @@ -0,0 +1,61 @@ +import { Locator, Page } from '@playwright/test'; + +export class EmailsSection { + private readonly visibilityEverythingRadio: Locator; + private readonly visibilitySubjectRadio: Locator; + private readonly visibilityMetadataRadio: Locator; + private readonly autoCreationReceivedRadio: Locator; + private readonly autoCreationSentRadio: Locator; + private readonly autoCreationNoneRadio: Locator; + private readonly excludeNonProfessionalToggle: Locator; + private readonly excludeGroupToggle: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.visibilityEverythingRadio = page.locator( + 'input[value="SHARE_EVERYTHING"]', + ); + this.visibilitySubjectRadio = page.locator('input[value="SUBJECT"]'); + this.visibilityMetadataRadio = page.locator('input[value="METADATA"]'); + this.autoCreationReceivedRadio = page.locator( + 'input[value="SENT_AND_RECEIVED"]', + ); + this.autoCreationSentRadio = page.locator('input[value="SENT"]'); + this.autoCreationNoneRadio = page.locator('input[value="NONE"]'); + // first checkbox is advanced settings toggle + this.excludeNonProfessionalToggle = page.getByRole('checkbox').nth(1); + this.excludeGroupToggle = page.getByRole('checkbox').nth(2); + } + + async changeVisibilityToEverything() { + await this.visibilityEverythingRadio.click(); + } + + async changeVisibilityToSubject() { + await this.visibilitySubjectRadio.click(); + } + + async changeVisibilityToMetadata() { + await this.visibilityMetadataRadio.click(); + } + + async changeAutoCreationToAll() { + await this.autoCreationReceivedRadio.click(); + } + + async changeAutoCreationToSent() { + await this.autoCreationSentRadio.click(); + } + + async changeAutoCreationToNone() { + await this.autoCreationNoneRadio.click(); + } + + async toggleExcludeNonProfessional() { + await this.excludeNonProfessionalToggle.click(); + } + + async toggleExcludeGroup() { + await this.excludeGroupToggle.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/experienceSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/experienceSection.ts new file mode 100644 index 000000000000..4ed6606c10c9 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/experienceSection.ts @@ -0,0 +1,55 @@ +import { Locator, Page } from '@playwright/test'; + +export class ExperienceSection { + private readonly lightThemeButton: Locator; + private readonly darkThemeButton: Locator; + private readonly timezoneDropdown: Locator; + private readonly dateFormatDropdown: Locator; + private readonly timeFormatDropdown: Locator; + private readonly searchInput: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.lightThemeButton = page.getByText('AaLight'); // it works + this.darkThemeButton = page.getByText('AaDark'); + this.timezoneDropdown = page.locator( + '//span[contains(., "Time zone")]/../div/div/div', + ); + this.dateFormatDropdown = page.locator( + '//span[contains(., "Date format")]/../div/div/div', + ); + this.timeFormatDropdown = page.locator( + '//span[contains(., "Time format")]/../div/div/div', + ); + this.searchInput = page.getByPlaceholder('Search'); + } + + async changeThemeToLight() { + await this.lightThemeButton.click(); + } + + async changeThemeToDark() { + await this.darkThemeButton.click(); + } + + async selectTimeZone(timezone: string) { + await this.timezoneDropdown.click(); + await this.page.getByText(timezone, { exact: true }).click(); + } + + async selectTimeZoneWithSearch(timezone: string) { + await this.timezoneDropdown.click(); + await this.searchInput.fill(timezone); + await this.page.getByText(timezone, { exact: true }).click(); + } + + async selectDateFormat(dateFormat: string) { + await this.dateFormatDropdown.click(); + await this.page.getByText(dateFormat, { exact: true }).click(); + } + + async selectTimeFormat(timeFormat: string) { + await this.timeFormatDropdown.click(); + await this.page.getByText(timeFormat, { exact: true }).click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/functionsSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/functionsSection.ts new file mode 100644 index 000000000000..f8f42418a17e --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/functionsSection.ts @@ -0,0 +1,159 @@ +import { Locator, Page } from '@playwright/test'; + +export class FunctionsSection { + private readonly newFunctionButton: Locator; + private readonly functionNameInput: Locator; + private readonly functionDescriptionInput: Locator; + private readonly editorTab: Locator; + private readonly codeEditorField: Locator; + private readonly resetButton: Locator; + private readonly publishButton: Locator; + private readonly testButton: Locator; + private readonly testTab: Locator; + private readonly runFunctionButton: Locator; + private readonly inputField: Locator; + private readonly settingsTab: Locator; + private readonly searchVariableInput: Locator; + private readonly addVariableButton: Locator; + private readonly nameVariableInput: Locator; + private readonly valueVariableInput: Locator; + private readonly cancelVariableButton: Locator; + private readonly saveVariableButton: Locator; + private readonly editVariableButton: Locator; + private readonly deleteVariableButton: Locator; + private readonly cancelButton: Locator; + private readonly saveButton: Locator; + private readonly deleteButton: Locator; + + constructor(public readonly page: Page) { + this.newFunctionButton = page.getByRole('button', { name: 'New Function' }); + this.functionNameInput = page.getByPlaceholder('Name'); + this.functionDescriptionInput = page.getByPlaceholder('Description'); + this.editorTab = page.getByTestId('tab-editor'); + this.codeEditorField = page.getByTestId('dummyInput'); // TODO: fix + this.resetButton = page.getByRole('button', { name: 'Reset' }); + this.publishButton = page.getByRole('button', { name: 'Publish' }); + this.testButton = page.getByRole('button', { name: 'Test' }); + this.testTab = page.getByTestId('tab-test'); + this.runFunctionButton = page.getByRole('button', { name: 'Run Function' }); + this.inputField = page.getByTestId('dummyInput'); // TODO: fix + this.settingsTab = page.getByTestId('tab-settings'); + this.searchVariableInput = page.getByPlaceholder('Search a variable'); + this.addVariableButton = page.getByRole('button', { name: 'Add Variable' }); + this.nameVariableInput = page.getByPlaceholder('Name').nth(1); + this.valueVariableInput = page.getByPlaceholder('Value'); + this.cancelVariableButton = page.locator('.css-uwqduk').first(); // TODO: fix + this.saveVariableButton = page.locator('.css-uwqduk').nth(1); // TODO: fix + this.editVariableButton = page.getByText('Edit', { exact: true }); + this.deleteVariableButton = page.getByText('Delete', { exact: true }); + this.cancelButton = page.getByRole('button', { name: 'Cancel' }); + this.saveButton = page.getByRole('button', { name: 'Save' }); + this.deleteButton = page.getByRole('button', { name: 'Delete function' }); + } + + async clickNewFunction() { + await this.newFunctionButton.click(); + } + + async typeFunctionName(name: string) { + await this.functionNameInput.fill(name); + } + + async typeFunctionDescription(description: string) { + await this.functionDescriptionInput.fill(description); + } + + async checkFunctionDetails(name: string) { + await this.page.getByRole('link', { name: `${name} nodejs18.x` }).click(); + } + + async clickEditorTab() { + await this.editorTab.click(); + } + + async clickResetButton() { + await this.resetButton.click(); + } + + async clickPublishButton() { + await this.publishButton.click(); + } + + async clickTestButton() { + await this.testButton.click(); + } + + async typeFunctionCode() { + // TODO: finish once utils are merged + } + + async clickTestTab() { + await this.testTab.click(); + } + + async runFunction() { + await this.runFunctionButton.click(); + } + + async typeFunctionInput() { + // TODO: finish once utils are merged + } + + async clickSettingsTab() { + await this.settingsTab.click(); + } + + async searchVariable(name: string) { + await this.searchVariableInput.fill(name); + } + + async addVariable() { + await this.addVariableButton.click(); + } + + async typeVariableName(name: string) { + await this.nameVariableInput.fill(name); + } + + async typeVariableValue(value: string) { + await this.valueVariableInput.fill(value); + } + + async editVariable(name: string) { + await this.page + .locator( + `//div[@data-testid='tooltip' and contains(., '${name}')]/../../div[last()]/div/div/button`, + ) + .click(); + await this.editVariableButton.click(); + } + + async deleteVariable(name: string) { + await this.page + .locator( + `//div[@data-testid='tooltip' and contains(., '${name}')]/../../div[last()]/div/div/button`, + ) + .click(); + await this.deleteVariableButton.click(); + } + + async cancelVariable() { + await this.cancelVariableButton.click(); + } + + async saveVariable() { + await this.saveVariableButton.click(); + } + + async clickCancelButton() { + await this.cancelButton.click(); + } + + async clickSaveButton() { + await this.saveButton.click(); + } + + async clickDeleteButton() { + await this.deleteButton.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/generalSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/generalSection.ts new file mode 100644 index 000000000000..d936a2e9e196 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/generalSection.ts @@ -0,0 +1,29 @@ +import { Locator, Page } from '@playwright/test'; + +export class GeneralSection { + private readonly workspaceNameField: Locator; + private readonly supportSwitch: Locator; + private readonly deleteWorkspaceButton: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.workspaceNameField = page.getByPlaceholder('Apple'); + this.supportSwitch = page.getByRole('checkbox').nth(1); + this.deleteWorkspaceButton = page.getByRole('button', { + name: 'Delete workspace', + }); + } + + async changeWorkspaceName(workspaceName: string) { + await this.workspaceNameField.clear(); + await this.workspaceNameField.fill(workspaceName); + } + + async changeSupportSwitchState() { + await this.supportSwitch.click(); + } + + async clickDeleteWorkSpaceButton() { + await this.deleteWorkspaceButton.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/membersSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/membersSection.ts new file mode 100644 index 000000000000..4f1086657028 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/membersSection.ts @@ -0,0 +1,48 @@ +import { Locator, Page } from '@playwright/test'; + +export class MembersSection { + private readonly inviteMembersField: Locator; + private readonly inviteMembersButton: Locator; + private readonly inviteLinkButton: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.inviteMembersField = page.getByPlaceholder( + 'tim@apple.com, jony.ive@apple', + ); + this.inviteMembersButton = page.getByRole('button', { name: 'Invite' }); + this.inviteLinkButton = page.getByRole('button', { name: 'Copy link' }); + } + + async copyInviteLink() { + await this.inviteLinkButton.click(); + } + + async sendInviteEmail(email: string) { + await this.inviteMembersField.click(); + await this.inviteMembersField.fill(email); + await this.inviteMembersButton.click(); + } + + async deleteMember(email: string) { + await this.page + .locator(`//div[contains(., '${email}')]/../../div[last()]/div/button`) + .click(); + } + + async deleteInviteEmail(email: string) { + await this.page + .locator( + `//div[contains(., '${email}')]/../../div[last()]/div/button[first()]`, + ) + .click(); + } + + async refreshInviteEmail(email: string) { + await this.page + .locator( + `//div[contains(., '${email}')]/../../div[last()]/div/button[last()]`, + ) + .click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/newFieldSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/newFieldSection.ts new file mode 100644 index 000000000000..50b37e03ac5b --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/newFieldSection.ts @@ -0,0 +1,250 @@ +import { Locator, Page } from '@playwright/test'; + +export class NewFieldSection { + private readonly searchTypeFieldInput: Locator; + private readonly currencyFieldLink: Locator; + private readonly currencyDefaultUnitSelect: Locator; + private readonly emailsFieldLink: Locator; + private readonly linksFieldLink: Locator; + private readonly phonesFieldLink: Locator; + private readonly addressFieldLink: Locator; + private readonly textFieldLink: Locator; + private readonly numberFieldLink: Locator; + private readonly decreaseDecimalsButton: Locator; + private readonly decimalsNumberInput: Locator; + private readonly increaseDecimalsButton: Locator; + private readonly booleanFieldLink: Locator; + private readonly defaultBooleanSelect: Locator; + private readonly dateTimeFieldLink: Locator; + private readonly dateFieldLink: Locator; + private readonly relativeDateToggle: Locator; + private readonly selectFieldLink: Locator; + private readonly multiSelectFieldLink: Locator; + private readonly setAsDefaultOptionButton: Locator; + private readonly removeOptionButton: Locator; + private readonly addOptionButton: Locator; + private readonly ratingFieldLink: Locator; + private readonly JSONFieldLink: Locator; + private readonly arrayFieldLink: Locator; + private readonly relationFieldLink: Locator; + private readonly relationTypeSelect: Locator; + private readonly objectDestinationSelect: Locator; + private readonly relationFieldNameInput: Locator; + private readonly fullNameFieldLink: Locator; + private readonly UUIDFieldLink: Locator; + private readonly nameFieldInput: Locator; + private readonly descriptionFieldInput: Locator; + + constructor(public readonly page: Page) { + this.searchTypeFieldInput = page.getByPlaceholder('Search a type'); + this.currencyFieldLink = page.getByRole('link', { name: 'Currency' }); + this.currencyDefaultUnitSelect = page.locator( + "//span[contains(., 'Default Unit')]/../div", + ); + this.emailsFieldLink = page.getByRole('link', { name: 'Emails' }).nth(1); + this.linksFieldLink = page.getByRole('link', { name: 'Links' }); + this.phonesFieldLink = page.getByRole('link', { name: 'Phones' }); + this.addressFieldLink = page.getByRole('link', { name: 'Address' }); + this.textFieldLink = page.getByRole('link', { name: 'Text' }); + this.numberFieldLink = page.getByRole('link', { name: 'Number' }); + this.decreaseDecimalsButton = page.locator( + "//div[contains(., 'Number of decimals')]/../div[last()]/div/div/button[2]", + ); + this.decimalsNumberInput = page.locator( + // would be better if first div was span tag + "//div[contains(., 'Number of decimals')]/../div[last()]/div/div/div/div/input[2]", + ); + this.increaseDecimalsButton = page.locator( + "//div[contains(., 'Number of decimals')]/../div[last()]/div/div/button[3]", + ); + this.booleanFieldLink = page.getByRole('link', { name: 'True/False' }); + this.defaultBooleanSelect = page.locator( + "//span[contains(., 'Default Value')]/../div", + ); + this.dateTimeFieldLink = page.getByRole('link', { name: 'Date and Time' }); + this.dateFieldLink = page.getByRole('link', { name: 'Date' }); + this.relativeDateToggle = page.getByRole('checkbox').nth(1); + this.selectFieldLink = page.getByRole('link', { name: 'Select' }); + this.multiSelectFieldLink = page.getByRole('link', { + name: 'Multi-select', + }); + this.setAsDefaultOptionButton = page + .getByTestId('tooltip') + .getByText('Set as default'); + this.removeOptionButton = page + .getByTestId('tooltip') + .getByText('Remove option'); + this.addOptionButton = page.getByRole('button', { name: 'Add option' }); + this.ratingFieldLink = page.getByRole('link', { name: 'Rating' }); + this.JSONFieldLink = page.getByRole('link', { name: 'JSON' }); + this.arrayFieldLink = page.getByRole('link', { name: 'Array' }); + this.relationFieldLink = page.getByRole('link', { name: 'Relation' }); + this.relationTypeSelect = page.locator( + "//span[contains(., 'Relation type')]/../div", + ); + this.objectDestinationSelect = page.locator( + "//span[contains(., 'Object destination')]/../div", + ); + this.relationIconSelect = page.getByLabel('Click to select icon (').nth(1); + this.relationFieldNameInput = page.getByPlaceholder('Field name'); + this.fullNameFieldLink = page.getByRole('link', { name: 'Full Name' }); + this.UUIDFieldLink = page.getByRole('link', { name: 'Unique ID' }); + this.nameFieldInput = page.getByPlaceholder('Employees'); + this.descriptionFieldInput = page.getByPlaceholder('Write a description'); + } + + async searchTypeField(name: string) { + await this.searchTypeFieldInput.fill(name); + } + + async clickCurrencyType() { + await this.currencyFieldLink.click(); + } + + async selectDefaultUnit(name: string) { + await this.currencyDefaultUnitSelect.click(); + await this.page.getByTestId('tooltip').filter({ hasText: name }).click(); + } + + async clickEmailsType() { + await this.emailsFieldLink.click(); + } + + async clickLinksType() { + await this.linksFieldLink.click(); + } + + async clickPhonesType() { + await this.phonesFieldLink.click(); + } + + async clickAddressType() { + await this.addressFieldLink.click(); + } + + async clickTextType() { + await this.textFieldLink.click(); + } + + async clickNumberType() { + await this.numberFieldLink.click(); + } + + async decreaseDecimals() { + await this.decreaseDecimalsButton.click(); + } + + async typeNumberOfDecimals(amount: number) { + await this.decimalsNumberInput.fill(String(amount)); + } + + async increaseDecimals() { + await this.increaseDecimalsButton.click(); + } + + async clickBooleanType() { + await this.booleanFieldLink.click(); + } + + // either True of False + async selectDefaultBooleanValue(value: string) { + await this.defaultBooleanSelect.click(); + await this.page.getByTestId('tooltip').filter({ hasText: value }).click(); + } + + async clickDateTimeType() { + await this.dateTimeFieldLink.click(); + } + + async clickDateType() { + await this.dateFieldLink.click(); + } + + async toggleRelativeDate() { + await this.relativeDateToggle.click(); + } + + async clickSelectType() { + await this.selectFieldLink.click(); + } + + async clickMultiSelectType() { + await this.multiSelectFieldLink.click(); + } + + async addSelectOption() { + await this.addOptionButton.click(); + } + + async setOptionAsDefault() { + // TODO: finish + await this.setAsDefaultOptionButton.click(); + } + + async deleteSelectOption() { + // TODO: finish + await this.removeOptionButton.click(); + } + + async changeOptionAPIName() { + // TODO: finish + } + + async changeOptionColor() { + // TODO: finish + } + + async changeOptionName() { + // TODO: finish + } + + async clickRatingType() { + await this.ratingFieldLink.click(); + } + + async clickJSONType() { + await this.JSONFieldLink.click(); + } + + async clickArrayType() { + await this.arrayFieldLink.click(); + } + + async clickRelationType() { + await this.relationFieldLink.click(); + } + + // either 'Has many' or 'Belongs to one' + async selectRelationType(name: string) { + await this.relationTypeSelect.click(); + await this.page.getByTestId('tooltip').filter({ hasText: name }).click(); + } + + async selectObjectDestination(name: string) { + await this.objectDestinationSelect.click(); + await this.page.getByTestId('tooltip').filter({ hasText: name }).click(); + } + + async typeRelationName(name: string) { + await this.relationFieldNameInput.clear(); + await this.relationFieldNameInput.fill(name); + } + + async clickFullNameType() { + await this.fullNameFieldLink.click(); + } + + async clickUUIDType() { + await this.UUIDFieldLink.click(); + } + + async typeFieldName(name: string) { + await this.nameFieldInput.clear(); + await this.nameFieldInput.fill(name); + } + + async typeFieldDescription(description: string) { + await this.descriptionFieldInput.clear(); + await this.descriptionFieldInput.fill(description); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/profileSection.ts b/packages/twenty-e2e-testing/lib/pom/settings/profileSection.ts new file mode 100644 index 000000000000..32feb9ac586c --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/profileSection.ts @@ -0,0 +1,44 @@ +import { Locator, Page } from '@playwright/test'; + +export class ProfileSection { + private readonly firstNameField: Locator; + private readonly lastNameField: Locator; + private readonly emailField: Locator; + private readonly changePasswordButton: Locator; + private readonly deleteAccountButton: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.firstNameField = page.getByPlaceholder('Tim'); + this.lastNameField = page.getByPlaceholder('Cook'); + this.emailField = page.getByRole('textbox').nth(2); + this.changePasswordButton = page.getByRole('button', { + name: 'Change Password', + }); + this.deleteAccountButton = page.getByRole('button', { + name: 'Delete account', + }); + } + + async changeFirstName(firstName: string) { + await this.firstNameField.clear(); + await this.firstNameField.fill(firstName); + } + + async changeLastName(lastName: string) { + await this.lastNameField.clear(); + await this.lastNameField.fill(lastName); + } + + async getEmail() { + await this.emailField.textContent(); + } + + async sendChangePasswordEmail() { + await this.changePasswordButton.click(); + } + + async deleteAccount() { + await this.deleteAccountButton.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settings/securitySection.ts b/packages/twenty-e2e-testing/lib/pom/settings/securitySection.ts new file mode 100644 index 000000000000..01b83b515578 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settings/securitySection.ts @@ -0,0 +1,13 @@ +import { Locator, Page } from '@playwright/test'; + +export class SecuritySection { + private readonly inviteByLinkToggle: Locator; + + constructor(public readonly page: Page) { + this.inviteByLinkToggle = page.locator('input[type="checkbox"]').nth(1); + } + + async toggleInviteByLink() { + await this.inviteByLinkToggle.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/pom/settingsPage.ts b/packages/twenty-e2e-testing/lib/pom/settingsPage.ts new file mode 100644 index 000000000000..c753bb8d0d1e --- /dev/null +++ b/packages/twenty-e2e-testing/lib/pom/settingsPage.ts @@ -0,0 +1,104 @@ +import { Locator, Page } from '@playwright/test'; + +export class SettingsPage { + private readonly exitSettingsLink: Locator; + private readonly profileLink: Locator; + private readonly experienceLink: Locator; + private readonly accountsLink: Locator; + private readonly emailsLink: Locator; + private readonly calendarsLink: Locator; + private readonly generalLink: Locator; + private readonly membersLink: Locator; + private readonly dataModelLink: Locator; + private readonly developersLink: Locator; + private readonly functionsLink: Locator; + private readonly securityLink: Locator; + private readonly integrationsLink: Locator; + private readonly releasesLink: Locator; + private readonly logoutLink: Locator; + private readonly advancedToggle: Locator; + + constructor(public readonly page: Page) { + this.page = page; + this.exitSettingsLink = page.getByRole('button', { name: 'Exit Settings' }); + this.profileLink = page.getByRole('link', { name: 'Profile' }); + this.experienceLink = page.getByRole('link', { name: 'Experience' }); + this.accountsLink = page.getByRole('link', { name: 'Accounts' }); + this.emailsLink = page.getByRole('link', { name: 'Emails', exact: true }); + this.calendarsLink = page.getByRole('link', { name: 'Calendars' }); + this.generalLink = page.getByRole('link', { name: 'General' }); + this.membersLink = page.getByRole('link', { name: 'Members' }); + this.dataModelLink = page.getByRole('link', { name: 'Data model' }); + this.developersLink = page.getByRole('link', { name: 'Developers' }); + this.functionsLink = page.getByRole('link', { name: 'Functions' }); + this.integrationsLink = page.getByRole('link', { name: 'Integrations' }); + this.securityLink = page.getByRole('link', { name: 'Security' }); + this.releasesLink = page.getByRole('link', { name: 'Releases' }); + this.logoutLink = page.getByText('Logout'); + this.advancedToggle = page.locator('input[type="checkbox"]').first(); + } + + async leaveSettingsPage() { + await this.exitSettingsLink.click(); + } + + async goToProfileSection() { + await this.profileLink.click(); + } + + async goToExperienceSection() { + await this.experienceLink.click(); + } + + async goToAccountsSection() { + await this.accountsLink.click(); + } + + async goToEmailsSection() { + await this.emailsLink.click(); + } + + async goToCalendarsSection() { + await this.calendarsLink.click(); + } + + async goToGeneralSection() { + await this.generalLink.click(); + } + + async goToMembersSection() { + await this.membersLink.click(); + } + + async goToDataModelSection() { + await this.dataModelLink.click(); + } + + async goToDevelopersSection() { + await this.developersLink.click(); + } + + async goToFunctionsSection() { + await this.functionsLink.click(); + } + + async goToSecuritySection() { + await this.securityLink.click(); + } + + async goToIntegrationsSection() { + await this.integrationsLink.click(); + } + + async goToReleasesIntegration() { + await this.releasesLink.click(); + } + + async logout() { + await this.logoutLink.click(); + } + + async toggleAdvancedSettings() { + await this.advancedToggle.click(); + } +} diff --git a/packages/twenty-e2e-testing/lib/utils/keyboardShortcuts.ts b/packages/twenty-e2e-testing/lib/utils/keyboardShortcuts.ts new file mode 100644 index 000000000000..470e63c1a5d8 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/utils/keyboardShortcuts.ts @@ -0,0 +1,94 @@ +import { Page } from '@playwright/test'; + +const MAC = process.platform === 'darwin'; + +async function keyDownCtrlOrMeta(page: Page) { + if (MAC) { + await page.keyboard.down('Meta'); + } else { + await page.keyboard.down('Control'); + } +} + +async function keyUpCtrlOrMeta(page: Page) { + if (MAC) { + await page.keyboard.up('Meta'); + } else { + await page.keyboard.up('Control'); + } +} + +export async function withCtrlOrMeta(page: Page, key: () => Promise) { + await keyDownCtrlOrMeta(page); + await key(); + await keyUpCtrlOrMeta(page); +} + +export async function selectAllByKeyboard(page: Page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press('a', { delay: 50 }); + await keyUpCtrlOrMeta(page); +} + +export async function copyByKeyboard(page: Page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press('c', { delay: 50 }); + await keyUpCtrlOrMeta(page); +} + +export async function cutByKeyboard(page: Page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press('x', { delay: 50 }); + await keyUpCtrlOrMeta(page); +} + +export async function pasteByKeyboard(page: Page) { + await keyDownCtrlOrMeta(page); + await page.keyboard.press('v', { delay: 50 }); + await keyUpCtrlOrMeta(page); +} + +export async function companiesShortcut(page: Page) { + await page.keyboard.press('g', { delay: 50 }); + await page.keyboard.press('c'); +} + +export async function notesShortcut(page: Page) { + await page.keyboard.press('g', { delay: 50 }); + await page.keyboard.press('n'); +} + +export async function opportunitiesShortcut(page: Page) { + await page.keyboard.press('g', { delay: 50 }); + await page.keyboard.press('o'); +} + +export async function peopleShortcut(page: Page) { + await page.keyboard.press('g', { delay: 50 }); + await page.keyboard.press('p'); +} + +export async function rocketsShortcut(page: Page) { + await page.keyboard.press('g', { delay: 50 }); + await page.keyboard.press('r'); +} + +export async function tasksShortcut(page: Page) { + await page.keyboard.press('g', { delay: 50 }); + await page.keyboard.press('t'); +} + +export async function workflowsShortcut(page: Page) { + await page.keyboard.press('g', { delay: 50 }); + await page.keyboard.press('w'); +} + +export async function settingsShortcut(page: Page) { + await page.keyboard.press('g', { delay: 50 }); + await page.keyboard.press('s'); +} + +export async function customShortcut(page: Page, shortcut: string) { + await page.keyboard.press('g', { delay: 50 }); + await page.keyboard.press(shortcut); +} diff --git a/packages/twenty-e2e-testing/lib/utils/pasteCodeToCodeEditor.ts b/packages/twenty-e2e-testing/lib/utils/pasteCodeToCodeEditor.ts new file mode 100644 index 000000000000..f67defef9047 --- /dev/null +++ b/packages/twenty-e2e-testing/lib/utils/pasteCodeToCodeEditor.ts @@ -0,0 +1,14 @@ +import { Locator, Page } from '@playwright/test'; +import { selectAllByKeyboard } from './keyboardShortcuts'; + +// https://github.com/microsoft/playwright/issues/14126 +// code must have \n at the end of lines otherwise everything will be in one line +export const pasteCodeToCodeEditor = async ( + page: Page, + locator: Locator, + code: string, +) => { + await locator.click(); + await selectAllByKeyboard(page); + await page.keyboard.type(code); +}; diff --git a/packages/twenty-e2e-testing/lib/utils/uploadFile.ts b/packages/twenty-e2e-testing/lib/utils/uploadFile.ts new file mode 100644 index 000000000000..81898bc2ba7d --- /dev/null +++ b/packages/twenty-e2e-testing/lib/utils/uploadFile.ts @@ -0,0 +1,15 @@ +import { Page } from '@playwright/test'; +import path from 'path'; + +export const fileUploader = async ( + page: Page, + trigger: () => Promise, + filename: string, +) => { + const fileChooserPromise = page.waitForEvent('filechooser'); + await trigger(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles( + path.join(__dirname, '..', 'test_files', filename), + ); +}; diff --git a/packages/twenty-e2e-testing/tests/companies.spec.ts b/packages/twenty-e2e-testing/tests/companies.spec.ts index b8f78c7ecad7..1aa53d61322e 100644 --- a/packages/twenty-e2e-testing/tests/companies.spec.ts +++ b/packages/twenty-e2e-testing/tests/companies.spec.ts @@ -1,7 +1,4 @@ import { test, expect } from '../lib/fixtures/screenshot'; -import { config } from 'dotenv'; -import path = require('path'); -config({ path: path.resolve(__dirname, '..', '.env') }); test.describe('Basic check', () => { test('Checking if table in Companies is visible', async ({ page }) => { diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json index 408930ac02ec..02e7b81936ef 100644 --- a/packages/twenty-emails/package.json +++ b/packages/twenty-emails/package.json @@ -1,6 +1,6 @@ { "name": "twenty-emails", - "version": "0.32.0-canary", + "version": "0.32.0", "description": "", "author": "", "private": true, diff --git a/packages/twenty-front/.env.example b/packages/twenty-front/.env.example index 3fccb201c4b9..345d0fb92ad7 100644 --- a/packages/twenty-front/.env.example +++ b/packages/twenty-front/.env.example @@ -2,6 +2,7 @@ REACT_APP_SERVER_BASE_URL=http://localhost:3000 GENERATE_SOURCEMAP=false # ———————— Optional ———————— +# REACT_APP_PORT=3001 # CHROMATIC_PROJECT_TOKEN= # VITE_DISABLE_TYPESCRIPT_CHECKER=true # VITE_DISABLE_ESLINT_CHECKER=true \ No newline at end of file diff --git a/packages/twenty-front/.eslintrc.cjs b/packages/twenty-front/.eslintrc.cjs index df4daf7633a4..4d14adac114f 100644 --- a/packages/twenty-front/.eslintrc.cjs +++ b/packages/twenty-front/.eslintrc.cjs @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { extends: ['../../.eslintrc.cjs', '../../.eslintrc.react.cjs'], ignorePatterns: [ @@ -21,7 +23,16 @@ module.exports = { parserOptions: { project: ['packages/twenty-front/tsconfig.{json,*.json}'], }, - rules: {}, + plugins: ['project-structure'], + settings: { + 'project-structure/folder-structure-config-path':path.resolve( + __dirname, + 'folderStructure.json' + ) + }, + rules: { + 'project-structure/folder-structure': 'error', + }, }, ], }; diff --git a/packages/twenty-front/.gitignore b/packages/twenty-front/.gitignore index a9e8e5b674c2..49985254bed4 100644 --- a/packages/twenty-front/.gitignore +++ b/packages/twenty-front/.gitignore @@ -41,4 +41,7 @@ dist-ssr *.sw? .vite/ -.nyc_output/ \ No newline at end of file +.nyc_output/ + +# eslint-plugin-project-structure +projectStructure.cache.json diff --git a/packages/twenty-front/.storybook/main.ts b/packages/twenty-front/.storybook/main.ts index 8b65348c4b69..6d34593abb2c 100644 --- a/packages/twenty-front/.storybook/main.ts +++ b/packages/twenty-front/.storybook/main.ts @@ -50,8 +50,13 @@ const config: StorybookConfig = { const { mergeConfig } = await import('vite'); return mergeConfig(config, { - // Add dependencies to pre-optimization + resolve: { + alias: { + 'react-dom/client': 'react-dom/profiling', + }, + }, }); }, + logLevel: 'error', }; export default config; diff --git a/packages/twenty-front/.storybook/preview.tsx b/packages/twenty-front/.storybook/preview.tsx index 1d67634e2a54..d35b87e856ca 100644 --- a/packages/twenty-front/.storybook/preview.tsx +++ b/packages/twenty-front/.storybook/preview.tsx @@ -29,6 +29,7 @@ initialize({ with payload ${JSON.stringify(requestBody)}\n This request should be mocked with MSW`); }, + quiet: true, }); const preview: Preview = { diff --git a/packages/twenty-front/folderStructure.json b/packages/twenty-front/folderStructure.json new file mode 100644 index 000000000000..2509807f518f --- /dev/null +++ b/packages/twenty-front/folderStructure.json @@ -0,0 +1,65 @@ +{ + "$schema": "../../node_modules/eslint-plugin-project-structure/folderStructure.schema.json", + "projectRoot": "packages/twenty-front", + "structureRoot": "src", + "regexParameters": { + "camelCase": "^[a-z]+([A-Za-z0-9]+)+", + "kebab-case": "[a-z][a-z0-9]*(?:-[a-z0-9]+)*" + }, + "structure": [ + { "name": "*" }, + { "name": "*", "children": [] }, + { "name": "modules", "ruleId": "modulesFolderRule" } + ], + "rules": { + "modulesFolderRule": { + "children": [ + { "ruleId": "moduleFolderRule" }, + { "name": "types", "children": [] } + ] + }, + + "moduleFolderRule": { + "name": "{kebab-case}", + "folderRecursionLimit": 6, + "children": [ + { "ruleId": "moduleFolderRule" }, + { "name": "hooks", "ruleId": "hooksLeafFolderRule" }, + { "name": "utils", "ruleId": "utilsLeafFolderRule" }, + { "name": "states", "children": [] }, + { "name": "types", "children": [] }, + { "name": "graphql", "children": [] }, + { "name": "components", "children": [] }, + { "name": "effect-components", "children": [] }, + { "name": "constants", "children": [] }, + { "name": "validation-schemas", "children": [] }, + { "name": "contexts", "children": [] }, + { "name": "scopes", "children": [] }, + { "name": "services", "children": [] }, + { "name": "errors", "children": [] } + ] + }, + + "hooksLeafFolderRule": { + "folderRecursionLimit": 2, + "children": [ + { "name": "use{PascalCase}.(ts|tsx)" }, + { + "name": "__tests__", + "children": [{ "name": "use{PascalCase}.test.(ts|tsx)" }] + }, + { "name": "internal", "ruleId": "hooksLeafFolderRule" } + ] + }, + + "utilsLeafFolderRule": { + "children": [ + { "name": "{camelCase}.ts" }, + { + "name": "__tests__", + "children": [{ "name": "{camelCase}.test.ts" }] + } + ] + } + } +} diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index 8ed7f398db4e..ecf046e155e8 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -25,9 +25,9 @@ const jestConfig: JestConfigWithTsJest = { extensionsToTreatAsEsm: ['.ts', '.tsx'], coverageThreshold: { global: { - statements: 60, + statements: 58, lines: 55, - functions: 50, + functions: 47, }, }, collectCoverageFrom: ['/src/**/*.ts'], diff --git a/packages/twenty-front/nyc.config.cjs b/packages/twenty-front/nyc.config.cjs index 8ae501c6910f..b4c04651ecb6 100644 --- a/packages/twenty-front/nyc.config.cjs +++ b/packages/twenty-front/nyc.config.cjs @@ -10,7 +10,7 @@ const modulesCoverage = { branches: 25, statements: 49, lines: 50, - functions: 40, + functions: 38, include: ['src/modules/**/*'], exclude: ['src/**/*.ts'], }; diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 80eb86a7cf2a..d1a9d196a627 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -1,6 +1,6 @@ { "name": "twenty-front", - "version": "0.32.0-canary", + "version": "0.32.0", "private": true, "type": "module", "scripts": { @@ -33,6 +33,12 @@ "@nivo/calendar": "^0.87.0", "@nivo/core": "^0.87.0", "@nivo/line": "^0.87.0", + "@tiptap/extension-document": "^2.9.0", + "@tiptap/extension-paragraph": "^2.9.0", + "@tiptap/extension-placeholder": "^2.9.0", + "@tiptap/extension-text": "^2.9.0", + "@tiptap/extension-text-style": "^2.8.0", + "@tiptap/react": "^2.8.0", "@xyflow/react": "^12.0.4", "transliteration": "^2.3.5" } diff --git a/packages/twenty-front/project.json b/packages/twenty-front/project.json index 3ed94b22f256..ad72457839c0 100644 --- a/packages/twenty-front/project.json +++ b/packages/twenty-front/project.json @@ -52,7 +52,9 @@ "reportUnusedDisableDirectives": "error" }, "configurations": { - "ci": { "eslintConfig": "{projectRoot}/.eslintrc-ci.cjs" }, + "ci": { + "eslintConfig": "{projectRoot}/.eslintrc-ci.cjs" + }, "fix": {} } }, @@ -68,6 +70,12 @@ "storybook:build": { "options": { "env": { "NODE_OPTIONS": "--max_old_space_size=6500" } + }, + "configurations": { + "docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } }, + "modules": { "env": { "STORYBOOK_SCOPE": "modules" } }, + "pages": { "env": { "STORYBOOK_SCOPE": "pages" } }, + "performance": { "env": { "STORYBOOK_SCOPE": "performance" } } } }, "storybook:serve:dev": { @@ -80,7 +88,13 @@ } }, "storybook:serve:static": { - "options": { "port": 6006 } + "options": { "port": 6006 }, + "configurations": { + "docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } }, + "modules": { "env": { "STORYBOOK_SCOPE": "modules" } }, + "pages": { "env": { "STORYBOOK_SCOPE": "pages" } }, + "performance": { "env": { "STORYBOOK_SCOPE": "performance" } } + } }, "storybook:coverage": { "configurations": { @@ -102,9 +116,6 @@ }, "storybook:serve-and-test:static": { "options": { - "commands": [ - "npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'" - ], "port": 6006 }, "configurations": { @@ -114,15 +125,6 @@ "performance": { "scope": "performance" } } }, - "storybook:serve-and-test:static:performance": {}, - "storybook:test:no-coverage": { - "configurations": { - "docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } }, - "modules": { "env": { "STORYBOOK_SCOPE": "modules" } }, - "pages": { "env": { "STORYBOOK_SCOPE": "pages" } }, - "performance": { "env": { "STORYBOOK_SCOPE": "performance" } } - } - }, "graphql:generate": { "executor": "nx:run-commands", "defaultConfiguration": "data", diff --git a/packages/twenty-front/public/logos/20-high-resolution-logo-black-transparent.png b/packages/twenty-front/public/logos/20-high-resolution-logo-black-transparent.png new file mode 100644 index 000000000000..236da7815022 Binary files /dev/null and b/packages/twenty-front/public/logos/20-high-resolution-logo-black-transparent.png differ diff --git a/packages/twenty-front/public/logos/20-high-resolution-logo-black.png b/packages/twenty-front/public/logos/20-high-resolution-logo-black.png new file mode 100644 index 000000000000..db41f79831e5 Binary files /dev/null and b/packages/twenty-front/public/logos/20-high-resolution-logo-black.png differ diff --git a/packages/twenty-front/public/logos/20-high-resolution-logo-white-transparent.png b/packages/twenty-front/public/logos/20-high-resolution-logo-white-transparent.png new file mode 100644 index 000000000000..a7437a0f356f Binary files /dev/null and b/packages/twenty-front/public/logos/20-high-resolution-logo-white-transparent.png differ diff --git a/packages/twenty-front/public/logos/20-high-resolution-logo.png b/packages/twenty-front/public/logos/20-high-resolution-logo.png new file mode 100644 index 000000000000..0dbe8de6cba1 Binary files /dev/null and b/packages/twenty-front/public/logos/20-high-resolution-logo.png differ diff --git a/packages/twenty-front/src/__stories__/AppRouter.stories.tsx b/packages/twenty-front/src/__stories__/AppRouter.stories.tsx index 9d2fe91a6523..f322919baeea 100644 --- a/packages/twenty-front/src/__stories__/AppRouter.stories.tsx +++ b/packages/twenty-front/src/__stories__/AppRouter.stories.tsx @@ -1,18 +1,18 @@ import { getOperationName } from '@apollo/client/utilities'; import { jest } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; +import { HttpResponse, graphql } from 'msw'; import { HelmetProvider } from 'react-helmet-async'; import { RecoilRoot } from 'recoil'; -import { IconsProvider } from 'twenty-ui'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import indexAppPath from '@/navigation/utils/indexAppPath'; -import { AppPath } from '@/types/AppPath'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { AppRouter } from '@/app/components/AppRouter'; +import { AppPath } from '@/types/AppPath'; +import { IconsProvider } from 'twenty-ui'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { mockedUserData } from '~/testing/mock-data/users'; diff --git a/packages/twenty-front/src/generated-metadata/gql.ts b/packages/twenty-front/src/generated-metadata/gql.ts index 415482505650..54153628a72b 100644 --- a/packages/twenty-front/src/generated-metadata/gql.ts +++ b/packages/twenty-front/src/generated-metadata/gql.ts @@ -32,16 +32,16 @@ const documents = { "\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.DeleteOneObjectMetadataItemDocument, "\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument, "\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument, - "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument, - "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc, + "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument, + "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n latestVersionInputSchema {\n name\n type\n }\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc, "\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument, - "\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument, + "\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument, "\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument, "\n \n mutation PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.PublishOneServerlessFunctionDocument, "\n \n mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {\n updateOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.UpdateOneServerlessFunctionDocument, "\n query FindManyAvailablePackages {\n getAvailablePackages\n }\n": types.FindManyAvailablePackagesDocument, - "\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n": types.GetManyServerlessFunctionsDocument, - "\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n": types.GetOneServerlessFunctionDocument, + "\n \n query GetManyServerlessFunctions {\n findManyServerlessFunctions {\n ...ServerlessFunctionFields\n }\n }\n": types.GetManyServerlessFunctionsDocument, + "\n \n query GetOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n findOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.GetOneServerlessFunctionDocument, "\n query FindOneServerlessFunctionSourceCode(\n $input: GetServerlessFunctionSourceCodeInput!\n ) {\n getServerlessFunctionSourceCode(input: $input)\n }\n": types.FindOneServerlessFunctionSourceCodeDocument, }; @@ -138,11 +138,11 @@ export function graphql(source: "\n mutation DeleteOneRelationMetadataItem($idT /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"]; +export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n"]; +export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n latestVersionInputSchema {\n name\n type\n }\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n latestVersionInputSchema {\n name\n type\n }\n publishedVersions\n createdAt\n updatedAt\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -150,7 +150,7 @@ export function graphql(source: "\n \n mutation CreateOneServerlessFunctionIte /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"]; +export function graphql(source: "\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -170,11 +170,11 @@ export function graphql(source: "\n query FindManyAvailablePackages {\n getA /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n"): (typeof documents)["\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n"]; +export function graphql(source: "\n \n query GetManyServerlessFunctions {\n findManyServerlessFunctions {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n query GetManyServerlessFunctions {\n findManyServerlessFunctions {\n ...ServerlessFunctionFields\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n"]; +export function graphql(source: "\n \n query GetOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n findOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n query GetOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n findOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 97705b937cba..04eb88d31957 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -28,13 +28,6 @@ export type Scalars = { Upload: { input: any; output: any; } }; -export type AisqlQueryResult = { - __typename?: 'AISQLQueryResult'; - queryFailedErrorMessage?: Maybe; - sqlQuery: Scalars['String']['output']; - sqlQueryResult?: Maybe; -}; - export type ActivateWorkspaceInput = { displayName?: InputMaybe; }; @@ -78,6 +71,7 @@ export type AuthProviders = { magicLink: Scalars['Boolean']['output']; microsoft: Scalars['Boolean']['output']; password: Scalars['Boolean']['output']; + sso: Scalars['Boolean']['output']; }; export type AuthToken = { @@ -155,6 +149,7 @@ export enum CaptchaDriverType { export type ClientConfig = { __typename?: 'ClientConfig'; + analyticsEnabled: Scalars['Boolean']['output']; api: ApiConfig; authProviders: AuthProviders; billing: Billing; @@ -167,6 +162,11 @@ export type ClientConfig = { support: Support; }; +export type ComputeStepOutputSchemaInput = { + /** Step JSON format */ + step: Scalars['JSON']['input']; +}; + export type CreateAppTokenInput = { expiresAt: Scalars['DateTime']['input']; }; @@ -193,6 +193,7 @@ export type CreateObjectInput = { description?: InputMaybe; icon?: InputMaybe; imageIdentifierFieldMetadataId?: InputMaybe; + isLabelSyncedWithName?: InputMaybe; isRemote?: InputMaybe; labelIdentifierFieldMetadataId?: InputMaybe; labelPlural: Scalars['String']['input']; @@ -201,6 +202,7 @@ export type CreateObjectInput = { nameSingular: Scalars['String']['input']; primaryKeyColumnType?: InputMaybe; primaryKeyFieldMetadataSettings?: InputMaybe; + shortcut?: InputMaybe; }; export type CreateOneAppTokenInput = { @@ -277,9 +279,13 @@ export type DeleteOneRelationInput = { id: Scalars['UUID']['input']; }; -export type DeleteServerlessFunctionInput = { - /** The id of the function. */ - id: Scalars['ID']['input']; +export type DeleteSsoInput = { + identityProviderId: Scalars['String']['input']; +}; + +export type DeleteSsoOutput = { + __typename?: 'DeleteSsoOutput'; + identityProviderId: Scalars['String']['output']; }; /** Schema update on a table */ @@ -290,6 +296,20 @@ export enum DistantTableUpdate { TableDeleted = 'TABLE_DELETED' } +export type EditSsoInput = { + id: Scalars['String']['input']; + status: SsoIdentityProviderStatus; +}; + +export type EditSsoOutput = { + __typename?: 'EditSsoOutput'; + id: Scalars['String']['output']; + issuer: Scalars['String']['output']; + name: Scalars['String']['output']; + status: SsoIdentityProviderStatus; + type: IdpType; +}; + export type EmailPasswordResetLink = { __typename?: 'EmailPasswordResetLink'; /** Boolean that confirms query was dispatched */ @@ -379,6 +399,20 @@ export enum FileFolder { WorkspaceLogo = 'WorkspaceLogo' } +export type FindAvailableSsoidpInput = { + email: Scalars['String']['input']; +}; + +export type FindAvailableSsoidpOutput = { + __typename?: 'FindAvailableSSOIDPOutput'; + id: Scalars['String']['output']; + issuer: Scalars['String']['output']; + name: Scalars['String']['output']; + status: SsoIdentityProviderStatus; + type: IdpType; + workspace: WorkspaceNameAndId; +}; + export type FindManyRemoteTablesInput = { /** The id of the remote server. */ id: Scalars['ID']['input']; @@ -392,6 +426,39 @@ export type FullName = { lastName: Scalars['String']['output']; }; +export type FunctionParameter = { + __typename?: 'FunctionParameter'; + name: Scalars['String']['output']; + type: Scalars['String']['output']; +}; + +export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth; + +export type GenerateJwtOutputWithAuthTokens = { + __typename?: 'GenerateJWTOutputWithAuthTokens'; + authTokens: AuthTokens; + reason: Scalars['String']['output']; + success: Scalars['Boolean']['output']; +}; + +export type GenerateJwtOutputWithSsoauth = { + __typename?: 'GenerateJWTOutputWithSSOAUTH'; + availableSSOIDPs: Array; + reason: Scalars['String']['output']; + success: Scalars['Boolean']['output']; +}; + +export type GetAuthorizationUrlInput = { + identityProviderId: Scalars['String']['input']; +}; + +export type GetAuthorizationUrlOutput = { + __typename?: 'GetAuthorizationUrlOutput'; + authorizationURL: Scalars['String']['output']; + id: Scalars['String']['output']; + type: Scalars['String']['output']; +}; + export type GetServerlessFunctionSourceCodeInput = { /** The id of the function. */ id: Scalars['ID']['input']; @@ -399,6 +466,11 @@ export type GetServerlessFunctionSourceCodeInput = { version?: Scalars['String']['input']; }; +export enum IdpType { + Oidc = 'OIDC', + Saml = 'SAML' +} + export type IndexConnection = { __typename?: 'IndexConnection'; /** Array of edges. */ @@ -468,12 +540,15 @@ export type Mutation = { authorizeApp: AuthorizeApp; challenge: LoginToken; checkoutSession: SessionEntity; + computeStepOutputSchema: Scalars['JSON']['output']; + createOIDCIdentityProvider: SetupSsoOutput; createOneAppToken: AppToken; createOneField: Field; createOneObject: Object; createOneRelation: Relation; createOneRemoteServer: RemoteServer; createOneServerlessFunction: ServerlessFunction; + createSAMLIdentityProvider: SetupSsoOutput; deactivateWorkflowVersion: Scalars['Boolean']['output']; deleteCurrentWorkspace: Workspace; deleteOneField: Field; @@ -481,16 +556,20 @@ export type Mutation = { deleteOneRelation: Relation; deleteOneRemoteServer: RemoteServer; deleteOneServerlessFunction: ServerlessFunction; + deleteSSOIdentityProvider: DeleteSsoOutput; deleteUser: User; deleteWorkspaceInvitation: Scalars['String']['output']; disablePostgresProxy: PostgresCredentials; + editSSOIdentityProvider: EditSsoOutput; emailPasswordResetLink: EmailPasswordResetLink; enablePostgresProxy: PostgresCredentials; exchangeAuthorizationCode: ExchangeAuthCode; executeOneServerlessFunction: ServerlessFunctionExecutionResult; + findAvailableSSOIdentityProviders: Array; generateApiKeyToken: ApiKeyToken; - generateJWT: AuthTokens; + generateJWT: GenerateJwt; generateTransientToken: TransientToken; + getAuthorizationUrl: GetAuthorizationUrlOutput; impersonate: Verify; publishServerlessFunction: ServerlessFunction; renewToken: AuthTokens; @@ -558,6 +637,16 @@ export type MutationCheckoutSessionArgs = { }; +export type MutationComputeStepOutputSchemaArgs = { + input: ComputeStepOutputSchemaInput; +}; + + +export type MutationCreateOidcIdentityProviderArgs = { + input: SetupOidcSsoInput; +}; + + export type MutationCreateOneAppTokenArgs = { input: CreateOneAppTokenInput; }; @@ -588,6 +677,11 @@ export type MutationCreateOneServerlessFunctionArgs = { }; +export type MutationCreateSamlIdentityProviderArgs = { + input: SetupSamlSsoInput; +}; + + export type MutationDeactivateWorkflowVersionArgs = { workflowVersionId: Scalars['String']['input']; }; @@ -614,7 +708,12 @@ export type MutationDeleteOneRemoteServerArgs = { export type MutationDeleteOneServerlessFunctionArgs = { - input: DeleteServerlessFunctionInput; + input: ServerlessFunctionIdInput; +}; + + +export type MutationDeleteSsoIdentityProviderArgs = { + input: DeleteSsoInput; }; @@ -623,6 +722,11 @@ export type MutationDeleteWorkspaceInvitationArgs = { }; +export type MutationEditSsoIdentityProviderArgs = { + input: EditSsoInput; +}; + + export type MutationEmailPasswordResetLinkArgs = { email: Scalars['String']['input']; }; @@ -640,6 +744,11 @@ export type MutationExecuteOneServerlessFunctionArgs = { }; +export type MutationFindAvailableSsoIdentityProvidersArgs = { + input: FindAvailableSsoidpInput; +}; + + export type MutationGenerateApiKeyTokenArgs = { apiKeyId: Scalars['String']['input']; expiresAt: Scalars['String']['input']; @@ -651,6 +760,11 @@ export type MutationGenerateJwtArgs = { }; +export type MutationGetAuthorizationUrlArgs = { + input: GetAuthorizationUrlInput; +}; + + export type MutationImpersonateArgs = { userId: Scalars['String']['input']; }; @@ -859,10 +973,11 @@ export type Query = { fields: FieldConnection; findDistantTablesWithStatus: Array; findManyRemoteServersByType: Array; + findManyServerlessFunctions: Array; findOneRemoteServerById: RemoteServer; + findOneServerlessFunction: ServerlessFunction; findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; - getAISQLQuery: AisqlQueryResult; getAvailablePackages: Scalars['JSON']['output']; getPostgresCredentials?: Maybe; getProductPrices: ProductPricesEntity; @@ -873,12 +988,11 @@ export type Query = { getTimelineThreadsFromPersonId: TimelineThreadsWithTotal; index: Index; indexMetadatas: IndexConnection; + listSSOIdentityProvidersByWorkspaceId: Array; object: Object; objects: ObjectConnection; relation: Relation; relations: RelationConnection; - serverlessFunction: ServerlessFunction; - serverlessFunctions: ServerlessFunctionConnection; validatePasswordResetToken: ValidatePasswordResetToken; }; @@ -925,13 +1039,13 @@ export type QueryFindOneRemoteServerByIdArgs = { }; -export type QueryFindWorkspaceFromInviteHashArgs = { - inviteHash: Scalars['String']['input']; +export type QueryFindOneServerlessFunctionArgs = { + input: ServerlessFunctionIdInput; }; -export type QueryGetAisqlQueryArgs = { - text: Scalars['String']['input']; +export type QueryFindWorkspaceFromInviteHashArgs = { + inviteHash: Scalars['String']['input']; }; @@ -1005,18 +1119,6 @@ export type QueryRelationsArgs = { }; -export type QueryServerlessFunctionArgs = { - id: Scalars['UUID']['input']; -}; - - -export type QueryServerlessFunctionsArgs = { - filter?: ServerlessFunctionFilter; - paging?: CursorPaging; - sorting?: Array; -}; - - export type QueryValidatePasswordResetTokenArgs = { passwordResetToken: Scalars['String']['input']; }; @@ -1104,6 +1206,12 @@ export type RunWorkflowVersionInput = { workflowVersionId: Scalars['String']['input']; }; +export enum SsoIdentityProviderStatus { + Active = 'Active', + Error = 'Error', + Inactive = 'Inactive' +} + export type SendInvitationsOutput = { __typename?: 'SendInvitationsOutput'; errors: Array; @@ -1125,28 +1233,14 @@ export type ServerlessFunction = { description?: Maybe; id: Scalars['UUID']['output']; latestVersion?: Maybe; + latestVersionInputSchema?: Maybe>; name: Scalars['String']['output']; + publishedVersions: Array; runtime: Scalars['String']['output']; syncStatus: ServerlessFunctionSyncStatus; updatedAt: Scalars['DateTime']['output']; }; -export type ServerlessFunctionConnection = { - __typename?: 'ServerlessFunctionConnection'; - /** Array of edges. */ - edges: Array; - /** Paging information */ - pageInfo: PageInfo; -}; - -export type ServerlessFunctionEdge = { - __typename?: 'ServerlessFunctionEdge'; - /** Cursor for this node. */ - cursor: Scalars['ConnectionCursor']['output']; - /** The node containing the ServerlessFunction */ - node: ServerlessFunction; -}; - export type ServerlessFunctionExecutionResult = { __typename?: 'ServerlessFunctionExecutionResult'; /** Execution result in JSON format */ @@ -1165,22 +1259,11 @@ export enum ServerlessFunctionExecutionStatus { Success = 'SUCCESS' } -export type ServerlessFunctionFilter = { - and?: InputMaybe>; - id?: InputMaybe; - or?: InputMaybe>; -}; - -export type ServerlessFunctionSort = { - direction: SortDirection; - field: ServerlessFunctionSortFields; - nulls?: InputMaybe; +export type ServerlessFunctionIdInput = { + /** The id of the function. */ + id: Scalars['ID']['input']; }; -export enum ServerlessFunctionSortFields { - Id = 'id' -} - /** SyncStatus of the serverlessFunction */ export enum ServerlessFunctionSyncStatus { NotReady = 'NOT_READY', @@ -1192,6 +1275,31 @@ export type SessionEntity = { url?: Maybe; }; +export type SetupOidcSsoInput = { + clientID: Scalars['String']['input']; + clientSecret: Scalars['String']['input']; + issuer: Scalars['String']['input']; + name: Scalars['String']['input']; +}; + +export type SetupSamlSsoInput = { + certificate: Scalars['String']['input']; + fingerprint?: InputMaybe; + id: Scalars['String']['input']; + issuer: Scalars['String']['input']; + name: Scalars['String']['input']; + ssoURL: Scalars['String']['input']; +}; + +export type SetupSsoOutput = { + __typename?: 'SetupSsoOutput'; + id: Scalars['String']['output']; + issuer: Scalars['String']['output']; + name: Scalars['String']['output']; + status: SsoIdentityProviderStatus; + type: IdpType; +}; + /** Sort Directions */ export enum SortDirection { Asc = 'ASC', @@ -1340,11 +1448,13 @@ export type UpdateObjectPayload = { icon?: InputMaybe; imageIdentifierFieldMetadataId?: InputMaybe; isActive?: InputMaybe; + isLabelSyncedWithName?: InputMaybe; labelIdentifierFieldMetadataId?: InputMaybe; labelPlural?: InputMaybe; labelSingular?: InputMaybe; namePlural?: InputMaybe; nameSingular?: InputMaybe; + shortcut?: InputMaybe; }; export type UpdateOneFieldMetadataInput = { @@ -1381,11 +1491,13 @@ export type UpdateWorkspaceInput = { displayName?: InputMaybe; domainName?: InputMaybe; inviteHash?: InputMaybe; + isPublicInviteLinkEnabled?: InputMaybe; logo?: InputMaybe; }; export type User = { __typename?: 'User'; + analyticsTinybirdJwt?: Maybe; canImpersonate: Scalars['Boolean']['output']; createdAt: Scalars['DateTime']['output']; defaultAvatarUrl?: Maybe; @@ -1478,8 +1590,10 @@ export type Workspace = { displayName?: Maybe; domainName?: Maybe; featureFlags?: Maybe>; + hasValidEntrepriseKey: Scalars['Boolean']['output']; id: Scalars['UUID']['output']; inviteHash?: Maybe; + isPublicInviteLinkEnabled: Scalars['Boolean']['output']; logo?: Maybe; metadataVersion: Scalars['Float']['output']; updatedAt: Scalars['DateTime']['output']; @@ -1552,6 +1666,12 @@ export enum WorkspaceMemberTimeFormatEnum { System = 'SYSTEM' } +export type WorkspaceNameAndId = { + __typename?: 'WorkspaceNameAndId'; + displayName?: Maybe; + id: Scalars['String']['output']; +}; + export type Field = { __typename?: 'field'; createdAt: Scalars['DateTime']['output']; @@ -1670,6 +1790,7 @@ export type Object = { indexMetadatas: ObjectIndexMetadatasConnection; isActive: Scalars['Boolean']['output']; isCustom: Scalars['Boolean']['output']; + isLabelSyncedWithName: Scalars['Boolean']['output']; isRemote: Scalars['Boolean']['output']; isSystem: Scalars['Boolean']['output']; labelIdentifierFieldMetadataId?: Maybe; @@ -1677,6 +1798,7 @@ export type Object = { labelSingular: Scalars['String']['output']; namePlural: Scalars['String']['output']; nameSingular: Scalars['String']['output']; + shortcut?: Maybe; updatedAt: Scalars['DateTime']['output']; }; @@ -1863,23 +1985,23 @@ export type ObjectMetadataItemsQueryVariables = Exact<{ }>; -export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, indexMetadatas: { __typename?: 'ObjectIndexMetadatasConnection', edges: Array<{ __typename?: 'indexEdge', node: { __typename?: 'index', id: any, createdAt: any, updatedAt: any, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, indexFieldMetadatas: { __typename?: 'IndexIndexFieldMetadatasConnection', edges: Array<{ __typename?: 'indexFieldEdge', node: { __typename?: 'indexField', id: any, createdAt: any, updatedAt: any, order: number, fieldMetadataId: any } }> } } }> }, fields: { __typename?: 'ObjectFieldsConnection', edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, settings?: any | null, relationDefinition?: { __typename?: 'RelationDefinition', relationId: any, direction: RelationDefinitionType, sourceObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'field', id: any, name: string }, targetObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, targetFieldMetadata: { __typename?: 'field', id: any, name: string } } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; +export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, shortcut?: string | null, isLabelSyncedWithName: boolean, indexMetadatas: { __typename?: 'ObjectIndexMetadatasConnection', edges: Array<{ __typename?: 'indexEdge', node: { __typename?: 'index', id: any, createdAt: any, updatedAt: any, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, indexFieldMetadatas: { __typename?: 'IndexIndexFieldMetadatasConnection', edges: Array<{ __typename?: 'indexFieldEdge', node: { __typename?: 'indexField', id: any, createdAt: any, updatedAt: any, order: number, fieldMetadataId: any } }> } } }> }, fields: { __typename?: 'ObjectFieldsConnection', edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, settings?: any | null, relationDefinition?: { __typename?: 'RelationDefinition', relationId: any, direction: RelationDefinitionType, sourceObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'field', id: any, name: string }, targetObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, targetFieldMetadata: { __typename?: 'field', id: any, name: string } } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; -export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any }; +export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any, latestVersionInputSchema?: Array<{ __typename?: 'FunctionParameter', name: string, type: string }> | null }; export type CreateOneServerlessFunctionItemMutationVariables = Exact<{ input: CreateServerlessFunctionInput; }>; -export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any, latestVersionInputSchema?: Array<{ __typename?: 'FunctionParameter', name: string, type: string }> | null } }; export type DeleteOneServerlessFunctionMutationVariables = Exact<{ - input: DeleteServerlessFunctionInput; + input: ServerlessFunctionIdInput; }>; -export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any, latestVersionInputSchema?: Array<{ __typename?: 'FunctionParameter', name: string, type: string }> | null } }; export type ExecuteOneServerlessFunctionMutationVariables = Exact<{ input: ExecuteServerlessFunctionInput; @@ -1893,14 +2015,14 @@ export type PublishOneServerlessFunctionMutationVariables = Exact<{ }>; -export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any, latestVersionInputSchema?: Array<{ __typename?: 'FunctionParameter', name: string, type: string }> | null } }; export type UpdateOneServerlessFunctionMutationVariables = Exact<{ input: UpdateServerlessFunctionInput; }>; -export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any, latestVersionInputSchema?: Array<{ __typename?: 'FunctionParameter', name: string, type: string }> | null } }; export type FindManyAvailablePackagesQueryVariables = Exact<{ [key: string]: never; }>; @@ -1910,14 +2032,14 @@ export type FindManyAvailablePackagesQuery = { __typename?: 'Query', getAvailabl export type GetManyServerlessFunctionsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', serverlessFunctions: { __typename?: 'ServerlessFunctionConnection', edges: Array<{ __typename?: 'ServerlessFunctionEdge', node: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }> } }; +export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', findManyServerlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any, latestVersionInputSchema?: Array<{ __typename?: 'FunctionParameter', name: string, type: string }> | null }> }; export type GetOneServerlessFunctionQueryVariables = Exact<{ - id: Scalars['UUID']['input']; + input: ServerlessFunctionIdInput; }>; -export type GetOneServerlessFunctionQuery = { __typename?: 'Query', serverlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type GetOneServerlessFunctionQuery = { __typename?: 'Query', findOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any, latestVersionInputSchema?: Array<{ __typename?: 'FunctionParameter', name: string, type: string }> | null } }; export type FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{ input: GetServerlessFunctionSourceCodeInput; @@ -1928,7 +2050,7 @@ export type FindOneServerlessFunctionSourceCodeQuery = { __typename?: 'Query', g export const RemoteServerFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}}]} as unknown as DocumentNode; export const RemoteTableFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"schemaPendingUpdates"}}]}}]} as unknown as DocumentNode; -export const ServerlessFunctionFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const ServerlessFunctionFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const CreateServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"createServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRemoteServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneRemoteServer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}}]} as unknown as DocumentNode; export const DeleteServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"deleteServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneRemoteServer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SyncRemoteTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"syncRemoteTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTableInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"syncRemoteTable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteTableFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"schemaPendingUpdates"}}]}}]} as unknown as DocumentNode; @@ -1946,13 +2068,13 @@ export const UpdateOneObjectMetadataItemDocument = {"kind":"Document","definitio export const DeleteOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneField"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneRelationMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneRelationMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneRelation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; -export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"indexMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"indexWhereClause"}},{"kind":"Field","name":{"kind":"Name","value":"indexType"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"indexFieldMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}}]}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}},{"kind":"Field","name":{"kind":"Name","value":"relationDefinition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"relationId"}},{"kind":"Field","name":{"kind":"Name","value":"direction"}},{"kind":"Field","name":{"kind":"Name","value":"sourceObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; -export const CreateOneServerlessFunctionItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneServerlessFunctionItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; -export const DeleteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"shortcut"}},{"kind":"Field","name":{"kind":"Name","value":"isLabelSyncedWithName"}},{"kind":"Field","name":{"kind":"Name","value":"indexMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"indexWhereClause"}},{"kind":"Field","name":{"kind":"Name","value":"indexType"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"indexFieldMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}}]}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}},{"kind":"Field","name":{"kind":"Name","value":"relationDefinition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"relationId"}},{"kind":"Field","name":{"kind":"Name","value":"direction"}},{"kind":"Field","name":{"kind":"Name","value":"sourceObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateOneServerlessFunctionItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneServerlessFunctionItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const DeleteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const ExecuteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ExecuteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ExecuteServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"executeOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode; -export const PublishOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"PublishOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PublishServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publishServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; -export const UpdateOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const PublishOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"PublishOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PublishServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publishServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const UpdateOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const FindManyAvailablePackagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyAvailablePackages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAvailablePackages"}}]}}]} as unknown as DocumentNode; -export const GetManyServerlessFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverlessFunctions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; -export const GetOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const GetManyServerlessFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const GetOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersionInputSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const FindOneServerlessFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneServerlessFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GetServerlessFunctionSourceCodeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getServerlessFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 9e930133b3fb..766a07067bab 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import * as Apollo from '@apollo/client'; import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -21,13 +21,6 @@ export type Scalars = { Upload: any; }; -export type AisqlQueryResult = { - __typename?: 'AISQLQueryResult'; - queryFailedErrorMessage?: Maybe; - sqlQuery: Scalars['String']; - sqlQueryResult?: Maybe; -}; - export type ActivateWorkspaceInput = { displayName?: InputMaybe; }; @@ -71,6 +64,7 @@ export type AuthProviders = { magicLink: Scalars['Boolean']; microsoft: Scalars['Boolean']; password: Scalars['Boolean']; + sso: Scalars['Boolean']; }; export type AuthToken = { @@ -148,6 +142,7 @@ export enum CaptchaDriverType { export type ClientConfig = { __typename?: 'ClientConfig'; + analyticsEnabled: Scalars['Boolean']; api: ApiConfig; authProviders: AuthProviders; billing: Billing; @@ -160,13 +155,12 @@ export type ClientConfig = { support: Support; }; -export type CreateServerlessFunctionFromFileInput = { - description?: InputMaybe; - name: Scalars['String']; +export type ComputeStepOutputSchemaInput = { + /** Step JSON format */ + step: Scalars['JSON']; }; export type CreateServerlessFunctionInput = { - code: Scalars['String']; description?: InputMaybe; name: Scalars['String']; }; @@ -187,9 +181,13 @@ export type DeleteOneObjectInput = { id: Scalars['UUID']; }; -export type DeleteServerlessFunctionInput = { - /** The id of the function. */ - id: Scalars['ID']; +export type DeleteSsoInput = { + identityProviderId: Scalars['String']; +}; + +export type DeleteSsoOutput = { + __typename?: 'DeleteSsoOutput'; + identityProviderId: Scalars['String']; }; /** Schema update on a table */ @@ -200,6 +198,20 @@ export enum DistantTableUpdate { TableDeleted = 'TABLE_DELETED' } +export type EditSsoInput = { + id: Scalars['String']; + status: SsoIdentityProviderStatus; +}; + +export type EditSsoOutput = { + __typename?: 'EditSsoOutput'; + id: Scalars['String']; + issuer: Scalars['String']; + name: Scalars['String']; + status: SsoIdentityProviderStatus; + type: IdpType; +}; + export type EmailPasswordResetLink = { __typename?: 'EmailPasswordResetLink'; /** Boolean that confirms query was dispatched */ @@ -289,12 +301,59 @@ export enum FileFolder { WorkspaceLogo = 'WorkspaceLogo' } +export type FindAvailableSsoidpInput = { + email: Scalars['String']; +}; + +export type FindAvailableSsoidpOutput = { + __typename?: 'FindAvailableSSOIDPOutput'; + id: Scalars['String']; + issuer: Scalars['String']; + name: Scalars['String']; + status: SsoIdentityProviderStatus; + type: IdpType; + workspace: WorkspaceNameAndId; +}; + export type FullName = { __typename?: 'FullName'; firstName: Scalars['String']; lastName: Scalars['String']; }; +export type FunctionParameter = { + __typename?: 'FunctionParameter'; + name: Scalars['String']; + type: Scalars['String']; +}; + +export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth; + +export type GenerateJwtOutputWithAuthTokens = { + __typename?: 'GenerateJWTOutputWithAuthTokens'; + authTokens: AuthTokens; + reason: Scalars['String']; + success: Scalars['Boolean']; +}; + +export type GenerateJwtOutputWithSsoauth = { + __typename?: 'GenerateJWTOutputWithSSOAUTH'; + availableSSOIDPs: Array; + reason: Scalars['String']; + success: Scalars['Boolean']; +}; + +export type GetAuthorizationUrlInput = { + identityProviderId: Scalars['String']; +}; + +export type GetAuthorizationUrlOutput = { + __typename?: 'GetAuthorizationUrlOutput'; + authorizationURL: Scalars['String']; + id: Scalars['String']; + type: Scalars['String']; +}; + export type GetServerlessFunctionSourceCodeInput = { /** The id of the function. */ id: Scalars['ID']; @@ -302,6 +361,41 @@ export type GetServerlessFunctionSourceCodeInput = { version?: Scalars['String']; }; +export enum IdpType { + Oidc = 'OIDC', + Saml = 'SAML' +} + +export type IndexConnection = { + __typename?: 'IndexConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + +export type IndexIndexFieldMetadatasConnection = { + __typename?: 'IndexIndexFieldMetadatasConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + +export type IndexObjectMetadataConnection = { + __typename?: 'IndexObjectMetadataConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + +/** Type of the index */ +export enum IndexType { + Btree = 'BTREE', + Gin = 'GIN' +} + export type InvalidatePassword = { __typename?: 'InvalidatePassword'; /** Boolean that confirms query was dispatched */ @@ -341,24 +435,30 @@ export type Mutation = { authorizeApp: AuthorizeApp; challenge: LoginToken; checkoutSession: SessionEntity; + computeStepOutputSchema: Scalars['JSON']; + createOIDCIdentityProvider: SetupSsoOutput; createOneAppToken: AppToken; createOneObject: Object; createOneServerlessFunction: ServerlessFunction; - createOneServerlessFunctionFromFile: ServerlessFunction; + createSAMLIdentityProvider: SetupSsoOutput; deactivateWorkflowVersion: Scalars['Boolean']; deleteCurrentWorkspace: Workspace; deleteOneObject: Object; deleteOneServerlessFunction: ServerlessFunction; + deleteSSOIdentityProvider: DeleteSsoOutput; deleteUser: User; deleteWorkspaceInvitation: Scalars['String']; disablePostgresProxy: PostgresCredentials; + editSSOIdentityProvider: EditSsoOutput; emailPasswordResetLink: EmailPasswordResetLink; enablePostgresProxy: PostgresCredentials; exchangeAuthorizationCode: ExchangeAuthCode; executeOneServerlessFunction: ServerlessFunctionExecutionResult; + findAvailableSSOIdentityProviders: Array; generateApiKeyToken: ApiKeyToken; - generateJWT: AuthTokens; + generateJWT: GenerateJwt; generateTransientToken: TransientToken; + getAuthorizationUrl: GetAuthorizationUrlOutput; impersonate: Verify; publishServerlessFunction: ServerlessFunction; renewToken: AuthTokens; @@ -421,14 +521,23 @@ export type MutationCheckoutSessionArgs = { }; +export type MutationComputeStepOutputSchemaArgs = { + input: ComputeStepOutputSchemaInput; +}; + + +export type MutationCreateOidcIdentityProviderArgs = { + input: SetupOidcSsoInput; +}; + + export type MutationCreateOneServerlessFunctionArgs = { input: CreateServerlessFunctionInput; }; -export type MutationCreateOneServerlessFunctionFromFileArgs = { - file: Scalars['Upload']; - input: CreateServerlessFunctionFromFileInput; +export type MutationCreateSamlIdentityProviderArgs = { + input: SetupSamlSsoInput; }; @@ -443,7 +552,12 @@ export type MutationDeleteOneObjectArgs = { export type MutationDeleteOneServerlessFunctionArgs = { - input: DeleteServerlessFunctionInput; + input: ServerlessFunctionIdInput; +}; + + +export type MutationDeleteSsoIdentityProviderArgs = { + input: DeleteSsoInput; }; @@ -452,6 +566,11 @@ export type MutationDeleteWorkspaceInvitationArgs = { }; +export type MutationEditSsoIdentityProviderArgs = { + input: EditSsoInput; +}; + + export type MutationEmailPasswordResetLinkArgs = { email: Scalars['String']; }; @@ -469,6 +588,11 @@ export type MutationExecuteOneServerlessFunctionArgs = { }; +export type MutationFindAvailableSsoIdentityProvidersArgs = { + input: FindAvailableSsoidpInput; +}; + + export type MutationGenerateApiKeyTokenArgs = { apiKeyId: Scalars['String']; expiresAt: Scalars['String']; @@ -480,6 +604,11 @@ export type MutationGenerateJwtArgs = { }; +export type MutationGetAuthorizationUrlArgs = { + input: GetAuthorizationUrlInput; +}; + + export type MutationImpersonateArgs = { userId: Scalars['String']; }; @@ -520,9 +649,8 @@ export type MutationSignUpArgs = { export type MutationTrackArgs = { - data: Scalars['JSON']; - sessionId: Scalars['String']; - type: Scalars['String']; + action: Scalars['String']; + payload: Scalars['JSON']; }; @@ -589,6 +717,14 @@ export type ObjectFieldsConnection = { pageInfo: PageInfo; }; +export type ObjectIndexMetadatasConnection = { + __typename?: 'ObjectIndexMetadatasConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; +}; + /** Onboarding status */ export enum OnboardingStatus { Completed = 'COMPLETED', @@ -652,21 +788,23 @@ export type Query = { clientConfig: ClientConfig; currentUser: User; currentWorkspace: Workspace; + findManyServerlessFunctions: Array; + findOneServerlessFunction: ServerlessFunction; findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; - getAISQLQuery: AisqlQueryResult; getAvailablePackages: Scalars['JSON']; getPostgresCredentials?: Maybe; getProductPrices: ProductPricesEntity; - getServerlessFunctionSourceCode?: Maybe; + getServerlessFunctionSourceCode?: Maybe; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal; getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal; getTimelineThreadsFromPersonId: TimelineThreadsWithTotal; + index: Index; + indexMetadatas: IndexConnection; + listSSOIdentityProvidersByWorkspaceId: Array; object: Object; objects: ObjectConnection; - serverlessFunction: ServerlessFunction; - serverlessFunctions: ServerlessFunctionConnection; validatePasswordResetToken: ValidatePasswordResetToken; }; @@ -687,13 +825,13 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = { }; -export type QueryFindWorkspaceFromInviteHashArgs = { - inviteHash: Scalars['String']; +export type QueryFindOneServerlessFunctionArgs = { + input: ServerlessFunctionIdInput; }; -export type QueryGetAisqlQueryArgs = { - text: Scalars['String']; +export type QueryFindWorkspaceFromInviteHashArgs = { + inviteHash: Scalars['String']; }; @@ -808,6 +946,12 @@ export type RunWorkflowVersionInput = { workflowVersionId: Scalars['String']; }; +export enum SsoIdentityProviderStatus { + Active = 'Active', + Error = 'Error', + Inactive = 'Inactive' +} + export type SendInvitationsOutput = { __typename?: 'SendInvitationsOutput'; errors: Array; @@ -829,29 +973,14 @@ export type ServerlessFunction = { description?: Maybe; id: Scalars['UUID']; latestVersion?: Maybe; + latestVersionInputSchema?: Maybe>; name: Scalars['String']; + publishedVersions: Array; runtime: Scalars['String']; - sourceCodeHash: Scalars['String']; syncStatus: ServerlessFunctionSyncStatus; updatedAt: Scalars['DateTime']; }; -export type ServerlessFunctionConnection = { - __typename?: 'ServerlessFunctionConnection'; - /** Array of edges. */ - edges: Array; - /** Paging information */ - pageInfo: PageInfo; -}; - -export type ServerlessFunctionEdge = { - __typename?: 'ServerlessFunctionEdge'; - /** Cursor for this node. */ - cursor: Scalars['ConnectionCursor']; - /** The node containing the ServerlessFunction */ - node: ServerlessFunction; -}; - export type ServerlessFunctionExecutionResult = { __typename?: 'ServerlessFunctionExecutionResult'; /** Execution result in JSON format */ @@ -870,6 +999,11 @@ export enum ServerlessFunctionExecutionStatus { Success = 'SUCCESS' } +export type ServerlessFunctionIdInput = { + /** The id of the function. */ + id: Scalars['ID']; +}; + /** SyncStatus of the serverlessFunction */ export enum ServerlessFunctionSyncStatus { NotReady = 'NOT_READY', @@ -881,6 +1015,31 @@ export type SessionEntity = { url?: Maybe; }; +export type SetupOidcSsoInput = { + clientID: Scalars['String']; + clientSecret: Scalars['String']; + issuer: Scalars['String']; + name: Scalars['String']; +}; + +export type SetupSamlSsoInput = { + certificate: Scalars['String']; + fingerprint?: InputMaybe; + id: Scalars['String']; + issuer: Scalars['String']; + name: Scalars['String']; + ssoURL: Scalars['String']; +}; + +export type SetupSsoOutput = { + __typename?: 'SetupSsoOutput'; + id: Scalars['String']; + issuer: Scalars['String']; + name: Scalars['String']; + status: SsoIdentityProviderStatus; + type: IdpType; +}; + /** Sort Directions */ export enum SortDirection { Asc = 'ASC', @@ -1014,11 +1173,13 @@ export type UpdateObjectPayload = { icon?: InputMaybe; imageIdentifierFieldMetadataId?: InputMaybe; isActive?: InputMaybe; + isLabelSyncedWithName?: InputMaybe; labelIdentifierFieldMetadataId?: InputMaybe; labelPlural?: InputMaybe; labelSingular?: InputMaybe; namePlural?: InputMaybe; nameSingular?: InputMaybe; + shortcut?: InputMaybe; }; export type UpdateOneObjectInput = { @@ -1028,7 +1189,7 @@ export type UpdateOneObjectInput = { }; export type UpdateServerlessFunctionInput = { - code: Scalars['String']; + code: Scalars['JSON']; description?: InputMaybe; /** Id of the serverless function to execute */ id: Scalars['UUID']; @@ -1040,11 +1201,13 @@ export type UpdateWorkspaceInput = { displayName?: InputMaybe; domainName?: InputMaybe; inviteHash?: InputMaybe; + isPublicInviteLinkEnabled?: InputMaybe; logo?: InputMaybe; }; export type User = { __typename?: 'User'; + analyticsTinybirdJwt?: Maybe; canImpersonate: Scalars['Boolean']; createdAt: Scalars['DateTime']; defaultAvatarUrl?: Maybe; @@ -1127,8 +1290,10 @@ export type Workspace = { displayName?: Maybe; domainName?: Maybe; featureFlags?: Maybe>; + hasValidEntrepriseKey: Scalars['Boolean']; id: Scalars['UUID']; inviteHash?: Maybe; + isPublicInviteLinkEnabled: Scalars['Boolean']; logo?: Maybe; metadataVersion: Scalars['Float']; updatedAt: Scalars['DateTime']; @@ -1201,6 +1366,12 @@ export enum WorkspaceMemberTimeFormatEnum { System = 'SYSTEM' } +export type WorkspaceNameAndId = { + __typename?: 'WorkspaceNameAndId'; + displayName?: Maybe; + id: Scalars['String']; +}; + export type Field = { __typename?: 'field'; createdAt: Scalars['DateTime']; @@ -1213,6 +1384,7 @@ export type Field = { isCustom?: Maybe; isNullable?: Maybe; isSystem?: Maybe; + isUnique?: Maybe; label: Scalars['String']; name: Scalars['String']; object?: Maybe; @@ -1241,6 +1413,71 @@ export type FieldFilter = { or?: InputMaybe>; }; +export type Index = { + __typename?: 'index'; + createdAt: Scalars['DateTime']; + id: Scalars['UUID']; + indexFieldMetadatas: IndexIndexFieldMetadatasConnection; + indexType: IndexType; + indexWhereClause?: Maybe; + isCustom?: Maybe; + isUnique: Scalars['Boolean']; + name: Scalars['String']; + objectMetadata: IndexObjectMetadataConnection; + updatedAt: Scalars['DateTime']; +}; + + +export type IndexIndexFieldMetadatasArgs = { + filter?: IndexFieldFilter; + paging?: CursorPaging; +}; + + +export type IndexObjectMetadataArgs = { + filter?: ObjectFilter; + paging?: CursorPaging; +}; + +export type IndexEdge = { + __typename?: 'indexEdge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']; + /** The node containing the index */ + node: Index; +}; + +export type IndexField = { + __typename?: 'indexField'; + createdAt: Scalars['DateTime']; + fieldMetadataId: Scalars['UUID']; + id: Scalars['UUID']; + order: Scalars['Float']; + updatedAt: Scalars['DateTime']; +}; + +export type IndexFieldEdge = { + __typename?: 'indexFieldEdge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']; + /** The node containing the indexField */ + node: IndexField; +}; + +export type IndexFieldFilter = { + and?: InputMaybe>; + fieldMetadataId?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; +}; + +export type IndexFilter = { + and?: InputMaybe>; + id?: InputMaybe; + isCustom?: InputMaybe; + or?: InputMaybe>; +}; + export type Object = { __typename?: 'object'; createdAt: Scalars['DateTime']; @@ -1250,8 +1487,10 @@ export type Object = { icon?: Maybe; id: Scalars['UUID']; imageIdentifierFieldMetadataId?: Maybe; + indexMetadatas: ObjectIndexMetadatasConnection; isActive: Scalars['Boolean']; isCustom: Scalars['Boolean']; + isLabelSyncedWithName: Scalars['Boolean']; isRemote: Scalars['Boolean']; isSystem: Scalars['Boolean']; labelIdentifierFieldMetadataId?: Maybe; @@ -1259,6 +1498,7 @@ export type Object = { labelSingular: Scalars['String']; namePlural: Scalars['String']; nameSingular: Scalars['String']; + shortcut?: Maybe; updatedAt: Scalars['DateTime']; }; @@ -1268,6 +1508,12 @@ export type ObjectFieldsArgs = { paging?: CursorPaging; }; + +export type ObjectIndexMetadatasArgs = { + filter?: IndexFilter; + paging?: CursorPaging; +}; + export type ObjectEdge = { __typename?: 'objectEdge'; /** Cursor for this node. */ @@ -1276,6 +1522,16 @@ export type ObjectEdge = { node: Object; }; +export type ObjectFilter = { + and?: InputMaybe>; + id?: InputMaybe; + isActive?: InputMaybe; + isCustom?: InputMaybe; + isRemote?: InputMaybe; + isSystem?: InputMaybe; + or?: InputMaybe>; +}; + export type Relation = { __typename?: 'relation'; createdAt: Scalars['DateTime']; @@ -1374,6 +1630,8 @@ export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: strin export type AuthTokensFragmentFragment = { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } }; +export type AvailableSsoIdentityProvidersFragmentFragment = { __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } }; + export type AuthorizeAppMutationVariables = Exact<{ clientId: Scalars['String']; codeChallenge: Scalars['String']; @@ -1399,6 +1657,13 @@ export type EmailPasswordResetLinkMutationVariables = Exact<{ export type EmailPasswordResetLinkMutation = { __typename?: 'Mutation', emailPasswordResetLink: { __typename?: 'EmailPasswordResetLink', success: boolean } }; +export type FindAvailableSsoIdentityProvidersMutationVariables = Exact<{ + input: FindAvailableSsoidpInput; +}>; + + +export type FindAvailableSsoIdentityProvidersMutation = { __typename?: 'Mutation', findAvailableSSOIdentityProviders: Array<{ __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } }> }; + export type GenerateApiKeyTokenMutationVariables = Exact<{ apiKeyId: Scalars['String']; expiresAt: Scalars['String']; @@ -1412,19 +1677,26 @@ export type GenerateJwtMutationVariables = Exact<{ }>; -export type GenerateJwtMutation = { __typename?: 'Mutation', generateJWT: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type GenerateJwtMutation = { __typename?: 'Mutation', generateJWT: { __typename?: 'GenerateJWTOutputWithAuthTokens', success: boolean, reason: string, authTokens: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } } | { __typename?: 'GenerateJWTOutputWithSSOAUTH', success: boolean, reason: string, availableSSOIDPs: Array<{ __typename?: 'FindAvailableSSOIDPOutput', id: string, issuer: string, name: string, status: SsoIdentityProviderStatus, workspace: { __typename?: 'WorkspaceNameAndId', id: string, displayName?: string | null } }> } }; export type GenerateTransientTokenMutationVariables = Exact<{ [key: string]: never; }>; export type GenerateTransientTokenMutation = { __typename?: 'Mutation', generateTransientToken: { __typename?: 'TransientToken', transientToken: { __typename?: 'AuthToken', token: string } } }; +export type GetAuthorizationUrlMutationVariables = Exact<{ + input: GetAuthorizationUrlInput; +}>; + + +export type GetAuthorizationUrlMutation = { __typename?: 'Mutation', getAuthorizationUrl: { __typename?: 'GetAuthorizationUrlOutput', id: string, type: string, authorizationURL: string } }; + export type ImpersonateMutationVariables = Exact<{ userId: Scalars['String']; }>; -export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type RenewTokenMutationVariables = Exact<{ appToken: Scalars['String']; @@ -1457,7 +1729,7 @@ export type VerifyMutationVariables = Exact<{ }>; -export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type CheckUserExistsQueryVariables = Exact<{ email: Scalars['String']; @@ -1504,21 +1776,47 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } }; -export type GetAisqlQueryQueryVariables = Exact<{ - text: Scalars['String']; +export type CreateOidcIdentityProviderMutationVariables = Exact<{ + input: SetupOidcSsoInput; }>; -export type GetAisqlQueryQuery = { __typename?: 'Query', getAISQLQuery: { __typename?: 'AISQLQueryResult', sqlQuery: string, sqlQueryResult?: string | null, queryFailedErrorMessage?: string | null } }; +export type CreateOidcIdentityProviderMutation = { __typename?: 'Mutation', createOIDCIdentityProvider: { __typename?: 'SetupSsoOutput', id: string, type: IdpType, issuer: string, name: string, status: SsoIdentityProviderStatus } }; + +export type CreateSamlIdentityProviderMutationVariables = Exact<{ + input: SetupSamlSsoInput; +}>; + -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; +export type CreateSamlIdentityProviderMutation = { __typename?: 'Mutation', createSAMLIdentityProvider: { __typename?: 'SetupSsoOutput', id: string, type: IdpType, issuer: string, name: string, status: SsoIdentityProviderStatus } }; + +export type DeleteSsoIdentityProviderMutationVariables = Exact<{ + input: DeleteSsoInput; +}>; + + +export type DeleteSsoIdentityProviderMutation = { __typename?: 'Mutation', deleteSSOIdentityProvider: { __typename?: 'DeleteSsoOutput', identityProviderId: string } }; + +export type EditSsoIdentityProviderMutationVariables = Exact<{ + input: EditSsoInput; +}>; + + +export type EditSsoIdentityProviderMutation = { __typename?: 'Mutation', editSSOIdentityProvider: { __typename?: 'EditSsoOutput', id: string, type: IdpType, issuer: string, name: string, status: SsoIdentityProviderStatus } }; + +export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key: string]: never; }>; + + +export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdpType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> }; + +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -1535,7 +1833,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -1544,6 +1842,13 @@ export type ActivateWorkflowVersionMutationVariables = Exact<{ export type ActivateWorkflowVersionMutation = { __typename?: 'Mutation', activateWorkflowVersion: boolean }; +export type ComputeStepOutputSchemaMutationVariables = Exact<{ + input: ComputeStepOutputSchemaInput; +}>; + + +export type ComputeStepOutputSchemaMutation = { __typename?: 'Mutation', computeStepOutputSchema: any }; + export type DeactivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; }>; @@ -1551,6 +1856,13 @@ export type DeactivateWorkflowVersionMutationVariables = Exact<{ export type DeactivateWorkflowVersionMutation = { __typename?: 'Mutation', deactivateWorkflowVersion: boolean }; +export type RunWorkflowVersionMutationVariables = Exact<{ + input: RunWorkflowVersionInput; +}>; + + +export type RunWorkflowVersionMutation = { __typename?: 'Mutation', runWorkflowVersion: { __typename?: 'WorkflowRun', workflowRunId: any } }; + export type DeleteWorkspaceInvitationMutationVariables = Exact<{ appTokenId: Scalars['String']; }>; @@ -1713,6 +2025,18 @@ export const AuthTokensFragmentFragmentDoc = gql` } } ${AuthTokenFragmentFragmentDoc}`; +export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql` + fragment AvailableSSOIdentityProvidersFragment on FindAvailableSSOIDPOutput { + id + issuer + name + status + workspace { + id + displayName + } +} + `; export const WorkspaceMemberQueryFragmentFragmentDoc = gql` fragment WorkspaceMemberQueryFragment on WorkspaceMember { id @@ -1736,6 +2060,7 @@ export const UserQueryFragmentFragmentDoc = gql` email canImpersonate supportUserHash + analyticsTinybirdJwt onboardingStatus workspaceMember { ...WorkspaceMemberQueryFragment @@ -1751,6 +2076,8 @@ export const UserQueryFragmentFragmentDoc = gql` inviteHash allowImpersonation activationStatus + isPublicInviteLinkEnabled + hasValidEntrepriseKey featureFlags { id key @@ -1962,7 +2289,7 @@ export type TrackMutationFn = Apollo.MutationFunction; export type EmailPasswordResetLinkMutationResult = Apollo.MutationResult; export type EmailPasswordResetLinkMutationOptions = Apollo.BaseMutationOptions; +export const FindAvailableSsoIdentityProvidersDocument = gql` + mutation FindAvailableSSOIdentityProviders($input: FindAvailableSSOIDPInput!) { + findAvailableSSOIdentityProviders(input: $input) { + ...AvailableSSOIdentityProvidersFragment + } +} + ${AvailableSsoIdentityProvidersFragmentFragmentDoc}`; +export type FindAvailableSsoIdentityProvidersMutationFn = Apollo.MutationFunction; + +/** + * __useFindAvailableSsoIdentityProvidersMutation__ + * + * To run a mutation, you first call `useFindAvailableSsoIdentityProvidersMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useFindAvailableSsoIdentityProvidersMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [findAvailableSsoIdentityProvidersMutation, { data, loading, error }] = useFindAvailableSsoIdentityProvidersMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useFindAvailableSsoIdentityProvidersMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(FindAvailableSsoIdentityProvidersDocument, options); + } +export type FindAvailableSsoIdentityProvidersMutationHookResult = ReturnType; +export type FindAvailableSsoIdentityProvidersMutationResult = Apollo.MutationResult; +export type FindAvailableSsoIdentityProvidersMutationOptions = Apollo.BaseMutationOptions; export const GenerateApiKeyTokenDocument = gql` mutation GenerateApiKeyToken($apiKeyId: String!, $expiresAt: String!) { generateApiKeyToken(apiKeyId: $apiKeyId, expiresAt: $expiresAt) { @@ -2184,12 +2544,26 @@ export type GenerateApiKeyTokenMutationOptions = Apollo.BaseMutationOptions; /** @@ -2250,6 +2624,41 @@ export function useGenerateTransientTokenMutation(baseOptions?: Apollo.MutationH export type GenerateTransientTokenMutationHookResult = ReturnType; export type GenerateTransientTokenMutationResult = Apollo.MutationResult; export type GenerateTransientTokenMutationOptions = Apollo.BaseMutationOptions; +export const GetAuthorizationUrlDocument = gql` + mutation GetAuthorizationUrl($input: GetAuthorizationUrlInput!) { + getAuthorizationUrl(input: $input) { + id + type + authorizationURL + } +} + `; +export type GetAuthorizationUrlMutationFn = Apollo.MutationFunction; + +/** + * __useGetAuthorizationUrlMutation__ + * + * To run a mutation, you first call `useGetAuthorizationUrlMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGetAuthorizationUrlMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [getAuthorizationUrlMutation, { data, loading, error }] = useGetAuthorizationUrlMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useGetAuthorizationUrlMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GetAuthorizationUrlDocument, options); + } +export type GetAuthorizationUrlMutationHookResult = ReturnType; +export type GetAuthorizationUrlMutationResult = Apollo.MutationResult; +export type GetAuthorizationUrlMutationOptions = Apollo.BaseMutationOptions; export const ImpersonateDocument = gql` mutation Impersonate($userId: String!) { impersonate(userId: $userId) { @@ -2668,6 +3077,7 @@ export const GetClientConfigDocument = gql` google password microsoft + sso } billing { isBillingEnabled @@ -2677,6 +3087,7 @@ export const GetClientConfigDocument = gql` signInPrefilled signUpDisabled debugMode + analyticsEnabled support { supportDriver supportFrontChatId @@ -2756,43 +3167,188 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType; export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult; export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions; -export const GetAisqlQueryDocument = gql` - query GetAISQLQuery($text: String!) { - getAISQLQuery(text: $text) { - sqlQuery - sqlQueryResult - queryFailedErrorMessage +export const CreateOidcIdentityProviderDocument = gql` + mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) { + createOIDCIdentityProvider(input: $input) { + id + type + issuer + name + status + } +} + `; +export type CreateOidcIdentityProviderMutationFn = Apollo.MutationFunction; + +/** + * __useCreateOidcIdentityProviderMutation__ + * + * To run a mutation, you first call `useCreateOidcIdentityProviderMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateOidcIdentityProviderMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createOidcIdentityProviderMutation, { data, loading, error }] = useCreateOidcIdentityProviderMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useCreateOidcIdentityProviderMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateOidcIdentityProviderDocument, options); + } +export type CreateOidcIdentityProviderMutationHookResult = ReturnType; +export type CreateOidcIdentityProviderMutationResult = Apollo.MutationResult; +export type CreateOidcIdentityProviderMutationOptions = Apollo.BaseMutationOptions; +export const CreateSamlIdentityProviderDocument = gql` + mutation CreateSAMLIdentityProvider($input: SetupSAMLSsoInput!) { + createSAMLIdentityProvider(input: $input) { + id + type + issuer + name + status + } +} + `; +export type CreateSamlIdentityProviderMutationFn = Apollo.MutationFunction; + +/** + * __useCreateSamlIdentityProviderMutation__ + * + * To run a mutation, you first call `useCreateSamlIdentityProviderMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateSamlIdentityProviderMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createSamlIdentityProviderMutation, { data, loading, error }] = useCreateSamlIdentityProviderMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useCreateSamlIdentityProviderMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateSamlIdentityProviderDocument, options); + } +export type CreateSamlIdentityProviderMutationHookResult = ReturnType; +export type CreateSamlIdentityProviderMutationResult = Apollo.MutationResult; +export type CreateSamlIdentityProviderMutationOptions = Apollo.BaseMutationOptions; +export const DeleteSsoIdentityProviderDocument = gql` + mutation DeleteSSOIdentityProvider($input: DeleteSsoInput!) { + deleteSSOIdentityProvider(input: $input) { + identityProviderId + } +} + `; +export type DeleteSsoIdentityProviderMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteSsoIdentityProviderMutation__ + * + * To run a mutation, you first call `useDeleteSsoIdentityProviderMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteSsoIdentityProviderMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [deleteSsoIdentityProviderMutation, { data, loading, error }] = useDeleteSsoIdentityProviderMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useDeleteSsoIdentityProviderMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteSsoIdentityProviderDocument, options); + } +export type DeleteSsoIdentityProviderMutationHookResult = ReturnType; +export type DeleteSsoIdentityProviderMutationResult = Apollo.MutationResult; +export type DeleteSsoIdentityProviderMutationOptions = Apollo.BaseMutationOptions; +export const EditSsoIdentityProviderDocument = gql` + mutation EditSSOIdentityProvider($input: EditSsoInput!) { + editSSOIdentityProvider(input: $input) { + id + type + issuer + name + status + } +} + `; +export type EditSsoIdentityProviderMutationFn = Apollo.MutationFunction; + +/** + * __useEditSsoIdentityProviderMutation__ + * + * To run a mutation, you first call `useEditSsoIdentityProviderMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useEditSsoIdentityProviderMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [editSsoIdentityProviderMutation, { data, loading, error }] = useEditSsoIdentityProviderMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useEditSsoIdentityProviderMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(EditSsoIdentityProviderDocument, options); + } +export type EditSsoIdentityProviderMutationHookResult = ReturnType; +export type EditSsoIdentityProviderMutationResult = Apollo.MutationResult; +export type EditSsoIdentityProviderMutationOptions = Apollo.BaseMutationOptions; +export const ListSsoIdentityProvidersByWorkspaceIdDocument = gql` + query ListSSOIdentityProvidersByWorkspaceId { + listSSOIdentityProvidersByWorkspaceId { + type + id + name + issuer + status } } `; /** - * __useGetAisqlQueryQuery__ + * __useListSsoIdentityProvidersByWorkspaceIdQuery__ * - * To run a query within a React component, call `useGetAisqlQueryQuery` and pass it any options that fit your needs. - * When your component renders, `useGetAisqlQueryQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useListSsoIdentityProvidersByWorkspaceIdQuery` and pass it any options that fit your needs. + * When your component renders, `useListSsoIdentityProvidersByWorkspaceIdQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useGetAisqlQueryQuery({ + * const { data, loading, error } = useListSsoIdentityProvidersByWorkspaceIdQuery({ * variables: { - * text: // value for 'text' * }, * }); */ -export function useGetAisqlQueryQuery(baseOptions: Apollo.QueryHookOptions) { +export function useListSsoIdentityProvidersByWorkspaceIdQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetAisqlQueryDocument, options); + return Apollo.useQuery(ListSsoIdentityProvidersByWorkspaceIdDocument, options); } -export function useGetAisqlQueryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useListSsoIdentityProvidersByWorkspaceIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetAisqlQueryDocument, options); + return Apollo.useLazyQuery(ListSsoIdentityProvidersByWorkspaceIdDocument, options); } -export type GetAisqlQueryQueryHookResult = ReturnType; -export type GetAisqlQueryLazyQueryHookResult = ReturnType; -export type GetAisqlQueryQueryResult = Apollo.QueryResult; +export type ListSsoIdentityProvidersByWorkspaceIdQueryHookResult = ReturnType; +export type ListSsoIdentityProvidersByWorkspaceIdLazyQueryHookResult = ReturnType; +export type ListSsoIdentityProvidersByWorkspaceIdQueryResult = Apollo.QueryResult; export const DeleteUserAccountDocument = gql` mutation DeleteUserAccount { deleteUser { @@ -2921,6 +3477,37 @@ export function useActivateWorkflowVersionMutation(baseOptions?: Apollo.Mutation export type ActivateWorkflowVersionMutationHookResult = ReturnType; export type ActivateWorkflowVersionMutationResult = Apollo.MutationResult; export type ActivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions; +export const ComputeStepOutputSchemaDocument = gql` + mutation ComputeStepOutputSchema($input: ComputeStepOutputSchemaInput!) { + computeStepOutputSchema(input: $input) +} + `; +export type ComputeStepOutputSchemaMutationFn = Apollo.MutationFunction; + +/** + * __useComputeStepOutputSchemaMutation__ + * + * To run a mutation, you first call `useComputeStepOutputSchemaMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useComputeStepOutputSchemaMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [computeStepOutputSchemaMutation, { data, loading, error }] = useComputeStepOutputSchemaMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useComputeStepOutputSchemaMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ComputeStepOutputSchemaDocument, options); + } +export type ComputeStepOutputSchemaMutationHookResult = ReturnType; +export type ComputeStepOutputSchemaMutationResult = Apollo.MutationResult; +export type ComputeStepOutputSchemaMutationOptions = Apollo.BaseMutationOptions; export const DeactivateWorkflowVersionDocument = gql` mutation DeactivateWorkflowVersion($workflowVersionId: String!) { deactivateWorkflowVersion(workflowVersionId: $workflowVersionId) @@ -2952,6 +3539,39 @@ export function useDeactivateWorkflowVersionMutation(baseOptions?: Apollo.Mutati export type DeactivateWorkflowVersionMutationHookResult = ReturnType; export type DeactivateWorkflowVersionMutationResult = Apollo.MutationResult; export type DeactivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions; +export const RunWorkflowVersionDocument = gql` + mutation RunWorkflowVersion($input: RunWorkflowVersionInput!) { + runWorkflowVersion(input: $input) { + workflowRunId + } +} + `; +export type RunWorkflowVersionMutationFn = Apollo.MutationFunction; + +/** + * __useRunWorkflowVersionMutation__ + * + * To run a mutation, you first call `useRunWorkflowVersionMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRunWorkflowVersionMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [runWorkflowVersionMutation, { data, loading, error }] = useRunWorkflowVersionMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useRunWorkflowVersionMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RunWorkflowVersionDocument, options); + } +export type RunWorkflowVersionMutationHookResult = ReturnType; +export type RunWorkflowVersionMutationResult = Apollo.MutationResult; +export type RunWorkflowVersionMutationOptions = Apollo.BaseMutationOptions; export const DeleteWorkspaceInvitationDocument = gql` mutation DeleteWorkspaceInvitation($appTokenId: String!) { deleteWorkspaceInvitation(appTokenId: $appTokenId) diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index 95c2a58b79c6..a7b683e660f7 100644 --- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts +++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts @@ -2,6 +2,7 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { AppPath } from '@/types/AppPath'; + import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql'; diff --git a/packages/twenty-front/src/hooks/useCleanRecoilState.ts b/packages/twenty-front/src/hooks/useCleanRecoilState.ts index 3f0a41df4aed..9da98c006af9 100644 --- a/packages/twenty-front/src/hooks/useCleanRecoilState.ts +++ b/packages/twenty-front/src/hooks/useCleanRecoilState.ts @@ -1,8 +1,9 @@ -import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; -import { SettingsPath } from '@/types/SettingsPath'; import { apiKeyTokenState } from '@/settings/developers/states/generatedApiKeyTokenState'; -import { useRecoilValue, useResetRecoilState } from 'recoil'; import { AppPath } from '@/types/AppPath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { useRecoilValue, useResetRecoilState } from 'recoil'; +import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; + import { isDefined } from '~/utils/isDefined'; export const useCleanRecoilState = () => { diff --git a/packages/twenty-front/src/index.css b/packages/twenty-front/src/index.css index 808fd8917db4..22b25687aafd 100644 --- a/packages/twenty-front/src/index.css +++ b/packages/twenty-front/src/index.css @@ -9,7 +9,11 @@ html { font-size: 13px; } +button { + font-size: 13px; +} + /* https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge */ -.grecaptcha-badge { +.grecaptcha-badge { visibility: hidden !important; } diff --git a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx index c8de2f64c46a..b3ea1eda955e 100644 --- a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx +++ b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx @@ -4,7 +4,7 @@ import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { ANIMATION, BACKGROUND_LIGHT, GRAY_SCALE } from 'twenty-ui'; import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; -import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths'; +import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { MainNavigationDrawerItemsSkeletonLoader } from '~/loading/components/MainNavigationDrawerItemsSkeletonLoader'; @@ -47,14 +47,14 @@ const StyledSkeletonTitleContainer = styled.div` export const LeftPanelSkeletonLoader = () => { const isMobile = useIsMobile(); - const mobileWidth = isMobile ? 0 : '100%'; - const desktopWidth = !mobileWidth ? 12 : DESKTOP_NAV_DRAWER_WIDTHS.menu; return ( { - const isCurrentUserLoaded = useRecoilValue(isCurrentUserLoadedState); - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - return !isCurrentUserLoaded || objectMetadataItems.length === 0; -}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter.tsx new file mode 100644 index 000000000000..35dc9b0706f4 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter.tsx @@ -0,0 +1,8 @@ +import { WorkflowRunActionEffect } from '@/action-menu/actions/global-actions/workflow-run-actions/components/WorkflowRunActionEffect'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; + +export const GlobalActionMenuEntriesSetter = () => { + const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); + + return <>{isWorkflowEnabled && }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/global-actions/workflow-run-actions/components/WorkflowRunActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/global-actions/workflow-run-actions/components/WorkflowRunActionEffect.tsx new file mode 100644 index 000000000000..7a9f78d869ee --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/global-actions/workflow-run-actions/components/WorkflowRunActionEffect.tsx @@ -0,0 +1,68 @@ +import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions'; +import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; + +import { useTheme } from '@emotion/react'; +import { useEffect } from 'react'; +import { IconSettingsAutomation } from 'twenty-ui'; +import { capitalize } from '~/utils/string/capitalize'; + +export const WorkflowRunActionEffect = () => { + const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); + + const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({ + triggerType: 'MANUAL', + }); + + const { runWorkflowVersion } = useRunWorkflowVersion(); + + const { enqueueSnackBar } = useSnackBar(); + + const theme = useTheme(); + + useEffect(() => { + for (const [ + index, + activeWorkflowVersion, + ] of activeWorkflowVersions.entries()) { + addActionMenuEntry({ + type: 'workflow-run', + key: `workflow-run-${activeWorkflowVersion.id}`, + label: capitalize(activeWorkflowVersion.workflow.name), + position: index, + Icon: IconSettingsAutomation, + onClick: async () => { + await runWorkflowVersion(activeWorkflowVersion.id); + + enqueueSnackBar('', { + variant: SnackBarVariant.Success, + title: `${capitalize(activeWorkflowVersion.workflow.name)} starting...`, + icon: ( + + ), + }); + }, + }); + } + + return () => { + for (const activeWorkflowVersion of activeWorkflowVersions) { + removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`); + } + }; + }, [ + activeWorkflowVersions, + addActionMenuEntry, + enqueueSnackBar, + removeActionMenuEntry, + runWorkflowVersion, + theme.snackBar.success.color, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx index 89243ead97c5..b4c7e585c17f 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx @@ -1,60 +1,109 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { useFavorites } from '@/favorites/hooks/useFavorites'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; -import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData'; +import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds'; +import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { useCallback, useEffect, useState } from 'react'; -import { useRecoilValue } from 'recoil'; -import { IconTrash } from 'twenty-ui'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { IconTrash, isDefined } from 'twenty-ui'; export const DeleteRecordsActionEffect = ({ position, + objectMetadataItem, }: { position: number; + objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = + useState(false); + + const { resetTableRowSelection } = useRecordTable({ + recordTableId: objectMetadataItem.namePlural, + }); + + const { deleteManyRecords } = useDeleteManyRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + }); + + const { favorites, deleteFavorite } = useFavorites(); + + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, + ); + + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, ); - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, + const graphqlFilter = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem, ); - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, + const { fetchAllRecordIds } = useFetchAllRecordIds({ + objectNameSingular: objectMetadataItem.nameSingular, + filter: graphqlFilter, }); - const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = - useState(false); + const { closeRightDrawer } = useRightDrawer(); - const { deleteTableData } = useDeleteTableData({ - objectNameSingular: objectMetadataItem?.nameSingular ?? '', - recordIndexId: objectMetadataItem?.namePlural ?? '', - }); + const handleDeleteClick = useCallback(async () => { + const recordIdsToDelete = await fetchAllRecordIds(); - const handleDeleteClick = useCallback(() => { - deleteTableData(contextStoreTargetedRecordIds); - }, [deleteTableData, contextStoreTargetedRecordIds]); + resetTableRowSelection(); - const isRemoteObject = objectMetadataItem?.isRemote ?? false; + for (const recordIdToDelete of recordIdsToDelete) { + const foundFavorite = favorites?.find( + (favorite) => favorite.recordId === recordIdToDelete, + ); - const numberOfSelectedRecords = contextStoreTargetedRecordIds.length; + if (foundFavorite !== undefined) { + deleteFavorite(foundFavorite.id); + } + } + + await deleteManyRecords(recordIdsToDelete, { + delayInMsBetweenRequests: 50, + }); + }, [ + deleteFavorite, + deleteManyRecords, + favorites, + fetchAllRecordIds, + resetTableRowSelection, + ]); + + const isRemoteObject = objectMetadataItem.isRemote; const canDelete = - !isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT; + !isRemoteObject && + isDefined(contextStoreNumberOfSelectedRecords) && + contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT && + contextStoreNumberOfSelectedRecords > 0; + + const { isInRightDrawer, onActionExecutedCallback } = + useContext(ActionMenuContext); useEffect(() => { if (canDelete) { addActionMenuEntry({ + type: 'standard', key: 'delete', label: 'Delete', position, Icon: IconTrash, accent: 'danger', + isPinned: true, onClick: () => { setIsDeleteRecordsModalOpen(true); }, @@ -62,17 +111,25 @@ export const DeleteRecordsActionEffect = ({ handleDeleteClick()} + onConfirmClick={() => { + handleDeleteClick(); + onActionExecutedCallback?.(); + if (isInRightDrawer) { + closeRightDrawer(); + } + }} deleteButtonText={`Delete ${ - numberOfSelectedRecords > 1 ? 'Records' : 'Record' + contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record' }`} /> ), @@ -80,14 +137,21 @@ export const DeleteRecordsActionEffect = ({ } else { removeActionMenuEntry('delete'); } + + return () => { + removeActionMenuEntry('delete'); + }; }, [ - canDelete, addActionMenuEntry, - removeActionMenuEntry, - isDeleteRecordsModalOpen, - numberOfSelectedRecords, + canDelete, + closeRightDrawer, + contextStoreNumberOfSelectedRecords, handleDeleteClick, + isDeleteRecordsModalOpen, + isInRightDrawer, + onActionExecutedCallback, position, + removeActionMenuEntry, ]); return null; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx index d7b50ddaf0d3..870eb85a8d35 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx @@ -1,46 +1,44 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { displayedExportProgress, - useExportTableData, -} from '@/object-record/record-index/options/hooks/useExportTableData'; + useExportRecordData, +} from '@/action-menu/hooks/useExportRecordData'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { IconDatabaseExport } from 'twenty-ui'; + import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; -import { IconFileExport } from 'twenty-ui'; export const ExportRecordsActionEffect = ({ position, + objectMetadataItem, }: { position: number; + objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, ); - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, - }); - - const baseTableDataParams = { + const { progress, download } = useExportRecordData({ delayMs: 100, - objectNameSingular: objectMetadataItem?.nameSingular ?? '', - recordIndexId: objectMetadataItem?.namePlural ?? '', - }; - - const { progress, download } = useExportTableData({ - ...baseTableDataParams, - filename: `${objectMetadataItem?.nameSingular}.csv`, + objectMetadataItem, + recordIndexId: objectMetadataItem.namePlural, + filename: `${objectMetadataItem.nameSingular}.csv`, }); useEffect(() => { addActionMenuEntry({ + type: 'standard', key: 'export', position, - label: displayedExportProgress(progress), - Icon: IconFileExport, + label: displayedExportProgress( + contextStoreNumberOfSelectedRecords > 0 ? 'selection' : 'all', + progress, + ), + Icon: IconDatabaseExport, accent: 'default', onClick: () => download(), }); @@ -48,6 +46,14 @@ export const ExportRecordsActionEffect = ({ return () => { removeActionMenuEntry('export'); }; - }, [download, progress, addActionMenuEntry, removeActionMenuEntry, position]); + }, [ + contextStoreNumberOfSelectedRecords, + download, + progress, + addActionMenuEntry, + removeActionMenuEntry, + position, + ]); + return null; }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx index e9767b034203..25d03d071664 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx @@ -1,39 +1,37 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui'; export const ManageFavoritesActionEffect = ({ position, + objectMetadataItem, }: { position: number; + objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, - ); - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, ); const { favorites, createFavorite, deleteFavorite } = useFavorites(); - const selectedRecordId = contextStoreTargetedRecordIds[0]; + const selectedRecordId = + contextStoreTargetedRecordsRule.mode === 'selection' + ? contextStoreTargetedRecordsRule.selectedRecordIds[0] + : undefined; const selectedRecord = useRecoilValue( - recordStoreFamilyState(selectedRecordId), + recordStoreFamilyState(selectedRecordId ?? ''), ); - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, - }); - const foundFavorite = favorites?.find( (favorite) => favorite.recordId === selectedRecordId, ); @@ -46,6 +44,7 @@ export const ManageFavoritesActionEffect = ({ } addActionMenuEntry({ + type: 'standard', key: 'manage-favorites', label: isFavorite ? 'Remove from favorites' : 'Add to favorites', position, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx deleted file mode 100644 index 69bfd3305094..000000000000 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; -import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; - -const actionEffects = [ExportRecordsActionEffect, DeleteRecordsActionEffect]; - -export const MultipleRecordsActionMenuEntriesSetter = () => { - return ( - <> - {actionEffects.map((ActionEffect, index) => ( - - ))} - - ); -}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx index 75267e445d49..2bcc8400d259 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx @@ -1,20 +1,69 @@ -import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter'; -import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; -import { useRecoilValue } from 'recoil'; +import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; +import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; +import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect'; +import { WorkflowRunRecordActionEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect'; +import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; + +const globalRecordActionEffects = [ExportRecordsActionEffect]; + +const singleRecordActionEffects = [ + ManageFavoritesActionEffect, + DeleteRecordsActionEffect, +]; + +const multipleRecordActionEffects = [DeleteRecordsActionEffect]; export const RecordActionMenuEntriesSetter = () => { - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, ); - if (contextStoreTargetedRecordIds.length === 0) { - return null; - } + const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataIdComponentState, + ); - if (contextStoreTargetedRecordIds.length === 1) { - return ; + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: contextStoreCurrentObjectMetadataId ?? '', + }); + + const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); + + if (!objectMetadataItem) { + throw new Error( + `Object metadata item not found for id ${contextStoreCurrentObjectMetadataId}`, + ); } - return ; + const actions = + contextStoreNumberOfSelectedRecords === 1 + ? singleRecordActionEffects + : multipleRecordActionEffects; + + return ( + <> + {globalRecordActionEffects.map((ActionEffect, index) => ( + + ))} + {actions.map((ActionEffect, index) => ( + + ))} + {contextStoreNumberOfSelectedRecords === 1 && isWorkflowEnabled && ( + + )} + + ); }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx deleted file mode 100644 index feeba5aabc61..000000000000 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; -import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; -import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect'; - -export const SingleRecordActionMenuEntriesSetter = () => { - const actionEffects = [ - ExportRecordsActionEffect, - DeleteRecordsActionEffect, - ManageFavoritesActionEffect, - ]; - return ( - <> - {actionEffects.map((ActionEffect, index) => ( - - ))} - - ); -}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx new file mode 100644 index 000000000000..9c8d509e3eb0 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx @@ -0,0 +1,101 @@ +import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions'; +import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; + +import { useTheme } from '@emotion/react'; +import { useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; +import { IconSettingsAutomation, isDefined } from 'twenty-ui'; +import { capitalize } from '~/utils/string/capitalize'; + +export const WorkflowRunRecordActionEffect = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); + + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, + ); + + const selectedRecordId = + contextStoreTargetedRecordsRule.mode === 'selection' + ? contextStoreTargetedRecordsRule.selectedRecordIds[0] + : undefined; + + const selectedRecord = useRecoilValue( + recordStoreFamilyState(selectedRecordId ?? ''), + ); + + const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({ + objectMetadataItem, + triggerType: 'MANUAL', + }); + + const { runWorkflowVersion } = useRunWorkflowVersion(); + + const { enqueueSnackBar } = useSnackBar(); + + const theme = useTheme(); + + useEffect(() => { + if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) { + return; + } + + for (const [ + index, + activeWorkflowVersion, + ] of activeWorkflowVersions.entries()) { + addActionMenuEntry({ + type: 'workflow-run', + key: `workflow-run-${activeWorkflowVersion.id}`, + label: capitalize(activeWorkflowVersion.workflow.name), + position: index, + Icon: IconSettingsAutomation, + onClick: async () => { + if (!isDefined(selectedRecord)) { + return; + } + + await runWorkflowVersion(activeWorkflowVersion.id, selectedRecord); + + enqueueSnackBar('', { + variant: SnackBarVariant.Success, + title: `${capitalize(activeWorkflowVersion.workflow.name)} starting...`, + icon: ( + + ), + }); + }, + }); + } + + return () => { + for (const activeWorkflowVersion of activeWorkflowVersions) { + removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`); + } + }; + }, [ + activeWorkflowVersions, + addActionMenuEntry, + enqueueSnackBar, + objectMetadataItem, + removeActionMenuEntry, + runWorkflowVersion, + selectedRecord, + theme.snackBar.success.color, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx index 0b3e52032c59..2dd9ab9071cf 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx @@ -1,5 +1,10 @@ import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import styled from '@emotion/styled'; + +const StyledActionMenuConfirmationModals = styled.div` + position: absolute; +`; export const ActionMenuConfirmationModals = () => { const actionMenuEntries = useRecoilComponentValueV2( @@ -7,12 +12,12 @@ export const ActionMenuConfirmationModals = () => { ); return ( -
+ {actionMenuEntries.map((actionMenuEntry, index) => actionMenuEntry.ConfirmationModal ? (
{actionMenuEntry.ConfirmationModal}
) : null, )} -
+ ); }; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx deleted file mode 100644 index 60355cc9256f..000000000000 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useActionMenu } from '@/action-menu/hooks/useActionMenu'; -import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; -import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; -import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; -import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; -import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; - -export const ActionMenuEffect = () => { - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, - ); - - const actionMenuId = useAvailableComponentInstanceIdOrThrow( - ActionMenuComponentInstanceContext, - ); - - const { openActionBar, closeActionBar } = useActionMenu(actionMenuId); - - const isDropdownOpen = useRecoilValue( - extractComponentState( - isDropdownOpenComponentState, - `action-menu-dropdown-${actionMenuId}`, - ), - ); - - useEffect(() => { - if (contextStoreTargetedRecordIds.length > 0 && !isDropdownOpen) { - // We only handle opening the ActionMenuBar here, not the Dropdown. - // The Dropdown is already managed by sync handlers for events like - // right-click to open and click outside to close. - openActionBar(); - } - if (contextStoreTargetedRecordIds.length === 0) { - closeActionBar(); - } - }, [ - contextStoreTargetedRecordIds, - openActionBar, - closeActionBar, - isDropdownOpen, - ]); - - return null; -}; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx new file mode 100644 index 000000000000..baa2be8b9d34 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx @@ -0,0 +1,36 @@ +import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter'; +import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; +import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; +import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar'; +import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown'; +import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect'; +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; + +import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordIndexActionMenu = () => { + const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataIdComponentState, + ); + + return ( + <> + {contextStoreCurrentObjectMetadataId && ( + {}, + }} + > + + + + + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx similarity index 55% rename from packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx rename to packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx index 258683347919..fcc93f5bc50e 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx @@ -1,14 +1,15 @@ import styled from '@emotion/styled'; -import { ActionMenuBarEntry } from '@/action-menu/components/ActionMenuBarEntry'; +import { RecordIndexActionMenuBarAllActionsButton } from '@/action-menu/components/RecordIndexActionMenuBarAllActionsButton'; +import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useRecoilValue } from 'recoil'; const StyledLabel = styled.div` color: ${({ theme }) => theme.font.color.tertiary}; @@ -18,9 +19,9 @@ const StyledLabel = styled.div` padding-right: ${({ theme }) => theme.spacing(2)}; `; -export const ActionMenuBar = () => { - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, +export const RecordIndexActionMenuBar = () => { + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, ); const actionMenuId = useAvailableComponentInstanceIdOrThrow( @@ -31,23 +32,24 @@ export const ActionMenuBar = () => { actionMenuEntriesComponentSelector, ); - if (actionMenuEntries.length === 0) { + const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned); + + if (contextStoreNumberOfSelectedRecords === 0) { return null; } return ( - - {contextStoreTargetedRecordIds.length} selected: - - {actionMenuEntries.map((entry, index) => ( - + {contextStoreNumberOfSelectedRecords} selected: + {pinnedEntries.map((entry, index) => ( + ))} + ); }; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx new file mode 100644 index 000000000000..336b4f734879 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx @@ -0,0 +1,53 @@ +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { IconLayoutSidebarRightExpand } from 'twenty-ui'; + +const StyledButton = styled.div` + border-radius: ${({ theme }) => theme.border.radius.sm}; + color: ${({ theme }) => theme.font.color.secondary}; + cursor: pointer; + display: flex; + justify-content: center; + + padding: ${({ theme }) => theme.spacing(2)}; + transition: background ${({ theme }) => theme.animation.duration.fast} ease; + user-select: none; + + &:hover { + background: ${({ theme }) => theme.background.tertiary}; + } +`; + +const StyledButtonLabel = styled.div` + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin-left: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledShortcutLabel = styled.div` + color: ${({ theme }) => theme.font.color.light}; + font-weight: ${({ theme }) => theme.font.weight.medium}; +`; + +const StyledSeparator = styled.div<{ size: 'sm' | 'md' }>` + background: ${({ theme }) => theme.border.color.light}; + height: ${({ theme, size }) => theme.spacing(size === 'sm' ? 4 : 8)}; + margin: 0 ${({ theme }) => theme.spacing(1)}; + width: 1px; +`; + +export const RecordIndexActionMenuBarAllActionsButton = () => { + const theme = useTheme(); + const { openCommandMenu } = useCommandMenu(); + return ( + <> + + openCommandMenu()}> + + All Actions + + ⌘K + + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx new file mode 100644 index 000000000000..ffa52d20590b --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx @@ -0,0 +1,41 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; + +type RecordIndexActionMenuBarEntryProps = { + entry: ActionMenuEntry; +}; + +const StyledButton = styled.div` + border-radius: ${({ theme }) => theme.border.radius.sm}; + color: ${({ theme }) => theme.font.color.secondary}; + cursor: pointer; + display: flex; + justify-content: center; + + padding: ${({ theme }) => theme.spacing(2)}; + transition: background ${({ theme }) => theme.animation.duration.fast} ease; + user-select: none; + + &:hover { + background: ${({ theme }) => theme.background.tertiary}; + } +`; + +const StyledButtonLabel = styled.div` + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin-left: ${({ theme }) => theme.spacing(1)}; +`; + +export const RecordIndexActionMenuBarEntry = ({ + entry, +}: RecordIndexActionMenuBarEntryProps) => { + const theme = useTheme(); + return ( + entry.onClick?.()}> + {entry.Icon && } + {entry.label} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuDropdown.tsx similarity index 67% rename from packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx rename to packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuDropdown.tsx index 18ebdac7667e..1a62cab35e7d 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuDropdown.tsx @@ -3,15 +3,17 @@ import { useRecoilValue } from 'recoil'; import { PositionType } from '../types/PositionType'; -import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState'; import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope'; +import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { MenuItem } from 'twenty-ui'; type StyledContainerProps = { position: PositionType; @@ -34,7 +36,7 @@ const StyledContainerActionMenuDropdown = styled.div` width: auto; `; -export const ActionMenuDropdown = () => { +export const RecordIndexActionMenuDropdown = () => { const actionMenuEntries = useRecoilComponentValueV2( actionMenuEntriesComponentSelector, ); @@ -45,15 +47,11 @@ export const ActionMenuDropdown = () => { const actionMenuDropdownPosition = useRecoilValue( extractComponentState( - actionMenuDropdownPositionComponentState, - `action-menu-dropdown-${actionMenuId}`, + recordIndexActionMenuDropdownPositionComponentState, + getActionMenuDropdownIdFromActionMenuId(actionMenuId), ), ); - if (actionMenuEntries.length === 0) { - return null; - } - //TODO: remove this const width = actionMenuEntries.some( (actionMenuEntry) => actionMenuEntry.label === 'Remove from favorites', @@ -64,24 +62,28 @@ export const ActionMenuDropdown = () => { return ( ( - - ))} + dropdownComponents={ + + {actionMenuEntries.map((item, index) => ( + + ))} + + } /> ); diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuEffect.tsx new file mode 100644 index 000000000000..3306d9fb0fb0 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuEffect.tsx @@ -0,0 +1,69 @@ +import { useActionMenu } from '@/action-menu/hooks/useActionMenu'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId'; +import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; +import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; +import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; + +export const RecordIndexActionMenuEffect = () => { + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, + ); + + const actionMenuId = useAvailableComponentInstanceIdOrThrow( + ActionMenuComponentInstanceContext, + ); + + const { openActionBar, closeActionBar } = useActionMenu(actionMenuId); + + // Using closeActionBar here was causing a bug because it goes back to the + // previous hotkey scope, and we don't want that here. + const setIsBottomBarOpened = useSetRecoilComponentStateV2( + isBottomBarOpenedComponentState, + getActionBarIdFromActionMenuId(actionMenuId), + ); + + const isDropdownOpen = useRecoilValue( + extractComponentState( + isDropdownOpenComponentState, + getActionMenuDropdownIdFromActionMenuId(actionMenuId), + ), + ); + const { isRightDrawerOpen } = useRightDrawer(); + + const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); + + useEffect(() => { + if (contextStoreNumberOfSelectedRecords > 0 && !isDropdownOpen) { + // We only handle opening the ActionMenuBar here, not the Dropdown. + // The Dropdown is already managed by sync handlers for events like + // right-click to open and click outside to close. + openActionBar(); + } + if (contextStoreNumberOfSelectedRecords === 0 && isDropdownOpen) { + closeActionBar(); + } + }, [ + contextStoreNumberOfSelectedRecords, + openActionBar, + closeActionBar, + isDropdownOpen, + ]); + + useEffect(() => { + if (isRightDrawerOpen || isCommandMenuOpened) { + setIsBottomBarOpened(false); + } + }, [isRightDrawerOpen, isCommandMenuOpened, setIsBottomBarOpened]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx new file mode 100644 index 000000000000..d92ae5603deb --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx @@ -0,0 +1,56 @@ +import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter'; +import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; +import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; + +import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader'; + +export const RecordShowActionMenu = ({ + isFavorite, + handleFavoriteButtonClick, + record, + objectMetadataItem, + objectNameSingular, +}: { + isFavorite: boolean; + handleFavoriteButtonClick: () => void; + record: ObjectRecord | undefined; + objectMetadataItem: ObjectMetadataItem; + objectNameSingular: string; +}) => { + const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataIdComponentState, + ); + + // TODO: refactor RecordShowPageBaseHeader to use the context store + + return ( + <> + {contextStoreCurrentObjectMetadataId && ( + {}, + }} + > + + + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx similarity index 71% rename from packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx rename to packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx index 02802ec4a616..b075565f4186 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx @@ -1,10 +1,9 @@ +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { MOBILE_VIEWPORT, MenuItemAccent } from 'twenty-ui'; -import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; -import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; - -type ActionMenuBarEntryProps = { +type RecordShowActionMenuBarEntryProps = { entry: ActionMenuEntry; }; @@ -26,7 +25,11 @@ const StyledButton = styled.div<{ accent: MenuItemAccent }>` background: ${({ theme, accent }) => accent === 'danger' ? theme.background.danger - : theme.background.tertiary}; + : theme.background.transparent.light}; + } + + @media (max-width: ${MOBILE_VIEWPORT}px) { + padding: ${({ theme }) => theme.spacing(1)}; } `; @@ -35,7 +38,11 @@ const StyledButtonLabel = styled.div` margin-left: ${({ theme }) => theme.spacing(1)}; `; -export const ActionMenuBarEntry = ({ entry }: ActionMenuBarEntryProps) => { +// For now, this component is the same as RecordIndexActionMenuBarEntry but they +// will probably diverge in the future +export const RecordShowActionMenuBarEntry = ({ + entry, +}: RecordShowActionMenuBarEntryProps) => { const theme = useTheme(); return ( { + const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataIdComponentState, + ); + + return ( + <> + {contextStoreCurrentObjectMetadataId && ( + {}, + }} + > + + + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenuBar.tsx new file mode 100644 index 000000000000..8f9540e4d623 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenuBar.tsx @@ -0,0 +1,21 @@ +import { RecordShowActionMenuBarEntry } from '@/action-menu/components/RecordShowActionMenuBarEntry'; +import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordShowRightDrawerActionMenuBar = () => { + const actionMenuEntries = useRecoilComponentValueV2( + actionMenuEntriesComponentSelector, + ); + + const standardActionMenuEntries = actionMenuEntries.filter( + (actionMenuEntry) => actionMenuEntry.type === 'standard', + ); + + return ( + <> + {standardActionMenuEntries.map((actionMenuEntry) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx deleted file mode 100644 index 34d709d1685d..000000000000 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { expect, jest } from '@storybook/jest'; -import { Meta, StoryObj } from '@storybook/react'; -import { RecoilRoot } from 'recoil'; - -import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; -import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; -import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; -import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; -import { userEvent, waitFor, within } from '@storybook/test'; -import { IconCheckbox, IconTrash } from 'twenty-ui'; - -const deleteMock = jest.fn(); -const markAsDoneMock = jest.fn(); - -const meta: Meta = { - title: 'Modules/ActionMenu/ActionMenuBar', - component: ActionMenuBar, - decorators: [ - (Story) => ( - { - set(contextStoreTargetedRecordIdsState, ['1', '2', '3']); - set( - actionMenuEntriesComponentState.atomFamily({ - instanceId: 'story-action-menu', - }), - new Map([ - [ - 'delete', - { - key: 'delete', - label: 'Delete', - position: 0, - Icon: IconTrash, - onClick: deleteMock, - }, - ], - [ - 'markAsDone', - { - key: 'markAsDone', - label: 'Mark as done', - position: 1, - Icon: IconCheckbox, - onClick: markAsDoneMock, - }, - ], - ]), - ); - set( - isBottomBarOpenedComponentState.atomFamily({ - instanceId: 'action-bar-story-action-menu', - }), - true, - ); - }} - > - - - - - ), - ], - args: { - actionMenuId: 'story-action-menu', - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - actionMenuId: 'story-action-menu', - }, -}; - -export const WithCustomSelection: Story = { - args: { - actionMenuId: 'story-action-menu', - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const selectionText = await canvas.findByText('3 selected:'); - expect(selectionText).toBeInTheDocument(); - }, -}; - -export const WithButtonClicks: Story = { - args: { - actionMenuId: 'story-action-menu', - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const deleteButton = await canvas.findByText('Delete'); - await userEvent.click(deleteButton); - - const markAsDoneButton = await canvas.findByText('Mark as done'); - await userEvent.click(markAsDoneButton); - - await waitFor(() => { - expect(deleteMock).toHaveBeenCalled(); - expect(markAsDoneMock).toHaveBeenCalled(); - }); - }, -}; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx new file mode 100644 index 000000000000..b7752d42f6f6 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx @@ -0,0 +1,123 @@ +import { expect, jest } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { RecoilRoot } from 'recoil'; + +import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; +import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId'; +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; +import { userEvent, waitFor, within } from '@storybook/test'; +import { IconTrash, RouterDecorator } from 'twenty-ui'; + +const deleteMock = jest.fn(); + +const meta: Meta = { + title: 'Modules/ActionMenu/RecordIndexActionMenuBar', + component: RecordIndexActionMenuBar, + decorators: [ + RouterDecorator, + (Story) => ( + + { + set( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + { + mode: 'selection', + selectedRecordIds: ['1', '2', '3'], + }, + ); + + set( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + 3, + ); + + const map = new Map(); + + map.set('delete', { + isPinned: true, + type: 'standard', + key: 'delete', + label: 'Delete', + position: 0, + Icon: IconTrash, + onClick: deleteMock, + }); + + set( + actionMenuEntriesComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + map, + ); + + set( + isBottomBarOpenedComponentState.atomFamily({ + instanceId: getActionBarIdFromActionMenuId('story-action-menu'), + }), + true, + ); + }} + > + + + + + + ), + ], + args: { + actionMenuId: 'story-action-menu', + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + actionMenuId: 'story-action-menu', + }, +}; + +export const WithCustomSelection: Story = { + args: { + actionMenuId: 'story-action-menu', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const selectionText = await canvas.findByText('3 selected:'); + expect(selectionText).toBeInTheDocument(); + }, +}; + +export const WithButtonClicks: Story = { + args: { + actionMenuId: 'story-action-menu', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const deleteButton = await canvas.findByText('Delete'); + await userEvent.click(deleteButton); + + await waitFor(() => { + expect(deleteMock).toHaveBeenCalled(); + }); + }, +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx similarity index 75% rename from packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx rename to packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx index a9c7b26b8430..275058bc276c 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx @@ -1,19 +1,19 @@ +import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry'; import { expect, jest } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/testing-library'; import { ComponentDecorator, IconCheckbox, IconTrash } from 'twenty-ui'; -import { ActionMenuBarEntry } from '../ActionMenuBarEntry'; -const meta: Meta = { - title: 'Modules/ActionMenu/ActionMenuBarEntry', - component: ActionMenuBarEntry, +const meta: Meta = { + title: 'Modules/ActionMenu/RecordIndexActionMenuBarEntry', + component: RecordIndexActionMenuBarEntry, decorators: [ComponentDecorator], }; export default meta; -type Story = StoryObj; +type Story = StoryObj; const deleteMock = jest.fn(); const markAsDoneMock = jest.fn(); @@ -21,6 +21,7 @@ const markAsDoneMock = jest.fn(); export const Default: Story = { args: { entry: { + type: 'standard', key: 'delete', label: 'Delete', position: 0, @@ -33,6 +34,7 @@ export const Default: Story = { export const WithDangerAccent: Story = { args: { entry: { + type: 'standard', key: 'delete', label: 'Delete', position: 0, @@ -46,6 +48,7 @@ export const WithDangerAccent: Story = { export const WithInteraction: Story = { args: { entry: { + type: 'standard', key: 'markAsDone', label: 'Mark as done', position: 0, diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx similarity index 63% rename from packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx rename to packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx index 53a0714cee9e..e9ba359dfa2e 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx @@ -3,10 +3,11 @@ import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/testing-library'; import { RecoilRoot } from 'recoil'; -import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown'; -import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState'; +import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState'; +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { IconCheckbox, IconHeart, IconTrash } from 'twenty-ui'; @@ -15,57 +16,57 @@ const deleteMock = jest.fn(); const markAsDoneMock = jest.fn(); const addToFavoritesMock = jest.fn(); -const meta: Meta = { - title: 'Modules/ActionMenu/ActionMenuDropdown', - component: ActionMenuDropdown, +const meta: Meta = { + title: 'Modules/ActionMenu/RecordIndexActionMenuDropdown', + component: RecordIndexActionMenuDropdown, decorators: [ (Story) => ( { set( extractComponentState( - actionMenuDropdownPositionComponentState, + recordIndexActionMenuDropdownPositionComponentState, 'action-menu-dropdown-story', ), { x: 10, y: 10 }, ); + + const map = new Map(); + set( actionMenuEntriesComponentState.atomFamily({ instanceId: 'story-action-menu', }), - new Map([ - [ - 'delete', - { - key: 'delete', - label: 'Delete', - position: 0, - Icon: IconTrash, - onClick: deleteMock, - }, - ], - [ - 'markAsDone', - { - key: 'markAsDone', - label: 'Mark as done', - position: 1, - Icon: IconCheckbox, - onClick: markAsDoneMock, - }, - ], - [ - 'addToFavorites', - { - key: 'addToFavorites', - label: 'Add to favorites', - position: 2, - Icon: IconHeart, - onClick: addToFavoritesMock, - }, - ], - ]), + map, ); + + map.set('delete', { + type: 'standard', + key: 'delete', + label: 'Delete', + position: 0, + Icon: IconTrash, + onClick: deleteMock, + }); + + map.set('markAsDone', { + type: 'standard', + key: 'markAsDone', + label: 'Mark as done', + position: 1, + Icon: IconCheckbox, + onClick: markAsDoneMock, + }); + + map.set('addToFavorites', { + type: 'standard', + key: 'addToFavorites', + label: 'Add to favorites', + position: 2, + Icon: IconHeart, + onClick: addToFavoritesMock, + }); + set( extractComponentState( isDropdownOpenComponentState, @@ -87,7 +88,7 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx new file mode 100644 index 000000000000..a1f4422b6825 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx @@ -0,0 +1,131 @@ +import { expect, jest } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { RecoilRoot } from 'recoil'; + +import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { userEvent, waitFor, within } from '@storybook/test'; +import { + ComponentDecorator, + IconFileExport, + IconHeart, + IconTrash, + MenuItemAccent, +} from 'twenty-ui'; + +const deleteMock = jest.fn(); +const addToFavoritesMock = jest.fn(); +const exportMock = jest.fn(); + +const meta: Meta = { + title: 'Modules/ActionMenu/RecordShowRightDrawerActionMenuBar', + component: RecordShowRightDrawerActionMenuBar, + decorators: [ + (Story) => ( + { + set( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + { + mode: 'selection', + selectedRecordIds: ['1'], + }, + ); + set( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + 1, + ); + + const map = new Map(); + + set( + actionMenuEntriesComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + map, + ); + + map.set('addToFavorites', { + type: 'standard', + key: 'addToFavorites', + label: 'Add to favorites', + position: 0, + Icon: IconHeart, + onClick: addToFavoritesMock, + }); + + map.set('export', { + type: 'standard', + key: 'export', + label: 'Export', + position: 1, + Icon: IconFileExport, + onClick: exportMock, + }); + + map.set('delete', { + type: 'standard', + key: 'delete', + label: 'Delete', + position: 2, + Icon: IconTrash, + onClick: deleteMock, + accent: 'danger' as MenuItemAccent, + }); + }} + > + + + + + ), + ComponentDecorator, + ], + args: { + actionMenuId: 'story-action-menu', + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + actionMenuId: 'story-action-menu', + }, +}; + +export const WithButtonClicks: Story = { + args: { + actionMenuId: 'story-action-menu', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const deleteButton = await canvas.findByText('Delete'); + await userEvent.click(deleteButton); + + const addToFavoritesButton = await canvas.findByText('Add to favorites'); + await userEvent.click(addToFavoritesButton); + + const exportButton = await canvas.findByText('Export'); + await userEvent.click(exportButton); + + await waitFor(() => { + expect(deleteMock).toHaveBeenCalled(); + expect(addToFavoritesMock).toHaveBeenCalled(); + expect(exportMock).toHaveBeenCalled(); + }); + }, +}; diff --git a/packages/twenty-front/src/modules/action-menu/contexts/ActionMenuContext.ts b/packages/twenty-front/src/modules/action-menu/contexts/ActionMenuContext.ts new file mode 100644 index 000000000000..0c1482f40b93 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/contexts/ActionMenuContext.ts @@ -0,0 +1,11 @@ +import { createContext } from 'react'; + +type ActionMenuContextType = { + isInRightDrawer: boolean; + onActionExecutedCallback: () => void; +}; + +export const ActionMenuContext = createContext({ + isInRightDrawer: false, + onActionExecutedCallback: () => {}, +}); diff --git a/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts index 0f37475adedc..aa00785961c0 100644 --- a/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts @@ -1,3 +1,5 @@ +import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId'; +import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { renderHook } from '@testing-library/react'; import { act } from 'react'; import { useActionMenu } from '../useActionMenu'; @@ -23,6 +25,9 @@ jest.mock('@/ui/layout/dropdown/hooks/useDropdownV2', () => ({ describe('useActionMenu', () => { const actionMenuId = 'test-action-menu'; + const actionBarId = getActionBarIdFromActionMenuId(actionMenuId); + const actionMenuDropdownId = + getActionMenuDropdownIdFromActionMenuId(actionMenuId); it('should return the correct functions', () => { const { result } = renderHook(() => useActionMenu(actionMenuId)); @@ -40,10 +45,8 @@ describe('useActionMenu', () => { result.current.openActionMenuDropdown(); }); - expect(closeBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`); - expect(openDropdown).toHaveBeenCalledWith( - `action-menu-dropdown-${actionMenuId}`, - ); + expect(closeBottomBar).toHaveBeenCalledWith(actionBarId); + expect(openDropdown).toHaveBeenCalledWith(actionMenuDropdownId); }); it('should call the correct functions when opening action bar', () => { @@ -53,10 +56,8 @@ describe('useActionMenu', () => { result.current.openActionBar(); }); - expect(closeDropdown).toHaveBeenCalledWith( - `action-menu-dropdown-${actionMenuId}`, - ); - expect(openBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`); + expect(closeDropdown).toHaveBeenCalledWith(actionMenuDropdownId); + expect(openBottomBar).toHaveBeenCalledWith(actionBarId); }); it('should call the correct function when closing action menu dropdown', () => { @@ -66,9 +67,7 @@ describe('useActionMenu', () => { result.current.closeActionMenuDropdown(); }); - expect(closeDropdown).toHaveBeenCalledWith( - `action-menu-dropdown-${actionMenuId}`, - ); + expect(closeDropdown).toHaveBeenCalledWith(actionMenuDropdownId); }); it('should call the correct function when closing action bar', () => { @@ -78,6 +77,6 @@ describe('useActionMenu', () => { result.current.closeActionBar(); }); - expect(closeBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`); + expect(closeBottomBar).toHaveBeenCalledWith(actionBarId); }); }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useExportRecordData.test.ts similarity index 95% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts rename to packages/twenty-front/src/modules/action-menu/hooks/__tests__/useExportRecordData.test.ts index 0494fd32f023..34f9aab740c9 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useExportRecordData.test.ts @@ -7,7 +7,7 @@ import { displayedExportProgress, download, generateCsv, -} from '../useExportTableData'; +} from '../useExportRecordData'; jest.useFakeTimers(); @@ -86,7 +86,7 @@ describe('csvDownloader', () => { describe('displayedExportProgress', () => { it.each([ - [undefined, undefined, 'percentage', 'Export'], + [undefined, undefined, 'percentage', 'Export View as CSV'], [20, 50, 'percentage', 'Export (40%)'], [0, 100, 'number', 'Export (0)'], [10, 10, 'percentage', 'Export (100%)'], @@ -96,7 +96,7 @@ describe('displayedExportProgress', () => { 'displays the export progress', (exportedRecordCount, totalRecordCount, displayType, expected) => { expect( - displayedExportProgress({ + displayedExportProgress('all', { exportedRecordCount, totalRecordCount, displayType: displayType as 'percentage' | 'number', diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts b/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts index 881cadd694e1..109bd1ee5932 100644 --- a/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts @@ -1,3 +1,5 @@ +import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId'; +import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { useBottomBar } from '@/ui/layout/bottom-bar/hooks/useBottomBar'; import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; @@ -5,22 +7,26 @@ export const useActionMenu = (actionMenuId: string) => { const { openDropdown, closeDropdown } = useDropdownV2(); const { openBottomBar, closeBottomBar } = useBottomBar(); + const actionBarId = getActionBarIdFromActionMenuId(actionMenuId); + const actionMenuDropdownId = + getActionMenuDropdownIdFromActionMenuId(actionMenuId); + const openActionMenuDropdown = () => { - closeBottomBar(`action-bar-${actionMenuId}`); - openDropdown(`action-menu-dropdown-${actionMenuId}`); + closeBottomBar(actionBarId); + openDropdown(actionMenuDropdownId); }; const openActionBar = () => { - closeDropdown(`action-menu-dropdown-${actionMenuId}`); - openBottomBar(`action-bar-${actionMenuId}`); + closeDropdown(actionMenuDropdownId); + openBottomBar(actionBarId); }; const closeActionMenuDropdown = () => { - closeDropdown(`action-menu-dropdown-${actionMenuId}`); + closeDropdown(actionMenuDropdownId); }; const closeActionBar = () => { - closeBottomBar(`action-bar-${actionMenuId}`); + closeBottomBar(actionBarId); }; return { diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts b/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts similarity index 87% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts rename to packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts index 532b8e0aa59b..7ae40eb4d656 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts @@ -4,10 +4,11 @@ import { useMemo } from 'react'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; import { useProcessRecordsForCSVExport } from '@/object-record/record-index/options/hooks/useProcessRecordsForCSVExport'; + import { - useTableData, - UseTableDataOptions, -} from '@/object-record/record-index/options/hooks/useTableData'; + UseRecordDataOptions, + useRecordData, +} from '@/object-record/record-index/options/hooks/useRecordData'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; @@ -107,9 +108,12 @@ const percentage = (part: number, whole: number): number => { return Math.round((part / whole) * 100); }; -export const displayedExportProgress = (progress?: ExportProgress): string => { +export const displayedExportProgress = ( + mode: 'all' | 'selection' = 'all', + progress?: ExportProgress, +): string => { if (isUndefinedOrNull(progress?.exportedRecordCount)) { - return 'Export'; + return mode === 'all' ? 'Export View as CSV' : 'Export Selection as CSV'; } if ( @@ -134,21 +138,22 @@ const downloader = (mimeType: string, generator: GenerateExport) => { export const csvDownloader = downloader('text/csv', generateCsv); -type UseExportTableDataOptions = Omit & { +type UseExportTableDataOptions = Omit & { filename: string; }; -export const useExportTableData = ({ +export const useExportRecordData = ({ delayMs, filename, maximumRequests = 100, - objectNameSingular, + objectMetadataItem, pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, recordIndexId, viewType, }: UseExportTableDataOptions) => { - const { processRecordsForCSVExport } = - useProcessRecordsForCSVExport(objectNameSingular); + const { processRecordsForCSVExport } = useProcessRecordsForCSVExport( + objectMetadataItem.nameSingular, + ); const downloadCsv = useMemo( () => @@ -160,10 +165,10 @@ export const useExportTableData = ({ [filename, processRecordsForCSVExport], ); - const { getTableData: download, progress } = useTableData({ + const { getTableData: download, progress } = useRecordData({ delayMs, maximumRequests, - objectNameSingular, + objectMetadataItem, pageSize, recordIndexId, callback: downloadCsv, diff --git a/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentSelector.ts b/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentSelector.ts index 921f97b38f52..9a87deb49435 100644 --- a/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentSelector.ts +++ b/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentSelector.ts @@ -8,7 +8,7 @@ export const actionMenuEntriesComponentSelector = createComponentSelectorV2< ActionMenuEntry[] >({ key: 'actionMenuEntriesComponentSelector', - instanceContext: ActionMenuComponentInstanceContext, + componentInstanceContext: ActionMenuComponentInstanceContext, get: ({ instanceId }) => ({ get }) => diff --git a/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts b/packages/twenty-front/src/modules/action-menu/states/recordIndexActionMenuDropdownPositionComponentState.ts similarity index 67% rename from packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts rename to packages/twenty-front/src/modules/action-menu/states/recordIndexActionMenuDropdownPositionComponentState.ts index f2f8f06b1372..4be2f83c5bac 100644 --- a/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts +++ b/packages/twenty-front/src/modules/action-menu/states/recordIndexActionMenuDropdownPositionComponentState.ts @@ -1,9 +1,9 @@ import { PositionType } from '@/action-menu/types/PositionType'; import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -export const actionMenuDropdownPositionComponentState = +export const recordIndexActionMenuDropdownPositionComponentState = createComponentState({ - key: 'actionMenuDropdownPositionComponentState', + key: 'recordIndexActionMenuDropdownPositionComponentState', defaultValue: { x: null, y: null, diff --git a/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts index 4fe180955238..568bd3a33b83 100644 --- a/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts +++ b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts @@ -1,13 +1,13 @@ import { MouseEvent, ReactNode } from 'react'; -import { IconComponent } from 'twenty-ui'; - -import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; +import { IconComponent, MenuItemAccent } from 'twenty-ui'; export type ActionMenuEntry = { + type: 'standard' | 'workflow-run'; key: string; label: string; position: number; Icon: IconComponent; + isPinned?: boolean; accent?: MenuItemAccent; onClick?: (event?: MouseEvent) => void; ConfirmationModal?: ReactNode; diff --git a/packages/twenty-front/src/modules/action-menu/utils/__tests__/getActionBarIdFromActionMenuId.test.ts b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getActionBarIdFromActionMenuId.test.ts new file mode 100644 index 000000000000..bf9990909926 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getActionBarIdFromActionMenuId.test.ts @@ -0,0 +1,9 @@ +import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId'; + +describe('getActionBarIdFromActionMenuId', () => { + it('should return the correct action bar id', () => { + expect(getActionBarIdFromActionMenuId('action-menu-id')).toBe( + 'action-bar-action-menu-id', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/utils/__tests__/getActionMenuDropdownIdFromActionMenuId.test.ts b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getActionMenuDropdownIdFromActionMenuId.test.ts new file mode 100644 index 000000000000..55fdebed4812 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getActionMenuDropdownIdFromActionMenuId.test.ts @@ -0,0 +1,9 @@ +import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; + +describe('getActionMenuDropdownIdFromActionMenuId', () => { + it('should return the correct action menu dropdown id', () => { + expect(getActionMenuDropdownIdFromActionMenuId('action-menu-id')).toBe( + 'action-menu-dropdown-action-menu-id', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/utils/__tests__/getActionMenuIdFromRecordIndexId.test.ts b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getActionMenuIdFromRecordIndexId.test.ts new file mode 100644 index 000000000000..ff547aa6d6e6 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getActionMenuIdFromRecordIndexId.test.ts @@ -0,0 +1,9 @@ +import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; + +describe('getActionMenuIdFromRecordIndexId', () => { + it('should return the correct action menu id', () => { + expect(getActionMenuIdFromRecordIndexId('record-index-id')).toBe( + 'action-menu-record-index-record-index-id', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/utils/getActionBarIdFromActionMenuId.ts b/packages/twenty-front/src/modules/action-menu/utils/getActionBarIdFromActionMenuId.ts new file mode 100644 index 000000000000..2005c48308b5 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/utils/getActionBarIdFromActionMenuId.ts @@ -0,0 +1,3 @@ +export const getActionBarIdFromActionMenuId = (actionMenuId: string) => { + return `action-bar-${actionMenuId}`; +}; diff --git a/packages/twenty-front/src/modules/action-menu/utils/getActionMenuDropdownIdFromActionMenuId.ts b/packages/twenty-front/src/modules/action-menu/utils/getActionMenuDropdownIdFromActionMenuId.ts new file mode 100644 index 000000000000..40b21dbe6c84 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/utils/getActionMenuDropdownIdFromActionMenuId.ts @@ -0,0 +1,5 @@ +export const getActionMenuDropdownIdFromActionMenuId = ( + actionMenuId: string, +) => { + return `action-menu-dropdown-${actionMenuId}`; +}; diff --git a/packages/twenty-front/src/modules/action-menu/utils/getActionMenuIdFromRecordIndexId.ts b/packages/twenty-front/src/modules/action-menu/utils/getActionMenuIdFromRecordIndexId.ts new file mode 100644 index 000000000000..883beab4fd9e --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/utils/getActionMenuIdFromRecordIndexId.ts @@ -0,0 +1,3 @@ +export const getActionMenuIdFromRecordIndexId = (recordIndexId: string) => { + return `action-menu-record-index-${recordIndexId}`; +}; diff --git a/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx similarity index 92% rename from packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx rename to packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx index 680ac551f662..5c8de03869d5 100644 --- a/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx +++ b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx @@ -3,14 +3,14 @@ import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import { ChangeEvent, useRef } from 'react'; -import { Button } from '@/ui/input/button/components/Button'; import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; +import { Button } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -import { AttachmentIcon } from '../files/components/AttachmentIcon'; -import { AttachmentType } from '../files/types/Attachment'; -import { getFileType } from '../files/utils/getFileType'; +import { AttachmentIcon } from '../../files/components/AttachmentIcon'; +import { AttachmentType } from '../../files/types/Attachment'; +import { getFileType } from '../../files/utils/getFileType'; const StyledFileInput = styled.input` display: none; diff --git a/packages/twenty-front/src/modules/activities/blocks/schema.ts b/packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts similarity index 57% rename from packages/twenty-front/src/modules/activities/blocks/schema.ts rename to packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts index d6ea82eac19b..2584f3c7b8c5 100644 --- a/packages/twenty-front/src/modules/activities/blocks/schema.ts +++ b/packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts @@ -1,8 +1,8 @@ import { BlockNoteSchema, defaultBlockSpecs } from '@blocknote/core'; -import { FileBlock } from './FileBlock'; +import { FileBlock } from '../components/FileBlock'; -export const blockSchema = BlockNoteSchema.create({ +export const BLOCK_SCHEMA = BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, file: FileBlock, diff --git a/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx b/packages/twenty-front/src/modules/activities/blocks/utils/getSlashMenu.ts similarity index 90% rename from packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx rename to packages/twenty-front/src/modules/activities/blocks/utils/getSlashMenu.ts index 34a161bab747..760a778b57b2 100644 --- a/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx +++ b/packages/twenty-front/src/modules/activities/blocks/utils/getSlashMenu.ts @@ -18,7 +18,7 @@ import { import { SuggestionItem } from '@/ui/input/editor/components/CustomSlashMenu'; -import { blockSchema } from './schema'; +import { BLOCK_SCHEMA } from '../constants/Schema'; const Icons: Record = { 'Heading 1': IconH1, @@ -35,7 +35,7 @@ const Icons: Record = { Emoji: IconMoodSmile, }; -export const getSlashMenu = (editor: typeof blockSchema.BlockNoteEditor) => { +export const getSlashMenu = (editor: typeof BLOCK_SCHEMA.BlockNoteEditor) => { const items: SuggestionItem[] = [ ...getDefaultReactSlashMenuItems(editor).map((x) => ({ ...x, diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx index 360eccbbcc2a..0be4c731adda 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx @@ -1,27 +1,27 @@ import styled from '@emotion/styled'; import { format, getYear } from 'date-fns'; -import { H3Title } from 'twenty-ui'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, + H3Title, + Section, +} from 'twenty-ui'; import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard'; import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from '@/activities/calendar/constants/Calendar'; import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext'; +import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId'; +import { getTimelineCalendarEventsFromPersonId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId'; import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents'; -import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId'; -import { getTimelineCalendarEventsFromPersonId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromPersonId'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { - AnimatedPlaceholderEmptyContainer, - AnimatedPlaceholderEmptySubTitle, - AnimatedPlaceholderEmptyTextContainer, - AnimatedPlaceholderEmptyTitle, - EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; -import { Section } from '@/ui/layout/section/components/Section'; import { TimelineCalendarEventsWithTotal } from '~/generated/graphql'; const StyledContainer = styled.div` @@ -38,6 +38,10 @@ const StyledYear = styled.span` color: ${({ theme }) => theme.font.color.light}; `; +const StyledTitleContainer = styled.div` + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + export const Calendar = ({ targetableObject, }: { @@ -131,14 +135,16 @@ export const Calendar = ({ return (
- - {monthLabel} - {isLastMonthOfYear && {year}} - - } - /> + + + {monthLabel} + {isLastMonthOfYear && {year}} + + } + /> +
); diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx index ca656f3b52c1..1ccf037161a3 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx @@ -4,7 +4,7 @@ import { differenceInSeconds, endOfDay, format } from 'date-fns'; import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow'; import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; -import { CardContent } from '@/ui/layout/card/components/CardContent'; +import { CardContent } from 'twenty-ui'; import { TimelineCalendarEvent } from '~/generated/graphql'; type CalendarDayCardContentProps = { diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx index cb9548f8bafa..5874898e0040 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx @@ -9,6 +9,8 @@ import { IconArrowRight, IconLock, isDefined, + Card, + CardContent, } from 'twenty-ui'; import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor'; @@ -18,8 +20,6 @@ import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendar import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { Card } from '@/ui/layout/card/components/Card'; -import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CalendarChannelVisibility, TimelineCalendarEvent, diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx index f42f2e35f594..97ca1d0297e6 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarMonthCard.tsx @@ -2,7 +2,7 @@ import { useContext } from 'react'; import { CalendarDayCardContent } from '@/activities/calendar/components/CalendarDayCardContent'; import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext'; -import { Card } from '@/ui/layout/card/components/Card'; +import { Card } from 'twenty-ui'; type CalendarMonthCardProps = { dayTimes: number[]; diff --git a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx index b2732df86544..eb4aa38eda72 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx @@ -1,10 +1,10 @@ import { getOperationName } from '@apollo/client/utilities'; import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; +import { HttpResponse, graphql } from 'msw'; import { ComponentDecorator } from 'twenty-ui'; import { Calendar } from '@/activities/calendar/components/Calendar'; -import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId'; +import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventFragment.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventFragment.ts similarity index 84% rename from packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventFragment.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventFragment.ts index d98c7bcf81b2..eb152294ccea 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventFragment.ts +++ b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventFragment.ts @@ -1,7 +1,6 @@ +import { timelineCalendarEventParticipantFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventParticipantFragment'; import { gql } from '@apollo/client'; -import { timelineCalendarEventParticipantFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventParticipantFragment'; - export const timelineCalendarEventFragment = gql` fragment TimelineCalendarEventFragment on TimelineCalendarEvent { id diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventParticipantFragment.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventParticipantFragment.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventParticipantFragment.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventParticipantFragment.ts diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment.ts similarity index 86% rename from packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment.ts index 2a76f0f7fa41..58de733417c1 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment.ts +++ b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment.ts @@ -1,7 +1,6 @@ +import { timelineCalendarEventFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventFragment'; import { gql } from '@apollo/client'; -import { timelineCalendarEventFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventFragment'; - export const timelineCalendarEventWithTotalFragment = gql` fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal { totalNumberOfCalendarEvents diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId.ts similarity index 86% rename from packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId.ts index e454e67452f3..c43d197e43ff 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts +++ b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId.ts @@ -1,7 +1,6 @@ +import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment'; import { gql } from '@apollo/client'; -import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment'; - export const getTimelineCalendarEventsFromCompanyId = gql` query GetTimelineCalendarEventsFromCompanyId( $companyId: UUID! diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts similarity index 86% rename from packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts index 7d9f221fbc78..3285fb475d23 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts +++ b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts @@ -1,7 +1,6 @@ +import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment'; import { gql } from '@apollo/client'; -import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment'; - export const getTimelineCalendarEventsFromPersonId = gql` query GetTimelineCalendarEventsFromPersonId( $personId: UUID! diff --git a/packages/twenty-front/src/modules/activities/components/ActivityList.tsx b/packages/twenty-front/src/modules/activities/components/ActivityList.tsx index b8b8b2f61d5f..0bb0e367e5db 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityList.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityList.tsx @@ -1,5 +1,5 @@ -import { Card } from '@/ui/layout/card/components/Card'; import styled from '@emotion/styled'; +import { Card } from 'twenty-ui'; const StyledList = styled(Card)` & > :not(:last-child) { diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx index 00fdbb1a68f8..075758fc8208 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx @@ -1,5 +1,5 @@ -import { CardContent } from '@/ui/layout/card/components/CardContent'; import styled from '@emotion/styled'; +import { CardContent } from 'twenty-ui'; import React from 'react'; const StyledRowContent = styled(CardContent)<{ diff --git a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx index c6842395f64f..26dd4204e631 100644 --- a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx @@ -1,13 +1,12 @@ import { useApolloClient } from '@apollo/client'; import { useCreateBlockNote } from '@blocknote/react'; import { isArray, isNonEmptyString } from '@sniptt/guards'; -import { ClipboardEvent, useCallback, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useRecoilCallback, useRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; import { useDebouncedCallback } from 'use-debounce'; import { v4 } from 'uuid'; -import { blockSchema } from '@/activities/blocks/schema'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState'; import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; @@ -21,19 +20,16 @@ import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDraw import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey'; -import { FileFolder, useUploadFileMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; - -import { getFileType } from '../files/utils/getFileType'; +import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; +import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { Note } from '@/activities/types/Note'; import { Task } from '@/activities/types/Task'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import '@blocknote/core/fonts/inter.css'; import '@blocknote/mantine/style.css'; import '@blocknote/react/style.css'; -import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI'; type RichTextEditorProps = { activityId: string; @@ -121,22 +117,13 @@ export const RichTextEditor = ({ canCreateActivityState, ); - const [uploadFile] = useUploadFileMutation(); + const { uploadAttachmentFile } = useUploadAttachmentFile(); - const handleUploadAttachment = async (file: File): Promise => { - if (isUndefinedOrNull(file)) { - return ''; - } - const result = await uploadFile({ - variables: { - file, - fileFolder: FileFolder.Attachment, - }, + const handleUploadAttachment = async (file: File) => { + return await uploadAttachmentFile(file, { + id: activityId, + targetObjectNameSingular: activityObjectNameSingular, }); - if (!result?.data?.uploadFile) { - throw new Error("Couldn't upload Image"); - } - return getFileAbsoluteURI(result.data.uploadFile); }; const prepareBody = (newStringifiedBody: string) => { @@ -152,8 +139,6 @@ export const RichTextEditor = ({ const imageProps = block.props; const imageUrl = new URL(imageProps.url); - imageUrl.searchParams.delete('token'); - return { ...block, props: { @@ -284,65 +269,19 @@ export const RichTextEditor = ({ } }, [activity, activityBody]); + const handleEditorBuiltInUploadFile = async (file: File) => { + const { attachementAbsoluteURL } = await handleUploadAttachment(file); + + return attachementAbsoluteURL; + }; + const editor = useCreateBlockNote({ initialContent: initialBody, domAttributes: { editor: { class: 'editor' } }, - schema: blockSchema, - uploadFile: handleUploadAttachment, + schema: BLOCK_SCHEMA, + uploadFile: handleEditorBuiltInUploadFile, }); - const handleImagePaste = async (event: ClipboardEvent) => { - const clipboardItems = event.clipboardData?.items; - - if (isDefined(clipboardItems)) { - for (let i = 0; i < clipboardItems.length; i++) { - if (clipboardItems[i].kind === 'file') { - const isImage = clipboardItems[i].type.match('^image/'); - const pastedFile = clipboardItems[i].getAsFile(); - if (!pastedFile) { - return; - } - - const attachmentUrl = await handleUploadAttachment(pastedFile); - - if (!attachmentUrl) { - return; - } - - if (isDefined(isImage)) { - editor?.insertBlocks( - [ - { - type: 'image', - props: { - url: attachmentUrl, - }, - }, - ], - editor?.getTextCursorPosition().block, - 'after', - ); - } else { - editor?.insertBlocks( - [ - { - type: 'file', - props: { - url: attachmentUrl, - fileType: getFileType(pastedFile.name), - name: pastedFile.name, - }, - }, - ], - editor?.getTextCursorPosition().block, - 'after', - ); - } - } - } - } - }; - useScopedHotkeys( Key.Escape, () => { @@ -427,7 +366,6 @@ export const RichTextEditor = ({ diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx index f497344606eb..444685852b7c 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx @@ -1,10 +1,10 @@ -import { Loader } from '@/ui/feedback/loader/components/Loader'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; + Loader, +} from 'twenty-ui'; export const EmailLoader = ({ loadingText }: { loadingText?: string }) => ( diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadBottomBar.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadBottomBar.tsx index c010da2cd1fb..902039f45241 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadBottomBar.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadBottomBar.tsx @@ -1,7 +1,5 @@ import styled from '@emotion/styled'; -import { IconArrowBackUp, IconUserCircle } from 'twenty-ui'; - -import { Button } from '@/ui/input/button/components/Button'; +import { Button, IconArrowBackUp, IconUserCircle } from 'twenty-ui'; const StyledThreadBottomBar = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageBody.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageBody.tsx index 128197c6093d..b31fa54fe214 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageBody.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageBody.tsx @@ -1,8 +1,6 @@ -import React from 'react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; - -import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut'; +import { AnimatedEaseInOut } from 'twenty-ui'; const StyledThreadMessageBody = styled(motion.div)` color: ${({ theme }) => theme.font.color.primary}; diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx index 8a3eef7ea33f..38f150dcc30d 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx @@ -1,25 +1,26 @@ import styled from '@emotion/styled'; -import { H1Title, H1TitleFontColor } from 'twenty-ui'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, + H1Title, + H1TitleFontColor, + Section, +} from 'twenty-ui'; import { ActivityList } from '@/activities/components/ActivityList'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview'; import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/Messaging'; -import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId'; -import { getTimelineThreadsFromPersonId } from '@/activities/emails/queries/getTimelineThreadsFromPersonId'; +import { getTimelineThreadsFromCompanyId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId'; +import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromPersonId'; import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { - AnimatedPlaceholderEmptyContainer, - AnimatedPlaceholderEmptySubTitle, - AnimatedPlaceholderEmptyTextContainer, - AnimatedPlaceholderEmptyTitle, - EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; -import { Section } from '@/ui/layout/section/components/Section'; import { TimelineThread, TimelineThreadsWithTotal } from '~/generated/graphql'; const StyledContainer = styled.div` diff --git a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem.tsx b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem.tsx index b56cdc0809cb..04de8d7184c6 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem.tsx @@ -1,9 +1,8 @@ import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; -import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; -import { IconPlus } from 'twenty-ui'; +import { IconPlus, MenuItemAvatar } from 'twenty-ui'; export const MessageThreadSubscriberDropdownAddSubscriberMenuItem = ({ workspaceMember, diff --git a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersDropdownButton.tsx b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersDropdownButton.tsx index 7f7c42e7ae26..2fdf1d634fdd 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersDropdownButton.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersDropdownButton.tsx @@ -1,5 +1,5 @@ import { offset } from '@floating-ui/react'; -import { IconMinus, IconPlus } from 'twenty-ui'; +import { IconMinus, IconPlus, MenuItem, MenuItemAvatar } from 'twenty-ui'; import { MessageThreadSubscriberDropdownAddSubscriber } from '@/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriber'; import { MessageThreadSubscribersChip } from '@/activities/emails/components/MessageThreadSubscribersChip'; @@ -10,8 +10,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose'; -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar'; import { useState } from 'react'; export const MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID = diff --git a/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts diff --git a/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromPersonId.test.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromPersonId.test.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromPersonId.test.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromPersonId.test.ts diff --git a/packages/twenty-front/src/modules/activities/emails/queries/fragments/participantFragment.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/participantFragment.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/emails/queries/fragments/participantFragment.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/participantFragment.ts diff --git a/packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadFragment.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadFragment.ts similarity index 80% rename from packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadFragment.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadFragment.ts index d5728f23efaf..7d8f8ab9c351 100644 --- a/packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadFragment.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadFragment.ts @@ -1,6 +1,6 @@ import { gql } from '@apollo/client'; -import { participantFragment } from '@/activities/emails/queries/fragments/participantFragment'; +import { participantFragment } from '@/activities/emails/graphql/queries/fragments/participantFragment'; export const timelineThreadFragment = gql` fragment TimelineThreadFragment on TimelineThread { diff --git a/packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadWithTotalFragment.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment.ts similarity index 71% rename from packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadWithTotalFragment.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment.ts index 89dc76d19b11..b5a8f351dacb 100644 --- a/packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadWithTotalFragment.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment.ts @@ -1,7 +1,6 @@ +import { timelineThreadFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadFragment'; import { gql } from '@apollo/client'; -import { timelineThreadFragment } from '@/activities/emails/queries/fragments/timelineThreadFragment'; - export const timelineThreadWithTotalFragment = gql` fragment TimelineThreadsWithTotalFragment on TimelineThreadsWithTotal { totalNumberOfThreads diff --git a/packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromCompanyId.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts similarity index 88% rename from packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromCompanyId.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts index 589905550c7d..e999e676b94e 100644 --- a/packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromCompanyId.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts @@ -1,7 +1,6 @@ +import { timelineThreadWithTotalFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment'; import { gql } from '@apollo/client'; -import { timelineThreadWithTotalFragment } from '@/activities/emails/queries/fragments/timelineThreadWithTotalFragment'; - export const getTimelineThreadsFromCompanyId = gql` query GetTimelineThreadsFromCompanyId( $companyId: UUID! diff --git a/packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromPersonId.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts similarity index 87% rename from packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromPersonId.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts index 84cd7053791e..7f1877f112eb 100644 --- a/packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromPersonId.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts @@ -1,6 +1,6 @@ import { gql } from '@apollo/client'; -import { timelineThreadWithTotalFragment } from '@/activities/emails/queries/fragments/timelineThreadWithTotalFragment'; +import { timelineThreadWithTotalFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment'; export const getTimelineThreadsFromPersonId = gql` query GetTimelineThreadsFromPersonId( diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/IntermediaryMessages.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/IntermediaryMessages.tsx index 3ef96d57310e..aee31f1e1b46 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/IntermediaryMessages.tsx +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/IntermediaryMessages.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; import { useState } from 'react'; -import { IconArrowsVertical } from 'twenty-ui'; +import { Button, IconArrowsVertical } from 'twenty-ui'; import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage'; import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender'; -import { Button } from '@/ui/input/button/components/Button'; const StyledButtonContainer = styled.div` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx index 42367f2f9adf..1ac7fb65194c 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx @@ -9,12 +9,11 @@ import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMe import { IntermediaryMessages } from '@/activities/emails/right-drawer/components/IntermediaryMessages'; import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread'; import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState'; -import { Button } from '@/ui/input/button/components/Button'; import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { IconArrowBackUp } from 'twenty-ui'; +import { Button, IconArrowBackUp } from 'twenty-ui'; const StyledWrapper = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentDropdown.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentDropdown.tsx index 7dc80b171729..128b4a2057de 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentDropdown.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentDropdown.tsx @@ -3,14 +3,14 @@ import { IconDownload, IconPencil, IconTrash, + LightIconButton, + MenuItem, } from 'twenty-ui'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; type AttachmentDropdownProps = { onDownload: () => void; diff --git a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx index c0bf6908e207..1cc6de187a09 100644 --- a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx @@ -1,6 +1,15 @@ import styled from '@emotion/styled'; import { ChangeEvent, useRef, useState } from 'react'; -import { IconPlus } from 'twenty-ui'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + Button, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, + IconPlus, +} from 'twenty-ui'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { AttachmentList } from '@/activities/files/components/AttachmentList'; @@ -8,15 +17,6 @@ import { DropZone } from '@/activities/files/components/DropZone'; import { useAttachments } from '@/activities/files/hooks/useAttachments'; import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { Button } from '@/ui/input/button/components/Button'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { - AnimatedPlaceholderEmptyContainer, - AnimatedPlaceholderEmptySubTitle, - AnimatedPlaceholderEmptyTextContainer, - AnimatedPlaceholderEmptyTitle, - EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { isDefined } from '~/utils/isDefined'; const StyledAttachmentsContainer = styled.div` diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx index 301a324668a7..bbd8d6152af4 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx @@ -7,7 +7,9 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { isNonEmptyString } from '@sniptt/guards'; import { FileFolder, useUploadFileMutation } from '~/generated/graphql'; +import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI'; // Note: This is probably not the right way to do this. export const computePathWithoutToken = (attachmentPath: string): string => { @@ -36,8 +38,8 @@ export const useUploadAttachmentFile = () => { const attachmentPath = result?.data?.uploadFile; - if (!attachmentPath) { - return; + if (!isNonEmptyString(attachmentPath)) { + throw new Error("Couldn't upload the attachment."); } const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ @@ -55,6 +57,10 @@ export const useUploadAttachmentFile = () => { } as Partial; await createOneAttachment(attachmentToCreate); + + const attachementAbsoluteURL = getFileAbsoluteURI(attachmentPath); + + return { attachementAbsoluteURL }; }; return { uploadAttachmentFile }; diff --git a/packages/twenty-front/src/modules/activities/files/types/Attachment.ts b/packages/twenty-front/src/modules/activities/files/types/Attachment.ts index e37bcf8f92ad..20cb73984cf8 100644 --- a/packages/twenty-front/src/modules/activities/files/types/Attachment.ts +++ b/packages/twenty-front/src/modules/activities/files/types/Attachment.ts @@ -5,7 +5,6 @@ export type Attachment = { type: AttachmentType; companyId: string; personId: string; - activityId: string; authorId: string; createdAt: string; __typename: string; diff --git a/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory.ts b/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory.ts index f71407866c4f..b9e18aaf42fe 100644 --- a/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory.ts +++ b/packages/twenty-front/src/modules/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory.ts @@ -19,8 +19,6 @@ export const findActivityTargetsOperationSignatureFactory: RecordGqlOperationSig __typename: true, createdAt: true, updatedAt: true, - activity: true, - activityId: true, ...generateActivityTargetMorphFieldKeys(objectMetadataItems), }, }); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx index 23229f581696..10e2351fd5ae 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx @@ -18,7 +18,6 @@ const mockActivityTarget = { updatedAt: '2021-08-03T19:20:06.000Z', createdAt: '2021-08-03T19:20:06.000Z', personId: '1', - activityId: '234', companyId: '1', id: '123', }; diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx index baddb1029bda..8907d6db06e1 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx @@ -37,7 +37,6 @@ const mocks: MockedResponse[] = [ edges { node { __typename - activityId authorId companyId createdAt diff --git a/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts index e054014aa2be..e3295d57e991 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts @@ -1,7 +1,7 @@ import { useRecoilValue } from 'recoil'; import { usePrepareFindManyActivitiesQuery } from '@/activities/hooks/usePrepareFindManyActivitiesQuery'; -import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline-activities/states/objectShowPageTargetableObjectIdState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts index 4c1c6e2499fc..76e91ec8bf53 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts @@ -4,7 +4,7 @@ import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB' import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; -import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline-activities/states/objectShowPageTargetableObjectIdState'; import { Note } from '@/activities/types/Note'; import { Task } from '@/activities/types/Task'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx index 4ed47174a550..7719f4576bed 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx @@ -27,6 +27,7 @@ import { import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect'; +import { ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect'; import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect'; import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope'; import { prefillRecord } from '@/object-record/utils/prefillRecord'; @@ -185,10 +186,18 @@ export const ActivityTargetInlineCellEditMode = ({ activityObjectNameSingular === CoreObjectNameSingular.Task ? activity.id : null, + task: + activityObjectNameSingular === CoreObjectNameSingular.Task + ? activity + : null, noteId: activityObjectNameSingular === CoreObjectNameSingular.Note ? activity.id : null, + note: + activityObjectNameSingular === CoreObjectNameSingular.Note + ? activity + : null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), [fieldName]: record.record, @@ -279,6 +288,7 @@ export const ActivityTargetInlineCellEditMode = ({ + diff --git a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx index b26624e1a9d6..c99a8e57c5c7 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx @@ -1,21 +1,20 @@ -import styled from '@emotion/styled'; -import { IconPlus } from 'twenty-ui'; - import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { NoteList } from '@/activities/notes/components/NoteList'; import { useNotes } from '@/activities/notes/hooks/useNotes'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { Button } from '@/ui/input/button/components/Button'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; +import styled from '@emotion/styled'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + Button, EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; + IconPlus, +} from 'twenty-ui'; const StyledNotesContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts index 04aa231d4c84..1a8248543747 100644 --- a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts +++ b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts @@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil'; import { useActivities } from '@/activities/hooks/useActivities'; import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; -import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy'; +import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline-activities/constants/FindManyTimelineActivitiesOrderBy'; import { Note } from '@/activities/types/Note'; import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx b/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx index 869e2f1adef6..0004f351d669 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx @@ -1,10 +1,9 @@ import { isNonEmptyArray } from '@sniptt/guards'; -import { IconPlus } from 'twenty-ui'; +import { Button, IconPlus } from 'twenty-ui'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { Button } from '@/ui/input/button/components/Button'; export const AddTaskButton = ({ activityTargetableObjects, diff --git a/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx b/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx index 265780072a7d..a7168d35ec85 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx @@ -1,6 +1,6 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { PageAddButton } from '@/ui/layout/page/PageAddButton'; +import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; export const PageAddTaskButton = () => { const openCreateActivity = useOpenCreateActivityDrawer({ diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx index 16ebbec0f38a..b32d17ed6f5f 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx @@ -1,25 +1,24 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { IconPlus } from 'twenty-ui'; - -import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; -import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; -import { TASKS_TAB_LIST_COMPONENT_ID } from '@/activities/tasks/constants/TasksTabListComponentId'; -import { useTasks } from '@/activities/tasks/hooks/useTasks'; -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { Button } from '@/ui/input/button/components/Button'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + Button, EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; -import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; + IconPlus, +} from 'twenty-ui'; +import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; +import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; +import { TASKS_TAB_LIST_COMPONENT_ID } from '@/activities/tasks/constants/TasksTabListComponentId'; +import { useTasks } from '@/activities/tasks/hooks/useTasks'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { Task } from '@/activities/types/Task'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import groupBy from 'lodash.groupby'; import { AddTaskButton } from './AddTaskButton'; import { TaskList } from './TaskList'; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index ad46a8a43b15..90e11ab06448 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -1,11 +1,15 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui'; +import { + Checkbox, + CheckboxShape, + IconCalendar, + OverflowingTextWithTooltip, +} from 'twenty-ui'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { getActivitySummary } from '@/activities/utils/getActivitySummary'; -import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox'; import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils'; import { ActivityRow } from '@/activities/components/ActivityRow'; diff --git a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx b/packages/twenty-front/src/modules/activities/tasks/components/__stories__/TaskGroups.stories.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx rename to packages/twenty-front/src/modules/activities/tasks/components/__stories__/TaskGroups.stories.tsx diff --git a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx b/packages/twenty-front/src/modules/activities/tasks/components/__stories__/TaskList.stories.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx rename to packages/twenty-front/src/modules/activities/tasks/components/__stories__/TaskList.stories.tsx diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx b/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx index 814b72fb6ce1..f99668b33a0a 100644 --- a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx @@ -51,7 +51,6 @@ const mocks: MockedResponse[] = [ edges { node { __typename - activityId authorId companyId createdAt @@ -95,6 +94,8 @@ const mocks: MockedResponse[] = [ updatedAt viewId workflowId + workflowRunId + workflowVersionId workspaceMemberId } } @@ -138,6 +139,9 @@ const mocks: MockedResponse[] = [ rocketId taskId updatedAt + workflowId + workflowRunId + workflowVersionId workspaceMemberId } } diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts index ef210d328a25..2085284d130d 100644 --- a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts +++ b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts @@ -1,5 +1,5 @@ import { useActivities } from '@/activities/hooks/useActivities'; -import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy'; +import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline-activities/constants/FindManyTimelineActivitiesOrderBy'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { Task } from '@/activities/types/Task'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventList.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx similarity index 86% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/EventList.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx index bf82cc2a42da..86b053536d88 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventList.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx @@ -1,10 +1,10 @@ import styled from '@emotion/styled'; import { ReactElement } from 'react'; -import { EventsGroup } from '@/activities/timelineActivities/components/EventsGroup'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { filterOutInvalidTimelineActivities } from '@/activities/timelineActivities/utils/filterOutInvalidTimelineActivities'; -import { groupEventsByMonth } from '@/activities/timelineActivities/utils/groupEventsByMonth'; +import { EventsGroup } from '@/activities/timeline-activities/components/EventsGroup'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { filterOutInvalidTimelineActivities } from '@/activities/timeline-activities/utils/filterOutInvalidTimelineActivities'; +import { groupEventsByMonth } from '@/activities/timeline-activities/utils/groupEventsByMonth'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx similarity index 89% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx index e046316132df..e2cd91889021 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx @@ -2,13 +2,13 @@ import styled from '@emotion/styled'; import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; -import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext'; +import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext'; -import { useLinkedObjectObjectMetadataItem } from '@/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem'; -import { EventIconDynamicComponent } from '@/activities/timelineActivities/rows/components/EventIconDynamicComponent'; -import { EventRowDynamicComponent } from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { getTimelineActivityAuthorFullName } from '@/activities/timelineActivities/utils/getTimelineActivityAuthorFullName'; +import { useLinkedObjectObjectMetadataItem } from '@/activities/timeline-activities/hooks/useLinkedObjectObjectMetadataItem'; +import { EventIconDynamicComponent } from '@/activities/timeline-activities/rows/components/EventIconDynamicComponent'; +import { EventRowDynamicComponent } from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventsGroup.tsx similarity index 85% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/EventsGroup.tsx index 430968f4f259..590f5657c66c 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventsGroup.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; -import { EventRow } from '@/activities/timelineActivities/components/EventRow'; -import { EventGroup } from '@/activities/timelineActivities/utils/groupEventsByMonth'; +import { EventRow } from '@/activities/timeline-activities/components/EventRow'; +import { EventGroup } from '@/activities/timeline-activities/utils/groupEventsByMonth'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; type EventsGroupProps = { @@ -20,8 +20,8 @@ const StyledActivityGroup = styled.div` `; const StyledActivityGroupContainer = styled.div` - padding-bottom: ${({ theme }) => theme.spacing(2)}; - padding-top: ${({ theme }) => theme.spacing(2)}; + margin-bottom: ${({ theme }) => theme.spacing(3)}; + margin-top: ${({ theme }) => theme.spacing(3)}; position: relative; `; @@ -29,7 +29,7 @@ const StyledActivityGroupBar = styled.div` align-items: center; background: ${({ theme }) => theme.background.secondary}; border: 1px solid ${({ theme }) => theme.border.color.light}; - border-radius: ${({ theme }) => theme.border.radius.xl}; + border-radius: ${({ theme }) => theme.border.radius.md}; display: flex; flex-direction: column; height: 100%; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx similarity index 86% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx index bbda464681f9..a0f8bf327f4d 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx @@ -2,19 +2,19 @@ import styled from '@emotion/styled'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; -import { EventList } from '@/activities/timelineActivities/components/EventList'; -import { TimelineCreateButtonGroup } from '@/activities/timelineActivities/components/TimelineCreateButtonGroup'; -import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities'; +import { EventList } from '@/activities/timeline-activities/components/EventList'; +import { TimelineCreateButtonGroup } from '@/activities/timeline-activities/components/TimelineCreateButtonGroup'; +import { useTimelineActivities } from '@/activities/timeline-activities/hooks/useTimelineActivities'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +} from 'twenty-ui'; const StyledMainContainer = styled.div` align-items: flex-start; @@ -46,7 +46,7 @@ export const TimelineActivities = ({ const isTimelineActivitiesEmpty = !timelineActivities || timelineActivities.length === 0; - if (loading) { + if (loading === true) { return ; } diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineCreateButtonGroup.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCreateButtonGroup.tsx similarity index 79% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineCreateButtonGroup.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCreateButtonGroup.tsx index 2889dfc77c0e..e5bc090bc278 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineCreateButtonGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCreateButtonGroup.tsx @@ -1,10 +1,13 @@ -import { useSetRecoilState } from 'recoil'; -import { IconCheckbox, IconNotes, IconPaperclip } from 'twenty-ui'; - -import { Button } from '@/ui/input/button/components/Button'; -import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup'; -import { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageRightContainer'; +import { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageSubContainer'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; +import { useSetRecoilState } from 'recoil'; +import { + Button, + ButtonGroup, + IconCheckbox, + IconNotes, + IconPaperclip, +} from 'twenty-ui'; export const TimelineCreateButtonGroup = ({ isInRightDrawer = false, diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/__stories__/TimelineActivities.stories.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx similarity index 89% rename from packages/twenty-front/src/modules/activities/timelineActivities/__stories__/TimelineActivities.stories.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx index d04d8281c761..7c16632eea75 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/__stories__/TimelineActivities.stories.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx @@ -1,9 +1,9 @@ import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; +import { HttpResponse, graphql } from 'msw'; import { ComponentDecorator } from 'twenty-ui'; -import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities'; -import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext'; +import { TimelineActivities } from '@/activities/timeline-activities/components/TimelineActivities'; +import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { mockedTimelineActivities } from '~/testing/mock-data/timeline-activities'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy.ts b/packages/twenty-front/src/modules/activities/timeline-activities/constants/FindManyTimelineActivitiesOrderBy.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/constants/FindManyTimelineActivitiesOrderBy.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/contexts/TimelineActivityContext.ts b/packages/twenty-front/src/modules/activities/timeline-activities/contexts/TimelineActivityContext.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/contexts/TimelineActivityContext.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/contexts/TimelineActivityContext.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/__tests__/useTimelineActivities.test.tsx similarity index 95% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/hooks/__tests__/useTimelineActivities.test.tsx index 2d1989cc68ea..158b628bee6a 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/__tests__/useTimelineActivities.test.tsx @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react'; -import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities'; +import { useTimelineActivities } from '@/activities/timeline-activities/hooks/useTimelineActivities'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; jest.mock('@/object-record/hooks/useFindManyRecords', () => ({ diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem.ts b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useLinkedObjectObjectMetadataItem.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/hooks/useLinkedObjectObjectMetadataItem.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectsTitle.ts b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useLinkedObjectsTitle.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectsTitle.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/hooks/useLinkedObjectsTitle.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts similarity index 90% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts index fb65053c151c..96c00233e6d8 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts @@ -1,5 +1,5 @@ -import { useLinkedObjectsTitle } from '@/activities/timelineActivities/hooks/useLinkedObjectsTitle'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { useLinkedObjectsTitle } from '@/activities/timeline-activities/hooks/useLinkedObjectsTitle'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx similarity index 96% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx index 1c1f34e43ade..24322ac7c598 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx @@ -5,7 +5,7 @@ import { EventRowDynamicComponentProps, StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { isNonEmptyString } from '@sniptt/guards'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventRowCalendarEvent.tsx similarity index 77% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventRowCalendarEvent.tsx index c1ffd1094ef3..a4172b8f4a90 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventRowCalendarEvent.tsx @@ -1,14 +1,14 @@ -import { useState } from 'react'; import styled from '@emotion/styled'; +import { useState } from 'react'; -import { EventCardCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent'; -import { EventCard } from '@/activities/timelineActivities/rows/components/EventCard'; -import { EventCardToggleButton } from '@/activities/timelineActivities/rows/components/EventCardToggleButton'; +import { EventCardCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent'; +import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard'; +import { EventCardToggleButton } from '@/activities/timeline-activities/rows/components/EventCardToggleButton'; import { EventRowDynamicComponentProps, StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; type EventRowCalendarEventProps = EventRowDynamicComponentProps; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx similarity index 90% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx index b0c3fd7ca54c..8e8f4b3aaa0b 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx @@ -1,8 +1,8 @@ import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; +import { HttpResponse, graphql } from 'msw'; import { ComponentDecorator } from 'twenty-ui'; -import { EventCardCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent'; +import { EventCardCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCard.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCard.tsx similarity index 94% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCard.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCard.tsx index 67e4f78cd726..52b8fa0de84e 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCard.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCard.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; -import { Card } from '@/ui/layout/card/components/Card'; +import { Card } from 'twenty-ui'; type EventCardProps = { children: React.ReactNode; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCardToggleButton.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCardToggleButton.tsx similarity index 81% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCardToggleButton.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCardToggleButton.tsx index 44a2cb4066a2..2e0795845dd0 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCardToggleButton.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCardToggleButton.tsx @@ -1,7 +1,5 @@ import styled from '@emotion/styled'; -import { IconChevronDown, IconChevronUp } from 'twenty-ui'; - -import { IconButton } from '@/ui/input/button/components/IconButton'; +import { IconButton, IconChevronDown, IconChevronUp } from 'twenty-ui'; type EventCardToggleButtonProps = { isOpen: boolean; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx similarity index 88% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx index ecb7bc90d51f..6e4b49b8feea 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx @@ -1,6 +1,6 @@ import { IconCirclePlus, IconEditCircle, IconTrash, useIcons } from 'twenty-ui'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; export const EventIconDynamicComponent = ({ diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventRowDynamicComponent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx similarity index 76% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventRowDynamicComponent.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx index e5542cd35480..e51556decdb9 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventRowDynamicComponent.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx @@ -1,10 +1,10 @@ import styled from '@emotion/styled'; -import { EventRowActivity } from '@/activities/timelineActivities/rows/activity/components/EventRowActivity'; -import { EventRowCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent'; -import { EventRowMainObject } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObject'; -import { EventRowMessage } from '@/activities/timelineActivities/rows/message/components/EventRowMessage'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { EventRowActivity } from '@/activities/timeline-activities/rows/activity/components/EventRowActivity'; +import { EventRowCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventRowCalendarEvent'; +import { EventRowMainObject } from '@/activities/timeline-activities/rows/main-object/components/EventRowMainObject'; +import { EventRowMessage } from '@/activities/timeline-activities/rows/message/components/EventRowMessage'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -35,9 +35,7 @@ export const EventRowDynamicComponent = ({ linkedObjectMetadataItem, authorFullName, }: EventRowDynamicComponentProps) => { - const [eventName] = event.name.split('.'); - - switch (eventName) { + switch (linkedObjectMetadataItem?.nameSingular) { case 'calendarEvent': return ( ); - case 'linked-task': + case 'task': return ( ); - case 'linked-note': + case 'note': return ( ); - case mainObjectMetadataItem?.nameSingular: + default: return ( ); - default: - throw new Error( - `Cannot find event component for event name ${eventName}`, - ); } }; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiff.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiff.tsx similarity index 88% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiff.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiff.tsx index 94726465f201..f1a50f9128e7 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiff.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiff.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; -import { EventFieldDiffLabel } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffLabel'; -import { EventFieldDiffValue } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffValue'; -import { EventFieldDiffValueEffect } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffValueEffect'; +import { EventFieldDiffLabel } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiffLabel'; +import { EventFieldDiffValue } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue'; +import { EventFieldDiffValueEffect } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffContainer.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffContainer.tsx similarity index 91% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffContainer.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffContainer.tsx index 3b4cf60396f8..3a5b36ee6f44 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffContainer.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffContainer.tsx @@ -1,4 +1,4 @@ -import { EventFieldDiff } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiff'; +import { EventFieldDiff } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiff'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffLabel.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffLabel.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffLabel.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffLabel.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffValue.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffValue.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffValueEffect.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffValueEffect.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx similarity index 91% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx index 053e9217bb66..448879073a1b 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx @@ -1,11 +1,10 @@ -import styled from '@emotion/styled'; - import { EventRowDynamicComponentProps, StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; -import { EventRowMainObjectUpdated } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; +import { EventRowMainObjectUpdated } from '@/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated'; +import styled from '@emotion/styled'; type EventRowMainObjectProps = EventRowDynamicComponentProps; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated.tsx similarity index 84% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated.tsx index 30e6343bd708..cc2b7102e0d1 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated.tsx @@ -1,14 +1,14 @@ -import { useState } from 'react'; import styled from '@emotion/styled'; +import { useState } from 'react'; -import { EventCard } from '@/activities/timelineActivities/rows/components/EventCard'; -import { EventCardToggleButton } from '@/activities/timelineActivities/rows/components/EventCardToggleButton'; +import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard'; +import { EventCardToggleButton } from '@/activities/timeline-activities/rows/components/EventCardToggleButton'; import { StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; -import { EventFieldDiffContainer } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffContainer'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; +import { EventFieldDiffContainer } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiffContainer'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx similarity index 90% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx index fe0b549d68da..d4a12a465643 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx @@ -1,8 +1,8 @@ +import { EventRowMainObjectUpdated } from '@/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator, RouterDecorator } from 'twenty-ui'; -import { EventRowMainObjectUpdated } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessage.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx similarity index 98% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessage.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx index 899c0414e76b..7114bd427667 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessage.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx @@ -4,7 +4,7 @@ import { OverflowingTextWithTooltip } from 'twenty-ui'; import { useEmailThread } from '@/activities/emails/hooks/useEmailThread'; import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage'; -import { EventCardMessageNotShared } from '@/activities/timelineActivities/rows/message/components/EventCardMessageNotShared'; +import { EventCardMessageNotShared } from '@/activities/timeline-activities/rows/message/components/EventCardMessageNotShared'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessageNotShared.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessageNotShared.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessageNotShared.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessageNotShared.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventRowMessage.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventRowMessage.tsx similarity index 79% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventRowMessage.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventRowMessage.tsx index 83513994517f..00bd68e93a2a 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventRowMessage.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventRowMessage.tsx @@ -1,14 +1,14 @@ -import { useState } from 'react'; import styled from '@emotion/styled'; +import { useState } from 'react'; -import { EventCard } from '@/activities/timelineActivities/rows/components/EventCard'; -import { EventCardToggleButton } from '@/activities/timelineActivities/rows/components/EventCardToggleButton'; +import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard'; +import { EventCardToggleButton } from '@/activities/timeline-activities/rows/components/EventCardToggleButton'; import { EventRowDynamicComponentProps, StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; -import { EventCardMessage } from '@/activities/timelineActivities/rows/message/components/EventCardMessage'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; +import { EventCardMessage } from '@/activities/timeline-activities/rows/message/components/EventCardMessage'; type EventRowMessageProps = EventRowDynamicComponentProps; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/__stories__/EventCardMessage.stories.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/__stories__/EventCardMessage.stories.tsx similarity index 87% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/__stories__/EventCardMessage.stories.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/__stories__/EventCardMessage.stories.tsx index 3e8e08cd06dc..40d27298fd5d 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/__stories__/EventCardMessage.stories.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/__stories__/EventCardMessage.stories.tsx @@ -1,9 +1,9 @@ import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; +import { HttpResponse, graphql } from 'msw'; import { ComponentDecorator } from 'twenty-ui'; -import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext'; -import { EventCardMessage } from '@/activities/timelineActivities/rows/message/components/EventCardMessage'; +import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext'; +import { EventCardMessage } from '@/activities/timeline-activities/rows/message/components/EventCardMessage'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/states/objectShowPageTargetableObjectIdState.ts b/packages/twenty-front/src/modules/activities/timeline-activities/states/objectShowPageTargetableObjectIdState.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/states/objectShowPageTargetableObjectIdState.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/states/objectShowPageTargetableObjectIdState.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivity.ts b/packages/twenty-front/src/modules/activities/timeline-activities/types/TimelineActivity.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivity.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/types/TimelineActivity.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivityLinkedObject.ts b/packages/twenty-front/src/modules/activities/timeline-activities/types/TimelineActivityLinkedObject.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivityLinkedObject.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/types/TimelineActivityLinkedObject.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts similarity index 96% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts index 1dc11441002b..a685d15055c5 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts @@ -1,5 +1,5 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { filterOutInvalidTimelineActivities } from '@/activities/timelineActivities/utils/filterOutInvalidTimelineActivities'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { filterOutInvalidTimelineActivities } from '@/activities/timeline-activities/utils/filterOutInvalidTimelineActivities'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts similarity index 91% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts index 85ac636ea096..7b3d817c64e0 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts @@ -1,5 +1,5 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { getTimelineActivityAuthorFullName } from '@/activities/timelineActivities/utils/getTimelineActivityAuthorFullName'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName'; import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; describe('getTimelineActivityAuthorFullName', () => { diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/groupEventsByMonth.test.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/groupEventsByMonth.test.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/groupEventsByMonth.test.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/groupEventsByMonth.test.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterOutInvalidTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts similarity index 95% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/filterOutInvalidTimelineActivities.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts index 5613db9d48b0..96413c89cd4e 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterOutInvalidTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts @@ -1,4 +1,4 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterTimelineActivityByLinkedObjectTypes.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterTimelineActivityByLinkedObjectTypes.ts similarity index 75% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/filterTimelineActivityByLinkedObjectTypes.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/filterTimelineActivityByLinkedObjectTypes.ts index 455ceca01c0a..781e85d8b67e 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterTimelineActivityByLinkedObjectTypes.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterTimelineActivityByLinkedObjectTypes.ts @@ -1,5 +1,5 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { TimelineActivityLinkedObject } from '@/activities/timelineActivities/types/TimelineActivityLinkedObject'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { TimelineActivityLinkedObject } from '@/activities/timeline-activities/types/TimelineActivityLinkedObject'; export const filterTimelineActivityByLinkedObjectTypes = (linkedObjectTypes: TimelineActivityLinkedObject[]) => diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/getTimelineActivityAuthorFullName.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/getTimelineActivityAuthorFullName.ts similarity index 84% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/getTimelineActivityAuthorFullName.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/getTimelineActivityAuthorFullName.ts index e97b27fa9450..4e141de6b3bd 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/getTimelineActivityAuthorFullName.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/getTimelineActivityAuthorFullName.ts @@ -1,4 +1,4 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/groupEventsByMonth.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/groupEventsByMonth.ts similarity index 89% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/groupEventsByMonth.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/groupEventsByMonth.ts index fa0779f538c3..cd5ce8a73364 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/groupEventsByMonth.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/groupEventsByMonth.ts @@ -1,4 +1,4 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { isDefined } from '~/utils/isDefined'; export type EventGroup = { diff --git a/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts b/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts index 04a5d4487293..44828ddaefb5 100644 --- a/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts +++ b/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts @@ -1,8 +1,8 @@ import { gql } from '@apollo/client'; export const TRACK = gql` - mutation Track($type: String!, $sessionId: String!, $data: JSON!) { - track(type: $type, sessionId: $sessionId, data: $data) { + mutation Track($action: String!, $payload: JSON!) { + track(action: $action, payload: $payload) { success } } diff --git a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts index b70cbdd17cb8..104364957cda 100644 --- a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts +++ b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts @@ -10,12 +10,12 @@ import { previousUrlState } from '@/auth/states/previousUrlState'; import { tokenPairState } from '@/auth/states/tokenPairState'; import { workspacesState } from '@/auth/states/workspaces'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; -import { AppPath } from '@/types/AppPath'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useUpdateEffect } from '~/hooks/useUpdateEffect'; import { isDefined } from '~/utils/isDefined'; +import { AppPath } from '@/types/AppPath'; import { ApolloFactory, Options } from '../services/apollo.factory'; export const useApolloFactory = (options: Partial> = {}) => { diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index 9136b83fcd04..c50e93b46f83 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -41,8 +41,8 @@ const makeRequest = async () => { await client.mutate({ mutation: gql` - mutation Track($type: String!, $sessionId: String!, $data: JSON!) { - track(type: $type, sessionId: $sessionId, data: $data) { + mutation Track($action: String!, $payload: JSON!) { + track(action: $action, payload: $payload) { success } } diff --git a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts index 65f69f6d290e..6eca2c821fc6 100644 --- a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts +++ b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts @@ -18,7 +18,7 @@ import { logDebug } from '~/utils/logDebug'; import { GraphQLFormattedError } from 'graphql'; import { ApolloManager } from '../types/apolloManager.interface'; -import { loggerLink } from '../utils'; +import { loggerLink } from '../utils/loggerLink'; const logger = loggerLink(() => 'Twenty'); diff --git a/packages/twenty-front/src/modules/apollo/utils/__tests__/format-title.test.ts b/packages/twenty-front/src/modules/apollo/utils/__tests__/formatTitle.test.ts similarity index 91% rename from packages/twenty-front/src/modules/apollo/utils/__tests__/format-title.test.ts rename to packages/twenty-front/src/modules/apollo/utils/__tests__/formatTitle.test.ts index 39773acb8dda..47d8dc2ae8e2 100644 --- a/packages/twenty-front/src/modules/apollo/utils/__tests__/format-title.test.ts +++ b/packages/twenty-front/src/modules/apollo/utils/__tests__/formatTitle.test.ts @@ -2,7 +2,7 @@ import { expect } from '@storybook/test'; import { OperationType } from '@/apollo/types/operation-type'; -import formatTitle from '../format-title'; +import formatTitle from '../formatTitle'; describe('formatTitle', () => { it('should correctly format the title', () => { diff --git a/packages/twenty-front/src/modules/apollo/utils/__tests__/utils.test.ts b/packages/twenty-front/src/modules/apollo/utils/__tests__/utils.test.ts deleted file mode 100644 index 0d87baac9971..000000000000 --- a/packages/twenty-front/src/modules/apollo/utils/__tests__/utils.test.ts +++ /dev/null @@ -1,4 +0,0 @@ -// More work needed here -describe.skip('loggerLink', () => { - it('should log the correct message', () => {}); -}); diff --git a/packages/twenty-front/src/modules/apollo/utils/format-title.ts b/packages/twenty-front/src/modules/apollo/utils/formatTitle.ts similarity index 100% rename from packages/twenty-front/src/modules/apollo/utils/format-title.ts rename to packages/twenty-front/src/modules/apollo/utils/formatTitle.ts diff --git a/packages/twenty-front/src/modules/apollo/utils/index.ts b/packages/twenty-front/src/modules/apollo/utils/loggerLink.ts similarity index 98% rename from packages/twenty-front/src/modules/apollo/utils/index.ts rename to packages/twenty-front/src/modules/apollo/utils/loggerLink.ts index b57f427cf192..174c5c3badda 100644 --- a/packages/twenty-front/src/modules/apollo/utils/index.ts +++ b/packages/twenty-front/src/modules/apollo/utils/loggerLink.ts @@ -4,7 +4,7 @@ import { isDefined } from '~/utils/isDefined'; import { logDebug } from '~/utils/logDebug'; import { logError } from '~/utils/logError'; -import formatTitle from './format-title'; +import formatTitle from './formatTitle'; const getGroup = (collapsed: boolean) => collapsed diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx index d8985e676332..45aa98098643 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouter.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx @@ -1,4 +1,4 @@ -import { createAppRouter } from '@/app/utils/createAppRouter'; +import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter'; import { billingState } from '@/client-config/states/billingState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { RouterProvider } from 'react-router-dom'; @@ -8,6 +8,7 @@ export const AppRouter = () => { const billing = useRecoilValue(billingState); const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED'); const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED'); + const isSSOEnabled = useIsFeatureEnabled('IS_SSO_ENABLED'); const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled( 'IS_FUNCTION_SETTINGS_ENABLED', ); @@ -17,10 +18,11 @@ export const AppRouter = () => { return ( ); diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx index e5a24da4057a..7f64849a27e0 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx @@ -1,6 +1,5 @@ import { ApolloProvider } from '@/apollo/components/ApolloProvider'; -import { CommandMenuEffect } from '@/app/effect-components/CommandMenuEffect'; -import { GotoHotkeys } from '@/app/effect-components/GotoHotkeysEffect'; +import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider'; import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect'; import { AuthProvider } from '@/auth/components/AuthProvider'; import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect'; @@ -15,7 +14,7 @@ import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogMan import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; -import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; import { UserProvider } from '@/users/components/UserProvider'; import { UserProviderEffect } from '@/users/components/UserProviderEffect'; import { StrictMode } from 'react'; @@ -44,8 +43,7 @@ export const AppRouterProviders = () => { - - + diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 5eede03959b5..b758acdc1177 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -143,12 +143,6 @@ const SettingsDevelopers = lazy(() => })), ); -const SettingsObjectEdit = lazy(() => - import('~/pages/settings/data-model/SettingsObjectEdit').then((module) => ({ - default: module.SettingsObjectEdit, - })), -); - const SettingsIntegrations = lazy(() => import('~/pages/settings/integrations/SettingsIntegrations').then( (module) => ({ @@ -234,16 +228,32 @@ const SettingsCRMMigration = lazy(() => ), ); +const SettingsSecurity = lazy(() => + import('~/pages/settings/security/SettingsSecurity').then((module) => ({ + default: module.SettingsSecurity, + })), +); + +const SettingsSecuritySSOIdentifyProvider = lazy(() => + import('~/pages/settings/security/SettingsSecuritySSOIdentifyProvider').then( + (module) => ({ + default: module.SettingsSecuritySSOIdentifyProvider, + }), + ), +); + type SettingsRoutesProps = { isBillingEnabled?: boolean; isCRMMigrationEnabled?: boolean; isServerlessFunctionSettingsEnabled?: boolean; + isSSOEnabled?: boolean; }; export const SettingsRoutes = ({ isBillingEnabled, isCRMMigrationEnabled, isServerlessFunctionSettingsEnabled, + isSSOEnabled, }: SettingsRoutesProps) => ( }> @@ -276,7 +286,6 @@ export const SettingsRoutes = ({ path={SettingsPath.ObjectDetail} element={} /> - } /> } /> } /> {isCRMMigrationEnabled && ( @@ -357,6 +366,15 @@ export const SettingsRoutes = ({ element={} /> } /> + {isSSOEnabled && ( + <> + } /> + } + /> + + )} ); diff --git a/packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx deleted file mode 100644 index b210ae724276..000000000000 --- a/packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; - -import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands'; -import { commandMenuCommandsState } from '@/command-menu/states/commandMenuCommandsState'; - -export const CommandMenuEffect = () => { - const setCommands = useSetRecoilState(commandMenuCommandsState); - - const commands = Object.values(COMMAND_MENU_COMMANDS); - useEffect(() => { - setCommands(commands); - }, [commands, setCommands]); - - return <>; -}; diff --git a/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx index a0b545302501..d6f9f70d7a72 100644 --- a/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx @@ -6,7 +6,7 @@ export const GoToHotkeyItemEffect = (props: { }) => { const { hotkey, pathToNavigateTo } = props; - useGoToHotkeys(hotkey, pathToNavigateTo); + useGoToHotkeys({ key: hotkey, location: pathToNavigateTo }); return <>; }; diff --git a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx deleted file mode 100644 index 15d371f9f44a..000000000000 --- a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { GoToHotkeyItemEffect } from '@/app/effect-components/GoToHotkeyItemEffect'; -import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems'; -import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys'; - -export const GotoHotkeys = () => { - const { nonSystemActiveObjectMetadataItems } = - useNonSystemActiveObjectMetadataItems(); - - // Hardcoded since settings is static - useGoToHotkeys('s', '/settings/profile'); - - return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => ( - - )); -}; diff --git a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffectsProvider.tsx b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffectsProvider.tsx new file mode 100644 index 000000000000..2cc96f5158e4 --- /dev/null +++ b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffectsProvider.tsx @@ -0,0 +1,43 @@ +import { GoToHotkeyItemEffect } from '@/app/effect-components/GoToHotkeyItemEffect'; +import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; +import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys'; +import { useLocation } from 'react-router-dom'; +import { useRecoilCallback } from 'recoil'; + +export const GotoHotkeysEffectsProvider = () => { + const { nonSystemActiveObjectMetadataItems } = + useNonSystemActiveObjectMetadataItems(); + + const location = useLocation(); + + useGoToHotkeys({ + key: 's', + location: '/settings/profile', + preNavigateFunction: useRecoilCallback( + ({ set }) => + () => { + set(isNavigationDrawerExpandedState, true); + set(navigationDrawerExpandedMemorizedState, true); + set(navigationMemorizedUrlState, location.pathname + location.search); + }, + [location.pathname, location.search], + ), + }); + + return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => { + if (!objectMetadataItem.shortcut) { + return null; + } + + return ( + + ); + }); +}; diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index a8b05f4c0904..3af2f15ada51 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { IconCheckbox } from 'twenty-ui'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { @@ -21,6 +20,7 @@ import { AppPath } from '@/types/AppPath'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { SettingsPath } from '@/types/SettingsPath'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { IconCheckbox } from 'twenty-ui'; import { useCleanRecoilState } from '~/hooks/useCleanRecoilState'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; @@ -115,7 +115,7 @@ export const PageChangeEffect = () => { break; } case isMatchingLocation(AppPath.CreateWorkspace): { - setHotkeyScope(PageHotkeyScope.CreateWokspace); + setHotkeyScope(PageHotkeyScope.CreateWorkspace); break; } case isMatchingLocation(AppPath.SyncEmails): { diff --git a/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx similarity index 93% rename from packages/twenty-front/src/modules/app/utils/createAppRouter.tsx rename to packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx index 0ddb70ac1a34..0aa19e6e16cb 100644 --- a/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx +++ b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx @@ -3,8 +3,8 @@ import { SettingsRoutes } from '@/app/components/SettingsRoutes'; import { VerifyEffect } from '@/auth/components/VerifyEffect'; import indexAppPath from '@/navigation/utils/indexAppPath'; import { AppPath } from '@/types/AppPath'; -import { BlankLayout } from '@/ui/layout/page/BlankLayout'; -import { DefaultLayout } from '@/ui/layout/page/DefaultLayout'; +import { BlankLayout } from '@/ui/layout/page/components/BlankLayout'; +import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout'; import { createBrowserRouter, createRoutesFromElements, @@ -25,10 +25,11 @@ import { InviteTeam } from '~/pages/onboarding/InviteTeam'; import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess'; import { SyncEmails } from '~/pages/onboarding/SyncEmails'; -export const createAppRouter = ( +export const useCreateAppRouter = ( isBillingEnabled?: boolean, isCRMMigrationEnabled?: boolean, isServerlessFunctionSettingsEnabled?: boolean, + isSSOEnabled?: boolean, ) => createBrowserRouter( createRoutesFromElements( @@ -65,6 +66,7 @@ export const createAppRouter = ( isServerlessFunctionSettingsEnabled={ isServerlessFunctionSettingsEnabled } + isSSOEnabled={isSSOEnabled} /> } /> diff --git a/packages/twenty-front/src/modules/auth/components/Title.tsx b/packages/twenty-front/src/modules/auth/components/Title.tsx index d0a496b252c2..7f87362b1d75 100644 --- a/packages/twenty-front/src/modules/auth/components/Title.tsx +++ b/packages/twenty-front/src/modules/auth/components/Title.tsx @@ -1,7 +1,6 @@ -import React from 'react'; import styled from '@emotion/styled'; - -import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; +import React from 'react'; +import { AnimatedEaseIn } from 'twenty-ui'; type TitleProps = React.PropsWithChildren & { animate?: boolean; diff --git a/packages/twenty-front/src/modules/auth/graphql/fragments/availableSSOIdentityProvidersFragment.ts b/packages/twenty-front/src/modules/auth/graphql/fragments/availableSSOIdentityProvidersFragment.ts new file mode 100644 index 000000000000..45bc6c944cbe --- /dev/null +++ b/packages/twenty-front/src/modules/auth/graphql/fragments/availableSSOIdentityProvidersFragment.ts @@ -0,0 +1,16 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const AVAILABLE_SSO_IDENTITY_PROVIDERS_FRAGMENT = gql` + fragment AvailableSSOIdentityProvidersFragment on FindAvailableSSOIDPOutput { + id + issuer + name + status + workspace { + id + displayName + } + } +`; diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/findAvailableSSOIdentityProviders.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/findAvailableSSOIdentityProviders.ts new file mode 100644 index 000000000000..888cda398c71 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/findAvailableSSOIdentityProviders.ts @@ -0,0 +1,13 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const FIND_AVAILABLE_SSO_IDENTITY_PROVIDERS = gql` + mutation FindAvailableSSOIdentityProviders( + $input: FindAvailableSSOIDPInput! + ) { + findAvailableSSOIdentityProviders(input: $input) { + ...AvailableSSOIdentityProvidersFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts index 7f7d19ae71be..620f70c69c86 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts @@ -3,8 +3,21 @@ import { gql } from '@apollo/client'; export const GENERATE_JWT = gql` mutation GenerateJWT($workspaceId: String!) { generateJWT(workspaceId: $workspaceId) { - tokens { - ...AuthTokensFragment + ... on GenerateJWTOutputWithAuthTokens { + success + reason + authTokens { + tokens { + ...AuthTokensFragment + } + } + } + ... on GenerateJWTOutputWithSSOAUTH { + success + reason + availableSSOIDPs { + ...AvailableSSOIdentityProvidersFragment + } } } } diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthorizationUrl.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthorizationUrl.ts new file mode 100644 index 000000000000..5492a9fade33 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthorizationUrl.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const GET_AUTHORIZATION_URL = gql` + mutation GetAuthorizationUrl($input: GetAuthorizationUrlInput!) { + getAuthorizationUrl(input: $input) { + id + type + authorizationURL + } + } +`; diff --git a/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx similarity index 99% rename from packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx rename to packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx index 60e4025a814f..8e30a3287ab5 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx @@ -116,6 +116,7 @@ describe('useAuth', () => { microsoft: false, magicLink: false, password: false, + sso: false, }); expect(state.billing).toBeNull(); expect(state.isSignInPrefilled).toBe(false); diff --git a/packages/twenty-front/src/modules/auth/hooks/__test__/useIsLogged.test.ts b/packages/twenty-front/src/modules/auth/hooks/__tests__/useIsLogged.test.ts similarity index 100% rename from packages/twenty-front/src/modules/auth/hooks/__test__/useIsLogged.test.ts rename to packages/twenty-front/src/modules/auth/hooks/__tests__/useIsLogged.test.ts diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 7a7de0807f1b..ae13d831fb7a 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -32,6 +32,8 @@ import { import { isDefined } from '~/utils/isDefined'; import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; import { detectDateFormat } from '@/localization/utils/detectDateFormat'; import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; @@ -143,12 +145,12 @@ export const useAuth = () => { ? getDateFormatFromWorkspaceDateFormat( user.workspaceMember.dateFormat, ) - : detectDateFormat(), + : DateFormat[detectDateFormat()], timeFormat: isDefined(user.workspaceMember.timeFormat) ? getTimeFormatFromWorkspaceTimeFormat( user.workspaceMember.timeFormat, ) - : detectTimeFormat(), + : TimeFormat[detectTimeFormat()], }); } diff --git a/packages/twenty-front/src/modules/auth/services/AuthService.ts b/packages/twenty-front/src/modules/auth/services/AuthService.ts index c4c8f1f43a13..120a71eeae12 100644 --- a/packages/twenty-front/src/modules/auth/services/AuthService.ts +++ b/packages/twenty-front/src/modules/auth/services/AuthService.ts @@ -6,7 +6,7 @@ import { UriFunction, } from '@apollo/client'; -import { loggerLink } from '@/apollo/utils'; +import { loggerLink } from '@/apollo/utils/loggerLink'; import { AuthTokenPair, RenewTokenDocument, diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx index 3bad98dcb083..46e38da11c9a 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx @@ -1,8 +1,6 @@ import styled from '@emotion/styled'; import React from 'react'; -type FooterNoteProps = { children: React.ReactNode }; - const StyledContainer = styled.div` align-items: center; color: ${({ theme }) => theme.font.color.tertiary}; @@ -20,6 +18,24 @@ const StyledContainer = styled.div` } `; -export const FooterNote = ({ children }: FooterNoteProps) => ( - {children} +export const FooterNote = () => ( + + By using Twenty, you agree to the{' '} + + Terms of Service + {' '} + and{' '} + + Privacy Policy + + . + ); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/HorizontalSeparator.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/HorizontalSeparator.tsx index f7c5b61f0e5d..d8d68bfbb71c 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/HorizontalSeparator.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/HorizontalSeparator.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; type HorizontalSeparatorProps = { visible?: boolean; + text?: string; }; const StyledSeparator = styled.div` background-color: ${({ theme }) => theme.border.color.medium}; @@ -12,8 +13,39 @@ const StyledSeparator = styled.div` width: 100%; `; +const StyledSeparatorContainer = styled.div` + align-items: center; + display: flex; + margin-bottom: ${({ theme }) => theme.spacing(3)}; + margin-top: ${({ theme }) => theme.spacing(3)}; + width: 100%; +`; + +const StyledLine = styled.div` + background-color: ${({ theme }) => theme.border.color.medium}; + height: ${({ visible }) => (visible ? '1px' : 0)}; + flex-grow: 1; +`; + +const StyledText = styled.span` + color: ${({ theme }) => theme.font.color.light}; + margin: 0 ${({ theme }) => theme.spacing(2)}; + white-space: nowrap; +`; + export const HorizontalSeparator = ({ visible = true, + text = '', }: HorizontalSeparatorProps): JSX.Element => ( - + <> + {text ? ( + + + {text && {text}} + + + ) : ( + + )} + ); diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx index 65021f7c6824..228fde192827 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx @@ -1,30 +1,34 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { motion } from 'framer-motion'; -import { useMemo, useState } from 'react'; -import { Controller } from 'react-hook-form'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { Key } from 'ts-key-enum'; -import { IconGoogle, IconMicrosoft } from 'twenty-ui'; - import { FooterNote } from '@/auth/sign-in-up/components/FooterNote'; import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator'; import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword'; +import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp'; import { - SignInUpMode, - SignInUpStep, - useSignInUp, -} from '@/auth/sign-in-up/hooks/useSignInUp'; -import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm'; + useSignInUpForm, + validationSchema, +} from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle'; import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft'; +import { SignInUpStep } from '@/auth/states/signInUpStepState'; import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; -import { Loader } from '@/ui/feedback/loader/components/Loader'; -import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInput } from '@/ui/input/components/TextInput'; -import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { motion } from 'framer-motion'; +import { useMemo, useState } from 'react'; +import { Controller } from 'react-hook-form'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { Key } from 'ts-key-enum'; +import { + ActionLink, + IconGoogle, + IconKey, + IconMicrosoft, + Loader, + MainButton, + StyledText, +} from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; const StyledContentContainer = styled.div` @@ -64,9 +68,28 @@ export const SignInUpForm = () => { signInUpMode, continueWithCredentials, continueWithEmail, + continueWithSSO, submitCredentials, + submitSSOEmail, } = useSignInUp(form); + if ( + signInUpStep === SignInUpStep.Init && + !authProviders.google && + !authProviders.microsoft && + !authProviders.sso + ) { + continueWithEmail(); + } + + const toggleSSOMode = () => { + if (signInUpStep === SignInUpStep.SSOEmail) { + continueWithEmail(); + } else { + continueWithSSO(); + } + }; + const handleKeyDown = async ( event: React.KeyboardEvent, ) => { @@ -86,6 +109,8 @@ export const SignInUpForm = () => { setShowErrors(true); form.handleSubmit(submitCredentials)(); } + } else if (signInUpStep === SignInUpStep.SSOEmail) { + submitSSOEmail(form.getValues('email')); } } }; @@ -99,6 +124,10 @@ export const SignInUpForm = () => { return 'Continue'; } + if (signInUpStep === SignInUpStep.SSOEmail) { + return 'Continue with SSO'; + } + return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up'; }, [signInUpMode, signInUpStep]); @@ -111,7 +140,8 @@ export const SignInUpForm = () => { const isEmailStepSubmitButtonDisabledCondition = signInUpStep === SignInUpStep.Email && - (form.watch('email')?.length === 0 || shouldWaitForCaptchaToken); + (!validationSchema.shape.email.safeParse(form.watch('email')).success || + shouldWaitForCaptchaToken); // TODO: isValid is actually a proxy function. If it is not rendered the first time, react might not trigger re-renders // We make the isValid check synchronous and update a reactState to make sure this does not happen @@ -134,9 +164,12 @@ export const SignInUpForm = () => { Icon={() => } title="Continue with Google" onClick={signInWithGoogle} + variant={ + signInUpStep === SignInUpStep.Init ? undefined : 'secondary' + } fullWidth /> - + )} @@ -146,19 +179,161 @@ export const SignInUpForm = () => { Icon={() => } title="Continue with Microsoft" onClick={signInWithMicrosoft} + variant={ + signInUpStep === SignInUpStep.Init ? undefined : 'secondary' + } + fullWidth + /> + + + )} + {authProviders.sso && ( + <> + } + variant={ + signInUpStep === SignInUpStep.Init ? undefined : 'secondary' + } + title={ + signInUpStep === SignInUpStep.SSOEmail + ? 'Continue with email' + : 'Single sign-on (SSO)' + } + onClick={toggleSSOMode} fullWidth /> - + )} - {authProviders.password && ( - { - event.preventDefault(); - }} - > - {signInUpStep !== SignInUpStep.Init && ( + {(authProviders.google || + authProviders.microsoft || + authProviders.sso) && } + + {authProviders.password && + (signInUpStep === SignInUpStep.Password || + signInUpStep === SignInUpStep.Email || + signInUpStep === SignInUpStep.Init) && ( + { + event.preventDefault(); + }} + > + {signInUpStep !== SignInUpStep.Init && ( + + ( + + { + onChange(value); + if (signInUpStep === SignInUpStep.Password) { + continueWithEmail(); + } + }} + error={showErrors ? error?.message : undefined} + fullWidth + disableHotkeys + onKeyDown={handleKeyDown} + /> + + )} + /> + + )} + {signInUpStep === SignInUpStep.Password && ( + + ( + + + {signInUpMode === SignInUpMode.SignUp && ( + + )} + + )} + /> + + )} + { + if (signInUpStep === SignInUpStep.Init) { + continueWithEmail(); + return; + } + if (signInUpStep === SignInUpStep.Email) { + if (isDefined(form?.formState?.errors?.email)) { + setShowErrors(true); + return; + } + continueWithCredentials(); + return; + } + setShowErrors(true); + form.handleSubmit(submitCredentials)(); + }} + Icon={() => (form.formState.isSubmitting ? : null)} + disabled={isSubmitButtonDisabled} + fullWidth + /> + + )} + { + event.preventDefault(); + }} + > + {signInUpStep === SignInUpStep.SSOEmail && ( + <> { value={value} placeholder="Email" onBlur={onBlur} - onChange={(value: string) => { - onChange(value); - if (signInUpStep === SignInUpStep.Password) { - continueWithEmail(); - } - }} - error={showErrors ? error?.message : undefined} - fullWidth - disableHotkeys - onKeyDown={handleKeyDown} - /> - - )} - /> - - )} - {signInUpStep === SignInUpStep.Password && ( - - ( - - { )} /> - )} - { - if (signInUpStep === SignInUpStep.Init) { - continueWithEmail(); - return; - } - if (signInUpStep === SignInUpStep.Email) { - if (isDefined(form?.formState?.errors?.email)) { - setShowErrors(true); - return; - } - continueWithCredentials(); - return; - } - setShowErrors(true); - form.handleSubmit(submitCredentials)(); - }} - Icon={() => form.formState.isSubmitting && } - disabled={isSubmitButtonDisabled} - fullWidth - /> - - )} + { + setShowErrors(true); + submitSSOEmail(form.getValues('email')); + }} + Icon={() => form.formState.isSubmitting && } + disabled={isSubmitButtonDisabled} + fullWidth + /> + + )} + {signInUpStep === SignInUpStep.Password && ( Forgot your password? )} - {signInUpStep === SignInUpStep.Init && ( - - By using Twenty, you agree to the{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - - . - - )} + {signInUpStep === SignInUpStep.Init && } ); }; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts new file mode 100644 index 000000000000..86a8b83928f0 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts @@ -0,0 +1,68 @@ +/* @license Enterprise */ + +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { + FindAvailableSsoIdentityProvidersMutationVariables, + GetAuthorizationUrlMutationVariables, + useFindAvailableSsoIdentityProvidersMutation, + useGetAuthorizationUrlMutation, +} from '~/generated/graphql'; +import { isDefined } from '~/utils/isDefined'; + +export const useSSO = () => { + const { enqueueSnackBar } = useSnackBar(); + + const [findAvailableSSOProviderByEmailMutation] = + useFindAvailableSsoIdentityProvidersMutation(); + const [getAuthorizationUrlMutation] = useGetAuthorizationUrlMutation(); + + const findAvailableSSOProviderByEmail = async ({ + email, + }: FindAvailableSsoIdentityProvidersMutationVariables['input']) => { + return await findAvailableSSOProviderByEmailMutation({ + variables: { + input: { email }, + }, + }); + }; + + const getAuthorizationUrlForSSO = async ({ + identityProviderId, + }: GetAuthorizationUrlMutationVariables['input']) => { + return await getAuthorizationUrlMutation({ + variables: { + input: { identityProviderId }, + }, + }); + }; + + const redirectToSSOLoginPage = async (identityProviderId: string) => { + const authorizationUrlForSSOResult = await getAuthorizationUrlForSSO({ + identityProviderId, + }); + + if ( + isDefined(authorizationUrlForSSOResult.errors) || + !authorizationUrlForSSOResult.data || + !authorizationUrlForSSOResult.data?.getAuthorizationUrl.authorizationURL + ) { + return enqueueSnackBar( + authorizationUrlForSSOResult.errors?.[0]?.message ?? 'Unknown error', + { + variant: SnackBarVariant.Error, + }, + ); + } + + window.location.href = + authorizationUrlForSSOResult.data?.getAuthorizationUrl.authorizationURL; + return; + }; + + return { + redirectToSSOLoginPage, + getAuthorizationUrlForSSO, + findAvailableSSOProviderByEmail, + }; +}; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx index 48c66f54ed65..53994713c93c 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx @@ -5,11 +5,19 @@ import { useParams, useSearchParams } from 'react-router-dom'; import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken'; import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken'; -import { AppPath } from '@/types/AppPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; - +import { isDefined } from '~/utils/isDefined'; + +import { useSSO } from '@/auth/sign-in-up/hooks/useSSO'; +import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO'; +import { + SignInUpStep, + signInUpStepState, +} from '@/auth/states/signInUpStepState'; +import { AppPath } from '@/types/AppPath'; import { useAuth } from '../../hooks/useAuth'; export enum SignInUpMode { @@ -17,17 +25,18 @@ export enum SignInUpMode { SignUp = 'sign-up', } -export enum SignInUpStep { - Init = 'init', - Email = 'email', - Password = 'password', -} - export const useSignInUp = (form: UseFormReturn
) => { const { enqueueSnackBar } = useSnackBar(); + const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState); + const isMatchingLocation = useIsMatchingLocation(); + const { redirectToSSOLoginPage, findAvailableSSOProviderByEmail } = useSSO(); + const setAvailableWorkspacesForSSOState = useSetRecoilState( + availableSSOIdentityProvidersState, + ); + const workspaceInviteHash = useParams().workspaceInviteHash; const [searchParams] = useSearchParams(); const workspacePersonalInviteToken = @@ -35,10 +44,6 @@ export const useSignInUp = (form: UseFormReturn) => { const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite)); - const [signInUpStep, setSignInUpStep] = useState( - SignInUpStep.Init, - ); - const [signInUpMode, setSignInUpMode] = useState(() => { return isMatchingLocation(AppPath.SignInUp) ? SignInUpMode.SignIn @@ -62,7 +67,7 @@ export const useSignInUp = (form: UseFormReturn) => { ? SignInUpMode.SignIn : SignInUpMode.SignUp, ); - }, [isMatchingLocation, requestFreshCaptchaToken]); + }, [isMatchingLocation, requestFreshCaptchaToken, setSignInUpStep]); const continueWithCredentials = useCallback(async () => { const token = await readCaptchaToken(); @@ -95,8 +100,48 @@ export const useSignInUp = (form: UseFormReturn) => { checkUserExistsQuery, enqueueSnackBar, requestFreshCaptchaToken, + setSignInUpStep, ]); + const continueWithSSO = () => { + setSignInUpStep(SignInUpStep.SSOEmail); + }; + + const submitSSOEmail = async (email: string) => { + const result = await findAvailableSSOProviderByEmail({ + email, + }); + + if (isDefined(result.errors)) { + return enqueueSnackBar(result.errors[0].message, { + variant: SnackBarVariant.Error, + }); + } + + if ( + !result.data?.findAvailableSSOIdentityProviders || + result.data?.findAvailableSSOIdentityProviders.length === 0 + ) { + enqueueSnackBar('No workspaces with SSO found', { + variant: SnackBarVariant.Error, + }); + return; + } + // If only one workspace, redirect to SSO + if (result.data?.findAvailableSSOIdentityProviders.length === 1) { + return redirectToSSOLoginPage( + result.data.findAvailableSSOIdentityProviders[0].id, + ); + } + + if (result.data?.findAvailableSSOIdentityProviders.length > 1) { + setAvailableWorkspacesForSSOState( + result.data.findAvailableSSOIdentityProviders, + ); + setSignInUpStep(SignInUpStep.SSOWorkspaceSelection); + } + }; + const submitCredentials: SubmitHandler = useCallback( async (data) => { const token = await readCaptchaToken(); @@ -144,6 +189,8 @@ export const useSignInUp = (form: UseFormReturn) => { signInUpMode, continueWithCredentials, continueWithEmail, + continueWithSSO, + submitSSOEmail, submitCredentials, }; }; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts index 817b276678b0..07e2aaf7b174 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts @@ -1,13 +1,13 @@ +import { zodResolver } from '@hookform/resolvers/zod'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; import { useRecoilValue } from 'recoil'; import { z } from 'zod'; import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; -const validationSchema = z +export const validationSchema = z .object({ exist: z.boolean(), email: z.string().trim().email('Email must be a valid email'), diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts index a51365b98a3b..0e238c3ab706 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts @@ -3,11 +3,12 @@ import { useNavigate, useParams } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { AppPath } from '@/types/AppPath'; + import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState'; +import { AppPath } from '@/types/AppPath'; import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/auth/states/availableWorkspacesForSSO.ts b/packages/twenty-front/src/modules/auth/states/availableWorkspacesForSSO.ts new file mode 100644 index 000000000000..e4c83c26e15e --- /dev/null +++ b/packages/twenty-front/src/modules/auth/states/availableWorkspacesForSSO.ts @@ -0,0 +1,11 @@ +import { createState } from 'twenty-ui'; +import { FindAvailableSsoIdentityProvidersMutationResult } from '~/generated/graphql'; + +export const availableSSOIdentityProvidersState = createState< + NonNullable< + FindAvailableSsoIdentityProvidersMutationResult['data'] + >['findAvailableSSOIdentityProviders'] +>({ + key: 'availableSSOIdentityProviders', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/auth/states/currentUserState.ts b/packages/twenty-front/src/modules/auth/states/currentUserState.ts index 2feedc94fea6..352013254120 100644 --- a/packages/twenty-front/src/modules/auth/states/currentUserState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentUserState.ts @@ -7,6 +7,7 @@ export type CurrentUser = Pick< | 'id' | 'email' | 'supportUserHash' + | 'analyticsTinybirdJwt' | 'canImpersonate' | 'onboardingStatus' | 'userVars' diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts index 8dd9ea7bc791..fde9761138e6 100644 --- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts @@ -13,6 +13,8 @@ export type CurrentWorkspace = Pick< | 'activationStatus' | 'currentBillingSubscription' | 'workspaceMembersCount' + | 'isPublicInviteLinkEnabled' + | 'hasValidEntrepriseKey' | 'metadataVersion' >; diff --git a/packages/twenty-front/src/modules/auth/states/signInUpStepState.ts b/packages/twenty-front/src/modules/auth/states/signInUpStepState.ts new file mode 100644 index 000000000000..71f359fdee2e --- /dev/null +++ b/packages/twenty-front/src/modules/auth/states/signInUpStepState.ts @@ -0,0 +1,14 @@ +import { createState } from 'twenty-ui'; + +export enum SignInUpStep { + Init = 'init', + Email = 'email', + Password = 'password', + SSOEmail = 'SSOEmail', + SSOWorkspaceSelection = 'SSOWorkspaceSelection', +} + +export const signInUpStepState = createState({ + key: 'signInUpStepState', + defaultValue: SignInUpStep.Init, +}); diff --git a/packages/twenty-front/src/modules/auth/utils/__test__/passwordRegex.test.ts b/packages/twenty-front/src/modules/auth/utils/__tests__/passwordRegex.test.ts similarity index 100% rename from packages/twenty-front/src/modules/auth/utils/__test__/passwordRegex.test.ts rename to packages/twenty-front/src/modules/auth/utils/__tests__/passwordRegex.test.ts diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index 9eccbeb98e10..bf11d6713c2c 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -1,23 +1,24 @@ -import { useEffect } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - import { apiConfigState } from '@/client-config/states/apiConfigState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; +import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState'; import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { supportChatState } from '@/client-config/states/supportChatState'; +import { useEffect } from 'react'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { useGetClientConfigQuery } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; export const ClientConfigProviderEffect = () => { const setAuthProviders = useSetRecoilState(authProvidersState); const setIsDebugMode = useSetRecoilState(isDebugModeState); + const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState); const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState); const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState); @@ -48,8 +49,10 @@ export const ClientConfigProviderEffect = () => { microsoft: data?.clientConfig.authProviders.microsoft, password: data?.clientConfig.authProviders.password, magicLink: false, + sso: data?.clientConfig.authProviders.sso, }); setIsDebugMode(data?.clientConfig.debugMode); + setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled); setIsSignInPrefilled(data?.clientConfig.signInPrefilled); setIsSignUpDisabled(data?.clientConfig.signUpDisabled); @@ -84,6 +87,7 @@ export const ClientConfigProviderEffect = () => { setCaptchaProvider, setChromeExtensionId, setApiConfig, + setIsAnalyticsEnabled, ]); return <>; diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index e702acefa4f1..eaf55e4d0a93 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -7,6 +7,7 @@ export const GET_CLIENT_CONFIG = gql` google password microsoft + sso } billing { isBillingEnabled @@ -16,6 +17,7 @@ export const GET_CLIENT_CONFIG = gql` signInPrefilled signUpDisabled debugMode + analyticsEnabled support { supportDriver supportFrontChatId diff --git a/packages/twenty-front/src/modules/client-config/states/authProvidersState.ts b/packages/twenty-front/src/modules/client-config/states/authProvidersState.ts index 6b705d037557..ef37f22cf9ff 100644 --- a/packages/twenty-front/src/modules/client-config/states/authProvidersState.ts +++ b/packages/twenty-front/src/modules/client-config/states/authProvidersState.ts @@ -9,5 +9,6 @@ export const authProvidersState = createState({ magicLink: false, password: false, microsoft: false, + sso: false, }, }); diff --git a/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts new file mode 100644 index 000000000000..50c0f5c89c25 --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const isAnalyticsEnabledState = createState({ + key: 'isAnalyticsEnabled', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 25e5294e2729..a7b56427e0da 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -10,15 +10,13 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { Command, CommandType } from '@/command-menu/types/Command'; import { Company } from '@/companies/types/Company'; +import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; -import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; -import { Opportunity } from '@/opportunities/Opportunity'; +import { Opportunity } from '@/opportunities/types/Opportunity'; import { Person } from '@/people/types/Person'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; @@ -32,7 +30,14 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useMemo, useRef } from 'react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; -import { Avatar, IconNotes, IconSparkles, IconX, isDefined } from 'twenty-ui'; +import { + Avatar, + IconNotes, + IconSparkles, + IconX, + LightIconButton, + isDefined, +} from 'twenty-ui'; import { useDebounce } from 'use-debounce'; import { getLogoUrlFromDomainName } from '~/utils'; @@ -181,16 +186,11 @@ export const CommandMenu = () => { searchInput: deferredCommandMenuSearch ?? undefined, }); - const { loading: isNotesLoading, records: notes } = useFindManyRecords({ + const { loading: isNotesLoading, records: notes } = useSearchRecords({ skip: !isCommandMenuOpened, objectNameSingular: CoreObjectNameSingular.Note, - filter: deferredCommandMenuSearch - ? makeOrFilterVariables([ - { title: { ilike: `%${deferredCommandMenuSearch}%` } }, - { body: { ilike: `%${deferredCommandMenuSearch}%` } }, - ]) - : undefined, limit: 3, + searchInput: deferredCommandMenuSearch ?? undefined, }); const { loading: isOpportunitiesLoading, records: opportunities } = @@ -288,6 +288,22 @@ export const CommandMenu = () => { : true) && cmd.type === CommandType.Create, ); + const matchingStandardActionCommands = commandMenuCommands.filter( + (cmd) => + (deferredCommandMenuSearch.length > 0 + ? checkInShortcuts(cmd, deferredCommandMenuSearch) || + checkInLabels(cmd, deferredCommandMenuSearch) + : true) && cmd.type === CommandType.StandardAction, + ); + + const matchingWorkflowRunCommands = commandMenuCommands.filter( + (cmd) => + (deferredCommandMenuSearch.length > 0 + ? checkInShortcuts(cmd, deferredCommandMenuSearch) || + checkInLabels(cmd, deferredCommandMenuSearch) + : true) && cmd.type === CommandType.WorkflowRun, + ); + useListenClickOutside({ refs: [commandMenuRef], callback: closeCommandMenu, @@ -313,6 +329,7 @@ export const CommandMenu = () => { const selectableItemIds = copilotCommands .map((cmd) => cmd.id) + .concat(matchingStandardActionCommands.map((cmd) => cmd.id)) .concat(matchingCreateCommand.map((cmd) => cmd.id)) .concat(matchingNavigateCommand.map((cmd) => cmd.id)) .concat(people?.map((person) => person.id)) @@ -321,22 +338,29 @@ export const CommandMenu = () => { .concat(notes?.map((note) => note.id)); const isNoResults = + !matchingStandardActionCommands.length && + !matchingWorkflowRunCommands.length && !matchingCreateCommand.length && !matchingNavigateCommand.length && !people?.length && !companies?.length && !notes?.length && !opportunities?.length; + const isLoading = isPeopleLoading || isNotesLoading || isOpportunitiesLoading || isCompaniesLoading; + const mainContextStoreComponentInstanceId = useRecoilValue( + mainContextStoreComponentInstanceIdState, + ); + return ( <> {isCommandMenuOpened && ( - + { )} - - {matchingCreateCommand.map((cmd) => ( + {mainContextStoreComponentInstanceId && ( + <> + + {matchingStandardActionCommands?.map( + (standardActionCommand) => ( + + + + ), + )} + + + + {matchingWorkflowRunCommands?.map( + (workflowRunCommand) => ( + + + + ), + )} + + + )} + + {matchingNavigateCommand.map((cmd) => ( { ))} - - {matchingNavigateCommand.map((cmd) => ( + + {matchingCreateCommand.map((cmd) => ( { namePlural: 'tasks', labelSingular: 'Task', labelPlural: 'Tasks', + isLabelSyncedWithName: true, + shortcut: 'T', description: 'A task', icon: 'IconCheckbox', isCustom: false, diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index d19c314a1c8b..71533359946c 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -1,7 +1,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useRecoilCallback, useSetRecoilState } from 'recoil'; +import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; @@ -9,7 +9,9 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { isDefined } from '~/utils/isDefined'; +import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands'; +import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons'; import { sortByProperty } from '~/utils/array/sortByProperty'; @@ -27,10 +29,59 @@ export const useCommandMenu = () => { goBackToPreviousHotkeyScope, } = usePreviousHotkeyScope(); - const openCommandMenu = useCallback(() => { - setIsCommandMenuOpened(true); - setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen); - }, [setHotkeyScopeAndMemorizePreviousScope, setIsCommandMenuOpened]); + const mainContextStoreComponentInstanceId = useRecoilValue( + mainContextStoreComponentInstanceIdState, + ); + + const openCommandMenu = useRecoilCallback( + ({ snapshot }) => + () => { + if (isDefined(mainContextStoreComponentInstanceId)) { + const actionMenuEntries = snapshot.getLoadable( + actionMenuEntriesComponentSelector.selectorFamily({ + instanceId: mainContextStoreComponentInstanceId, + }), + ); + + const commands = Object.values(COMMAND_MENU_COMMANDS); + + const actionCommands = actionMenuEntries + .getValue() + ?.filter((actionMenuEntry) => actionMenuEntry.type === 'standard') + ?.map((actionMenuEntry) => ({ + id: actionMenuEntry.key, + label: actionMenuEntry.label, + Icon: actionMenuEntry.Icon, + onCommandClick: actionMenuEntry.onClick, + type: CommandType.StandardAction, + })); + + const workflowRunCommands = actionMenuEntries + .getValue() + ?.filter( + (actionMenuEntry) => actionMenuEntry.type === 'workflow-run', + ) + ?.map((actionMenuEntry) => ({ + id: actionMenuEntry.key, + label: actionMenuEntry.label, + Icon: actionMenuEntry.Icon, + onCommandClick: actionMenuEntry.onClick, + type: CommandType.WorkflowRun, + })); + + setCommands([...commands, ...actionCommands, ...workflowRunCommands]); + } + + setIsCommandMenuOpened(true); + setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen); + }, + [ + mainContextStoreComponentInstanceId, + setCommands, + setHotkeyScopeAndMemorizePreviousScope, + setIsCommandMenuOpened, + ], + ); const closeCommandMenu = useRecoilCallback( ({ snapshot }) => @@ -41,11 +92,17 @@ export const useCommandMenu = () => { if (isCommandMenuOpened) { setIsCommandMenuOpened(false); + setCommands([]); resetSelectedItem(); goBackToPreviousHotkeyScope(); } }, - [goBackToPreviousHotkeyScope, resetSelectedItem, setIsCommandMenuOpened], + [ + goBackToPreviousHotkeyScope, + resetSelectedItem, + setCommands, + setIsCommandMenuOpened, + ], ); const toggleCommandMenu = useRecoilCallback( @@ -83,8 +140,8 @@ export const useCommandMenu = () => { to: `/objects/${item.namePlural}`, label: `Go to ${item.labelPlural}`, type: CommandType.Navigate, - firstHotKey: 'G', - secondHotKey: item.labelPlural[0], + firstHotKey: item.shortcut ? 'G' : undefined, + secondHotKey: item.shortcut, Icon: ALL_ICONS[ (item?.icon as keyof typeof ALL_ICONS) ?? 'IconArrowUpRight' ], diff --git a/packages/twenty-front/src/modules/command-menu/types/Command.ts b/packages/twenty-front/src/modules/command-menu/types/Command.ts index 38b430bb3459..181ec439183f 100644 --- a/packages/twenty-front/src/modules/command-menu/types/Command.ts +++ b/packages/twenty-front/src/modules/command-menu/types/Command.ts @@ -3,13 +3,19 @@ import { IconComponent } from 'twenty-ui'; export enum CommandType { Navigate = 'Navigate', Create = 'Create', + StandardAction = 'StandardAction', + WorkflowRun = 'WorkflowRun', } export type Command = { id: string; - to: string; + to?: string; label: string; - type: CommandType.Navigate | CommandType.Create; + type: + | CommandType.Navigate + | CommandType.Create + | CommandType.StandardAction + | CommandType.WorkflowRun; Icon?: IconComponent; firstHotKey?: string; secondHotKey?: string; diff --git a/packages/twenty-front/src/modules/context-store/components/MainContextStoreComponentInstanceIdSetterEffect.tsx b/packages/twenty-front/src/modules/context-store/components/MainContextStoreComponentInstanceIdSetterEffect.tsx new file mode 100644 index 000000000000..cc838bb7ed56 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/components/MainContextStoreComponentInstanceIdSetterEffect.tsx @@ -0,0 +1,22 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; +import { useContext, useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; + +export const MainContextStoreComponentInstanceIdSetterEffect = () => { + const setMainContextStoreComponentInstanceId = useSetRecoilState( + mainContextStoreComponentInstanceIdState, + ); + + const context = useContext(ContextStoreComponentInstanceContext); + + useEffect(() => { + setMainContextStoreComponentInstanceId(context?.instanceId ?? null); + + return () => { + setMainContextStoreComponentInstanceId(null); + }; + }, [context, setMainContextStoreComponentInstanceId]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdComponentState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdComponentState.ts new file mode 100644 index 000000000000..9898e6c6f540 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdComponentState.ts @@ -0,0 +1,9 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const contextStoreCurrentObjectMetadataIdComponentState = + createComponentStateV2({ + key: 'contextStoreCurrentObjectMetadataIdComponentState', + defaultValue: null, + componentInstanceContext: ContextStoreComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts deleted file mode 100644 index 3227e53807df..000000000000 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const contextStoreCurrentObjectMetadataIdState = createState< - string | null ->({ - key: 'contextStoreCurrentObjectMetadataIdState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdComponentState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdComponentState.ts new file mode 100644 index 000000000000..10136c28d004 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdComponentState.ts @@ -0,0 +1,10 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const contextStoreCurrentViewIdComponentState = createComponentStateV2< + string | null +>({ + key: 'contextStoreCurrentViewIdComponentState', + defaultValue: null, + componentInstanceContext: ContextStoreComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdState.ts deleted file mode 100644 index 41af1cc1357b..000000000000 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const contextStoreCurrentViewIdState = createState({ - key: 'contextStoreCurrentViewIdState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsComponentState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsComponentState.ts new file mode 100644 index 000000000000..54a6cb1adba9 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsComponentState.ts @@ -0,0 +1,9 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const contextStoreNumberOfSelectedRecordsComponentState = + createComponentStateV2({ + key: 'contextStoreNumberOfSelectedRecordsComponentState', + defaultValue: 0, + componentInstanceContext: ContextStoreComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts deleted file mode 100644 index df0c3451172c..000000000000 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const contextStoreTargetedRecordIdsState = createState({ - key: 'contextStoreTargetedRecordIdsState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleComponentState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleComponentState.ts new file mode 100644 index 000000000000..1540c05f3f6d --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleComponentState.ts @@ -0,0 +1,28 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +type ContextStoreTargetedRecordsRuleSelectionMode = { + mode: 'selection'; + selectedRecordIds: string[]; +}; + +type ContextStoreTargetedRecordsRuleExclusionMode = { + mode: 'exclusion'; + excludedRecordIds: string[]; + filters: Filter[]; +}; + +export type ContextStoreTargetedRecordsRule = + | ContextStoreTargetedRecordsRuleSelectionMode + | ContextStoreTargetedRecordsRuleExclusionMode; + +export const contextStoreTargetedRecordsRuleComponentState = + createComponentStateV2({ + key: 'contextStoreTargetedRecordsRuleComponentState', + defaultValue: { + mode: 'selection', + selectedRecordIds: [], + }, + componentInstanceContext: ContextStoreComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/context-store/states/contexts/ContextStoreComponentInstanceContext.tsx b/packages/twenty-front/src/modules/context-store/states/contexts/ContextStoreComponentInstanceContext.tsx new file mode 100644 index 000000000000..f82a5d53b14d --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contexts/ContextStoreComponentInstanceContext.tsx @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const ContextStoreComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts b/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts new file mode 100644 index 000000000000..2e7343672710 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts @@ -0,0 +1,8 @@ +import { createState } from 'twenty-ui'; + +export const mainContextStoreComponentInstanceIdState = createState< + string | null +>({ + key: 'mainContextStoreComponentInstanceIdState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts new file mode 100644 index 000000000000..1c65848ede05 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts @@ -0,0 +1,77 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +describe('computeContextStoreFilters', () => { + const personObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', + )!; + + it('should work for selection mode', () => { + const contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule = { + mode: 'selection', + selectedRecordIds: ['1', '2', '3'], + }; + + const filters = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + personObjectMetadataItem, + ); + + expect(filters).toEqual({ + id: { + in: ['1', '2', '3'], + }, + }); + }); + + it('should work for exclusion mode', () => { + const contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule = { + mode: 'exclusion', + filters: [ + { + id: 'name-filter', + variant: 'default', + fieldMetadataId: personObjectMetadataItem.fields.find( + (field) => field.name === 'name', + )!.id, + value: 'John', + displayValue: 'John', + displayAvatarUrl: undefined, + operand: ViewFilterOperand.Contains, + definition: { + fieldMetadataId: personObjectMetadataItem.fields.find( + (field) => field.name === 'name', + )!.id, + label: 'Name', + iconName: 'person', + type: 'TEXT', + }, + }, + ], + excludedRecordIds: ['1', '2', '3'], + }; + + const filters = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + personObjectMetadataItem, + ); + + expect(filters).toEqual({ + and: [ + { + name: { + ilike: '%John%', + }, + }, + { + not: { + id: { + in: ['1', '2', '3'], + }, + }, + }, + ], + }); + }); +}); diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts new file mode 100644 index 000000000000..5126ae272ccb --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -0,0 +1,43 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; + +export const computeContextStoreFilters = ( + contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule, + objectMetadataItem: ObjectMetadataItem, +) => { + let queryFilter: RecordGqlOperationFilter | undefined; + + if (contextStoreTargetedRecordsRule.mode === 'exclusion') { + queryFilter = makeAndFilterVariables([ + computeViewRecordGqlOperationFilter( + contextStoreTargetedRecordsRule.filters, + objectMetadataItem?.fields ?? [], + [], + ), + contextStoreTargetedRecordsRule.excludedRecordIds.length > 0 + ? { + not: { + id: { + in: contextStoreTargetedRecordsRule.excludedRecordIds, + }, + }, + } + : undefined, + ]); + } + if (contextStoreTargetedRecordsRule.mode === 'selection') { + queryFilter = + contextStoreTargetedRecordsRule.selectedRecordIds.length > 0 + ? { + id: { + in: contextStoreTargetedRecordsRule.selectedRecordIds, + }, + } + : undefined; + } + + return queryFilter; +}; diff --git a/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts b/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts index 8155f348def3..e74c3f6edc18 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useCreateOneDatabaseConnection.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { CREATE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/createOneDatabaseConnection'; import { GET_MANY_DATABASE_CONNECTIONS } from '@/databases/graphql/queries/findManyDatabaseConnections'; @@ -17,7 +17,7 @@ export const useCreateOneDatabaseConnection = () => { CreateServerMutation, CreateServerMutationVariables >(CREATE_ONE_DATABASE_CONNECTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const createOneDatabaseConnection = async ( diff --git a/packages/twenty-front/src/modules/databases/hooks/useDeleteOneDatabaseConnection.ts b/packages/twenty-front/src/modules/databases/hooks/useDeleteOneDatabaseConnection.ts index eae2f4a42d49..cc10c9d2e1b1 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useDeleteOneDatabaseConnection.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useDeleteOneDatabaseConnection.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { DELETE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/deleteOneDatabaseConnection'; @@ -17,7 +17,7 @@ export const useDeleteOneDatabaseConnection = () => { DeleteServerMutation, DeleteServerMutationVariables >(DELETE_ONE_DATABASE_CONNECTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const deleteOneDatabaseConnection = async (input: RemoteServerIdInput) => { diff --git a/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTable.ts b/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTable.ts index 24da83024d59..884339b46e75 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTable.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTable.ts @@ -1,5 +1,5 @@ +import { useApolloClient, useMutation } from '@apollo/client'; import { useCallback } from 'react'; -import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; import { SYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/syncRemoteTable'; import { modifyRemoteTableFromCache } from '@/databases/utils/modifyRemoteTableFromCache'; @@ -28,7 +28,7 @@ export const useSyncRemoteTable = () => { SyncRemoteTableMutation, SyncRemoteTableMutationVariables >(SYNC_REMOTE_TABLE, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const syncRemoteTable = useCallback( diff --git a/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTableSchemaChanges.ts b/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTableSchemaChanges.ts index a352eb0b77e4..0ce4bf68c991 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTableSchemaChanges.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useSyncRemoteTableSchemaChanges.ts @@ -1,5 +1,5 @@ +import { useApolloClient, useMutation } from '@apollo/client'; import { useCallback } from 'react'; -import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; import { SYNC_REMOTE_TABLE_SCHEMA_CHANGES } from '@/databases/graphql/mutations/syncRemoteTableSchemaChanges'; import { modifyRemoteTableFromCache } from '@/databases/utils/modifyRemoteTableFromCache'; @@ -29,7 +29,7 @@ export const useSyncRemoteTableSchemaChanges = () => { SyncRemoteTableSchemaChangesMutation, SyncRemoteTableSchemaChangesMutationVariables >(SYNC_REMOTE_TABLE_SCHEMA_CHANGES, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const syncRemoteTableSchemaChanges = useCallback( diff --git a/packages/twenty-front/src/modules/databases/hooks/useUnsyncRemoteTable.ts b/packages/twenty-front/src/modules/databases/hooks/useUnsyncRemoteTable.ts index 7e08b5fded7b..4a331e3892d1 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useUnsyncRemoteTable.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useUnsyncRemoteTable.ts @@ -1,5 +1,5 @@ +import { useMutation } from '@apollo/client'; import { useCallback } from 'react'; -import { ApolloClient, useMutation } from '@apollo/client'; import { UNSYNC_REMOTE_TABLE } from '@/databases/graphql/mutations/unsyncRemoteTable'; import { modifyRemoteTableFromCache } from '@/databases/utils/modifyRemoteTableFromCache'; @@ -21,7 +21,7 @@ export const useUnsyncRemoteTable = () => { UnsyncRemoteTableMutation, UnsyncRemoteTableMutationVariables >(UNSYNC_REMOTE_TABLE, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const unsyncRemoteTable = useCallback( diff --git a/packages/twenty-front/src/modules/databases/hooks/useUpdateOneDatabaseConnection.ts b/packages/twenty-front/src/modules/databases/hooks/useUpdateOneDatabaseConnection.ts index 5cffbf2a37fa..4fcc930a2836 100644 --- a/packages/twenty-front/src/modules/databases/hooks/useUpdateOneDatabaseConnection.ts +++ b/packages/twenty-front/src/modules/databases/hooks/useUpdateOneDatabaseConnection.ts @@ -1,4 +1,4 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { UPDATE_ONE_DATABASE_CONNECTION } from '@/databases/graphql/mutations/updateOneDatabaseConnection'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; @@ -15,7 +15,7 @@ export const useUpdateOneDatabaseConnection = () => { UpdateServerMutation, UpdateServerMutationVariables >(UPDATE_ONE_DATABASE_CONNECTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const updateOneDatabaseConnection = async ( diff --git a/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx b/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx index 8e758e67f6f4..fc6c822441c3 100644 --- a/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx @@ -1,6 +1,6 @@ +import * as Sentry from '@sentry/react'; import { ErrorInfo, ReactNode } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import * as Sentry from '@sentry/react'; import { GenericErrorFallback } from '@/error-handler/components/GenericErrorFallback'; diff --git a/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx b/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx index 2109891aca14..8b3c647054bc 100644 --- a/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx @@ -1,18 +1,18 @@ +import { PageBody } from '@/ui/layout/page/components/PageBody'; +import { PageContainer } from '@/ui/layout/page/components/PageContainer'; +import { PageHeader } from '@/ui/layout/page/components/PageHeader'; import { useEffect, useState } from 'react'; import { FallbackProps } from 'react-error-boundary'; import { useLocation } from 'react-router-dom'; -import { ThemeProvider, useTheme } from '@emotion/react'; -import isEmpty from 'lodash.isempty'; -import { IconRefresh, THEME_LIGHT } from 'twenty-ui'; - -import { Button } from '@/ui/input/button/components/Button'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; + Button, + IconRefresh, +} from 'twenty-ui'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; type GenericErrorFallbackProps = FallbackProps; @@ -31,27 +31,28 @@ export const GenericErrorFallback = ({ } }, [previousLocation, location, resetErrorBoundary]); - const theme = useTheme(); - return ( - - - - - - Server’s on a coffee break - - - {error.message} - - - + {isXMLMetadataValid() && ( + + )} + + +
+ + + + + + + + + + + +
+ + ); +}; diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx new file mode 100644 index 000000000000..f7b9dc2adea5 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx @@ -0,0 +1,60 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; +import { Card, IconLink, Toggle } from 'twenty-ui'; +import { useUpdateWorkspaceMutation } from '~/generated/graphql'; + +const StyledToggle = styled(Toggle)` + margin-left: auto; +`; + +export const SettingsSecurityOptionsList = () => { + const { enqueueSnackBar } = useSnackBar(); + + const [currentWorkspace, setCurrentWorkspace] = useRecoilState( + currentWorkspaceState, + ); + + const [updateWorkspace] = useUpdateWorkspaceMutation(); + + const handleChange = async (value: boolean) => { + try { + if (!currentWorkspace?.id) { + throw new Error('User is not logged in'); + } + await updateWorkspace({ + variables: { + input: { + isPublicInviteLinkEnabled: value, + }, + }, + }); + setCurrentWorkspace({ + ...currentWorkspace, + isPublicInviteLinkEnabled: value, + }); + } catch (err: any) { + enqueueSnackBar(err?.message, { + variant: SnackBarVariant.Error, + }); + } + }; + + return ( + + + handleChange(!currentWorkspace?.isPublicInviteLinkEnabled) + } + > + + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx new file mode 100644 index 000000000000..55b43525d122 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx @@ -0,0 +1,106 @@ +/* @license Enterprise */ + +import { + IconArchive, + IconDotsVertical, + IconTrash, + LightIconButton, + MenuItem, +} from 'twenty-ui'; + +import { useDeleteSSOIdentityProvider } from '@/settings/security/hooks/useDeleteSSOIdentityProvider'; +import { useUpdateSSOIdentityProvider } from '@/settings/security/hooks/useUpdateSSOIdentityProvider'; +import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { UnwrapRecoilValue } from 'recoil'; +import { SsoIdentityProviderStatus } from '~/generated/graphql'; +import { isDefined } from '~/utils/isDefined'; + +type SettingsSecuritySSORowDropdownMenuProps = { + SSOIdp: UnwrapRecoilValue[0]; +}; + +export const SettingsSecuritySSORowDropdownMenu = ({ + SSOIdp, +}: SettingsSecuritySSORowDropdownMenuProps) => { + const dropdownId = `settings-account-row-${SSOIdp.id}`; + + const { enqueueSnackBar } = useSnackBar(); + + const { closeDropdown } = useDropdown(dropdownId); + + const { deleteSSOIdentityProvider } = useDeleteSSOIdentityProvider(); + const { updateSSOIdentityProvider } = useUpdateSSOIdentityProvider(); + + const handleDeleteSSOIdentityProvider = async ( + identityProviderId: string, + ) => { + const result = await deleteSSOIdentityProvider({ + identityProviderId, + }); + if (isDefined(result.errors)) { + enqueueSnackBar('Error deleting SSO Identity Provider', { + variant: SnackBarVariant.Error, + duration: 2000, + }); + } + }; + + const toggleSSOIdentityProviderStatus = async ( + identityProviderId: string, + ) => { + const result = await updateSSOIdentityProvider({ + id: identityProviderId, + status: + SSOIdp.status === 'Active' + ? SsoIdentityProviderStatus.Inactive + : SsoIdentityProviderStatus.Active, + }); + if (isDefined(result.errors)) { + enqueueSnackBar('Error editing SSO Identity Provider', { + variant: SnackBarVariant.Error, + duration: 2000, + }); + } + }; + + return ( + + } + dropdownComponents={ + + + { + toggleSSOIdentityProviderStatus(SSOIdp.id); + closeDropdown(); + }} + /> + { + handleDeleteSSOIdentityProvider(SSOIdp.id); + closeDropdown(); + }} + /> + + + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/mutations/createOIDCSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/graphql/mutations/createOIDCSSOIdentityProvider.ts new file mode 100644 index 000000000000..e0cd4b6dd323 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/mutations/createOIDCSSOIdentityProvider.ts @@ -0,0 +1,15 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const CREATE_OIDC_SSO_IDENTITY_PROVIDER = gql` + mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) { + createOIDCIdentityProvider(input: $input) { + id + type + issuer + name + status + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/mutations/createSAMLSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/graphql/mutations/createSAMLSSOIdentityProvider.ts new file mode 100644 index 000000000000..3729b4f504a9 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/mutations/createSAMLSSOIdentityProvider.ts @@ -0,0 +1,15 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const CREATE_SAML_SSO_IDENTITY_PROVIDER = gql` + mutation CreateSAMLIdentityProvider($input: SetupSAMLSsoInput!) { + createSAMLIdentityProvider(input: $input) { + id + type + issuer + name + status + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/mutations/deleteSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/graphql/mutations/deleteSSOIdentityProvider.ts new file mode 100644 index 000000000000..d9153d33d909 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/mutations/deleteSSOIdentityProvider.ts @@ -0,0 +1,11 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const DELETE_SSO_IDENTITY_PROVIDER = gql` + mutation DeleteSSOIdentityProvider($input: DeleteSsoInput!) { + deleteSSOIdentityProvider(input: $input) { + identityProviderId + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/mutations/editSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/graphql/mutations/editSSOIdentityProvider.ts new file mode 100644 index 000000000000..78a83a3b53fa --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/mutations/editSSOIdentityProvider.ts @@ -0,0 +1,15 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const EDIT_SSO_IDENTITY_PROVIDER = gql` + mutation EditSSOIdentityProvider($input: EditSsoInput!) { + editSSOIdentityProvider(input: $input) { + id + type + issuer + name + status + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/queries/getWorkspaceSSOIdentitiesProviders.ts b/packages/twenty-front/src/modules/settings/security/graphql/queries/getWorkspaceSSOIdentitiesProviders.ts new file mode 100644 index 000000000000..0fdd9701e722 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/queries/getWorkspaceSSOIdentitiesProviders.ts @@ -0,0 +1,15 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const LIST_WORKSPACE_SSO_IDENTITY_PROVIDERS = gql` + query ListSSOIdentityProvidersByWorkspaceId { + listSSOIdentityProvidersByWorkspaceId { + type + id + name + issuer + status + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx new file mode 100644 index 000000000000..50b71e727b2b --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx @@ -0,0 +1,94 @@ +/* @license Enterprise */ + +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; + +import { useCreateSSOIdentityProvider } from '@/settings/security/hooks/useCreateSSOIdentityProvider'; + +const mutationOIDCCallSpy = jest.fn(); +const mutationSAMLCallSpy = jest.fn(); + +jest.mock('~/generated/graphql', () => ({ + useCreateOidcIdentityProviderMutation: () => [mutationOIDCCallSpy], + useCreateSamlIdentityProviderMutation: () => [mutationSAMLCallSpy], +})); + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +describe('useCreateSSOIdentityProvider', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('create OIDC sso identity provider', async () => { + const OIDCParams = { + type: 'OIDC' as const, + name: 'test', + clientID: 'test', + clientSecret: 'test', + issuer: 'test', + }; + renderHook( + () => { + const { createSSOIdentityProvider } = useCreateSSOIdentityProvider(); + createSSOIdentityProvider(OIDCParams); + }, + { wrapper: Wrapper }, + ); + + // eslint-disable-next-line unused-imports/no-unused-vars + const { type, ...input } = OIDCParams; + expect(mutationOIDCCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: { + input, + }, + }); + }); + it('create SAML sso identity provider', async () => { + const SAMLParams = { + type: 'SAML' as const, + name: 'test', + metadata: 'test', + certificate: 'test', + id: 'test', + issuer: 'test', + ssoURL: 'test', + }; + renderHook( + () => { + const { createSSOIdentityProvider } = useCreateSSOIdentityProvider(); + createSSOIdentityProvider(SAMLParams); + }, + { wrapper: Wrapper }, + ); + + // eslint-disable-next-line unused-imports/no-unused-vars + const { type, ...input } = SAMLParams; + expect(mutationOIDCCallSpy).not.toHaveBeenCalled(); + expect(mutationSAMLCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: { + input, + }, + }); + }); + it('throw error if provider is not SAML or OIDC', async () => { + const OTHERParams = { + type: 'OTHER' as const, + }; + renderHook( + async () => { + const { createSSOIdentityProvider } = useCreateSSOIdentityProvider(); + await expect( + // @ts-expect-error - It's expected to throw an error + createSSOIdentityProvider(OTHERParams), + ).rejects.toThrowError(); + }, + { wrapper: Wrapper }, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useDeleteSSOIdentityProvider.test.tsx b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useDeleteSSOIdentityProvider.test.tsx new file mode 100644 index 000000000000..48b5e101918c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useDeleteSSOIdentityProvider.test.tsx @@ -0,0 +1,40 @@ +/* @license Enterprise */ + +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; + +import { useDeleteSSOIdentityProvider } from '@/settings/security/hooks/useDeleteSSOIdentityProvider'; + +const mutationDeleteSSOIDPCallSpy = jest.fn(); + +jest.mock('~/generated/graphql', () => ({ + useDeleteSsoIdentityProviderMutation: () => [mutationDeleteSSOIDPCallSpy], +})); + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +describe('useDeleteSsoIdentityProvider', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('delete SSO identity provider', async () => { + renderHook( + () => { + const { deleteSSOIdentityProvider } = useDeleteSSOIdentityProvider(); + deleteSSOIdentityProvider({ identityProviderId: 'test' }); + }, + { wrapper: Wrapper }, + ); + + expect(mutationDeleteSSOIDPCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: { + input: { identityProviderId: 'test' }, + }, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useEditSSOIdentityProvider.test.tsx b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useEditSSOIdentityProvider.test.tsx new file mode 100644 index 000000000000..f253f10cb432 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useEditSSOIdentityProvider.test.tsx @@ -0,0 +1,49 @@ +/* @license Enterprise */ + +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; + +import { useUpdateSSOIdentityProvider } from '@/settings/security/hooks/useUpdateSSOIdentityProvider'; +import { SsoIdentityProviderStatus } from '~/generated/graphql'; + +const mutationEditSSOIDPCallSpy = jest.fn(); + +jest.mock('~/generated/graphql', () => { + const actual = jest.requireActual('~/generated/graphql'); + return { + useEditSsoIdentityProviderMutation: () => [mutationEditSSOIDPCallSpy], + SsoIdentityProviderStatus: actual.SsoIdentityProviderStatus, + }; +}); + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +describe('useEditSsoIdentityProvider', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Deactivate SSO identity provider', async () => { + const params = { + id: 'test', + status: SsoIdentityProviderStatus.Inactive, + }; + renderHook( + () => { + const { updateSSOIdentityProvider } = useUpdateSSOIdentityProvider(); + updateSSOIdentityProvider(params); + }, + { wrapper: Wrapper }, + ); + + expect(mutationEditSSOIDPCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: { + input: params, + }, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/security/hooks/useCreateSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/hooks/useCreateSSOIdentityProvider.ts new file mode 100644 index 000000000000..b7dd56f1b13e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/useCreateSSOIdentityProvider.ts @@ -0,0 +1,63 @@ +/* @license Enterprise */ + +import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; +import { useSetRecoilState } from 'recoil'; +import { + CreateOidcIdentityProviderMutationVariables, + CreateSamlIdentityProviderMutationVariables, + useCreateOidcIdentityProviderMutation, + useCreateSamlIdentityProviderMutation, +} from '~/generated/graphql'; + +export const useCreateSSOIdentityProvider = () => { + const [createOidcIdentityProviderMutation] = + useCreateOidcIdentityProviderMutation(); + const [createSamlIdentityProviderMutation] = + useCreateSamlIdentityProviderMutation(); + + const setSSOIdentitiesProviders = useSetRecoilState( + SSOIdentitiesProvidersState, + ); + + const createSSOIdentityProvider = async ( + input: + | ({ + type: 'OIDC'; + } & CreateOidcIdentityProviderMutationVariables['input']) + | ({ + type: 'SAML'; + } & CreateSamlIdentityProviderMutationVariables['input']), + ) => { + if (input.type === 'OIDC') { + // eslint-disable-next-line unused-imports/no-unused-vars + const { type, ...params } = input; + return await createOidcIdentityProviderMutation({ + variables: { input: params }, + onCompleted: (data) => { + setSSOIdentitiesProviders((existingProvider) => [ + ...existingProvider, + data.createOIDCIdentityProvider, + ]); + }, + }); + } else if (input.type === 'SAML') { + // eslint-disable-next-line unused-imports/no-unused-vars + const { type, ...params } = input; + return await createSamlIdentityProviderMutation({ + variables: { input: params }, + onCompleted: (data) => { + setSSOIdentitiesProviders((existingProvider) => [ + ...existingProvider, + data.createSAMLIdentityProvider, + ]); + }, + }); + } else { + throw new Error('Invalid IdpType'); + } + }; + + return { + createSSOIdentityProvider, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/security/hooks/useDeleteSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/hooks/useDeleteSSOIdentityProvider.ts new file mode 100644 index 000000000000..a140444631cb --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/useDeleteSSOIdentityProvider.ts @@ -0,0 +1,40 @@ +/* @license Enterprise */ + +import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; +import { useSetRecoilState } from 'recoil'; +import { + DeleteSsoIdentityProviderMutationVariables, + useDeleteSsoIdentityProviderMutation, +} from '~/generated/graphql'; + +export const useDeleteSSOIdentityProvider = () => { + const [deleteSsoIdentityProviderMutation] = + useDeleteSsoIdentityProviderMutation(); + + const setSSOIdentitiesProviders = useSetRecoilState( + SSOIdentitiesProvidersState, + ); + + const deleteSSOIdentityProvider = async ({ + identityProviderId, + }: DeleteSsoIdentityProviderMutationVariables['input']) => { + return await deleteSsoIdentityProviderMutation({ + variables: { + input: { identityProviderId }, + }, + onCompleted: (data) => { + setSSOIdentitiesProviders((SSOIdentitiesProviders) => + SSOIdentitiesProviders.filter( + (identityProvider) => + identityProvider.id !== + data.deleteSSOIdentityProvider.identityProviderId, + ), + ); + }, + }); + }; + + return { + deleteSSOIdentityProvider, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/security/hooks/useUpdateSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/hooks/useUpdateSSOIdentityProvider.ts new file mode 100644 index 000000000000..07baaaae6a7a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/useUpdateSSOIdentityProvider.ts @@ -0,0 +1,40 @@ +/* @license Enterprise */ + +import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; +import { useSetRecoilState } from 'recoil'; +import { + EditSsoIdentityProviderMutationVariables, + useEditSsoIdentityProviderMutation, +} from '~/generated/graphql'; + +export const useUpdateSSOIdentityProvider = () => { + const [editSsoIdentityProviderMutation] = + useEditSsoIdentityProviderMutation(); + + const setSSOIdentitiesProviders = useSetRecoilState( + SSOIdentitiesProvidersState, + ); + + const updateSSOIdentityProvider = async ( + payload: EditSsoIdentityProviderMutationVariables['input'], + ) => { + return await editSsoIdentityProviderMutation({ + variables: { + input: payload, + }, + onCompleted: (data) => { + setSSOIdentitiesProviders((SSOIdentitiesProviders) => + SSOIdentitiesProviders.map((identityProvider) => + identityProvider.id === data.editSSOIdentityProvider.id + ? data.editSSOIdentityProvider + : identityProvider, + ), + ); + }, + }); + }; + + return { + updateSSOIdentityProvider, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/security/states/SSOIdentitiesProviders.state.ts b/packages/twenty-front/src/modules/settings/security/states/SSOIdentitiesProviders.state.ts new file mode 100644 index 000000000000..76dc7cfdfbde --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/states/SSOIdentitiesProviders.state.ts @@ -0,0 +1,11 @@ +/* @license Enterprise */ + +import { SSOIdentityProvider } from '@/settings/security/types/SSOIdentityProvider'; +import { createState } from 'twenty-ui'; + +export const SSOIdentitiesProvidersState = createState< + Omit[] +>({ + key: 'SSOIdentitiesProvidersState', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/settings/security/types/SSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/types/SSOIdentityProvider.ts new file mode 100644 index 000000000000..fe7226c9d2c4 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/types/SSOIdentityProvider.ts @@ -0,0 +1,18 @@ +/* @license Enterprise */ + +import { SSOIdentitiesProvidersParamsSchema } from '@/settings/security/validation-schemas/SSOIdentityProviderSchema'; +import { z } from 'zod'; +import { IdpType, SsoIdentityProviderStatus } from '~/generated/graphql'; + +export type SSOIdentityProvider = { + __typename: 'SSOIdentityProvider'; + id: string; + type: IdpType; + issuer: string; + name?: string | null; + status: SsoIdentityProviderStatus; +}; + +export type SettingSecurityNewSSOIdentityFormValues = z.infer< + typeof SSOIdentitiesProvidersParamsSchema +>; diff --git a/packages/twenty-front/src/modules/settings/security/utils/__tests__/parseSAMLMetadataFromXMLFile.test.ts b/packages/twenty-front/src/modules/settings/security/utils/__tests__/parseSAMLMetadataFromXMLFile.test.ts new file mode 100644 index 000000000000..e1d79168e823 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/__tests__/parseSAMLMetadataFromXMLFile.test.ts @@ -0,0 +1,39 @@ +/* @license Enterprise */ + +import { parseSAMLMetadataFromXMLFile } from '../parseSAMLMetadataFromXMLFile'; + +describe('parseSAMLMetadataFromXMLFile', () => { + it('should parse SAML metadata from XML file', () => { + const xmlString = ` + + + + + test + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + +`; + const result = parseSAMLMetadataFromXMLFile(xmlString); + expect(result).toEqual({ + success: true, + data: { + entityID: 'https://test.com', + ssoUrl: 'https://test.com', + certificate: 'test', + }, + }); + }); + it('should return error if XML is invalid', () => { + const xmlString = 'invalid xml'; + const result = parseSAMLMetadataFromXMLFile(xmlString); + expect(result).toEqual({ + success: false, + error: new Error('Error parsing XML'), + }); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/security/utils/getColorBySSOIdentityProviderStatus.ts b/packages/twenty-front/src/modules/settings/security/utils/getColorBySSOIdentityProviderStatus.ts new file mode 100644 index 000000000000..94fd86f95dd0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/getColorBySSOIdentityProviderStatus.ts @@ -0,0 +1,13 @@ +/* @license Enterprise */ + +import { ThemeColor } from 'twenty-ui'; +import { SsoIdentityProviderStatus } from '~/generated/graphql'; + +export const getColorBySSOIdentityProviderStatus: Record< + SsoIdentityProviderStatus, + ThemeColor +> = { + Active: 'green', + Inactive: 'gray', + Error: 'red', +}; diff --git a/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts b/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts new file mode 100644 index 000000000000..f8582577f999 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts @@ -0,0 +1,13 @@ +/* @license Enterprise */ + +import { IconComponent, IconGoogle, IconKey } from 'twenty-ui'; + +export const guessSSOIdentityProviderIconByUrl = ( + url: string, +): IconComponent => { + if (url.includes('google')) { + return IconGoogle; + } + + return IconKey; +}; diff --git a/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts b/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts new file mode 100644 index 000000000000..2e4fdf294b2d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts @@ -0,0 +1,59 @@ +/* @license Enterprise */ + +import { z } from 'zod'; + +const validator = z.object({ + entityID: z.string().url(), + ssoUrl: z.string().url(), + certificate: z.string().min(1), +}); + +export const parseSAMLMetadataFromXMLFile = ( + xmlString: string, +): + | { success: true; data: z.infer } + | { success: false; error: unknown } => { + try { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlString, 'application/xml'); + + if (xmlDoc.getElementsByTagName('parsererror').length > 0) { + throw new Error('Error parsing XML'); + } + + const entityDescriptor = xmlDoc.getElementsByTagName( + 'md:EntityDescriptor', + )?.[0]; + const idpSSODescriptor = xmlDoc.getElementsByTagName( + 'md:IDPSSODescriptor', + )?.[0]; + const keyDescriptor = xmlDoc.getElementsByTagName('md:KeyDescriptor')[0]; + const keyInfo = keyDescriptor.getElementsByTagName('ds:KeyInfo')[0]; + const x509Data = keyInfo.getElementsByTagName('ds:X509Data')[0]; + const x509Certificate = x509Data + .getElementsByTagName('ds:X509Certificate')?.[0] + .textContent?.trim(); + + const singleSignOnServices = Array.from( + idpSSODescriptor.getElementsByTagName('md:SingleSignOnService'), + ).map((service) => ({ + Binding: service.getAttribute('Binding'), + Location: service.getAttribute('Location'), + })); + + const result = { + ssoUrl: singleSignOnServices.find((singleSignOnService) => { + return ( + singleSignOnService.Binding === + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + ); + })?.Location, + certificate: x509Certificate, + entityID: entityDescriptor?.getAttribute('entityID'), + }; + + return { success: true, data: validator.parse(result) }; + } catch (error) { + return { success: false, error }; + } +}; diff --git a/packages/twenty-front/src/modules/settings/security/utils/sSOIdentityProviderDefaultValues.ts b/packages/twenty-front/src/modules/settings/security/utils/sSOIdentityProviderDefaultValues.ts new file mode 100644 index 000000000000..a5358e948b86 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/sSOIdentityProviderDefaultValues.ts @@ -0,0 +1,25 @@ +/* @license Enterprise */ + +import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider'; +import { IdpType } from '~/generated/graphql'; + +export const sSOIdentityProviderDefaultValues: Record< + IdpType, + () => SettingSecurityNewSSOIdentityFormValues +> = { + SAML: () => ({ + type: 'SAML', + ssoURL: '', + name: '', + id: crypto.randomUUID(), + certificate: '', + issuer: '', + }), + OIDC: () => ({ + type: 'OIDC', + name: '', + clientID: '', + clientSecret: '', + issuer: '', + }), +}; diff --git a/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts b/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts new file mode 100644 index 000000000000..adfd8680f590 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts @@ -0,0 +1,34 @@ +/* @license Enterprise */ + +import { z } from 'zod'; + +export const SSOIdentitiesProvidersOIDCParamsSchema = z + .object({ + type: z.literal('OIDC'), + clientID: z.string().optional(), + clientSecret: z.string().optional(), + }) + .required(); + +export const SSOIdentitiesProvidersSAMLParamsSchema = z + .object({ + type: z.literal('SAML'), + id: z.string().optional(), + ssoURL: z.string().url().optional(), + certificate: z.string().optional(), + }) + .required(); + +export const SSOIdentitiesProvidersParamsSchema = z + .discriminatedUnion('type', [ + SSOIdentitiesProvidersOIDCParamsSchema, + SSOIdentitiesProvidersSAMLParamsSchema, + ]) + .and( + z + .object({ + name: z.string().min(1), + issuer: z.string().url().optional(), + }) + .required(), + ); diff --git a/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx similarity index 58% rename from packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx rename to packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx index 1240a6d6025e..543c4772e97e 100644 --- a/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx @@ -1,43 +1,36 @@ -import Editor, { Monaco, EditorProps } from '@monaco-editor/react'; +import { SettingsServerlessFunctionCodeEditorContainer } from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer'; +import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; +import { EditorProps, Monaco } from '@monaco-editor/react'; import dotenv from 'dotenv'; -import { AutoTypings } from 'monaco-editor-auto-typings'; import { editor, MarkerSeverity } from 'monaco-editor'; -import { codeEditorTheme } from '@/ui/input/code-editor/theme/CodeEditorTheme'; -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; +import { AutoTypings } from 'monaco-editor-auto-typings'; +import { CodeEditor } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; -const StyledEditor = styled(Editor)` - border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-top: none; - border-radius: 0 0 ${({ theme }) => theme.border.radius.sm} - ${({ theme }) => theme.border.radius.sm}; -`; - export type File = { language: string; content: string; path: string; }; -type CodeEditorProps = Omit & { +type SettingsServerlessFunctionCodeEditorProps = Omit< + EditorProps, + 'onChange' +> & { currentFilePath: string; files: File[]; - onChange?: (value: string) => void; - setIsCodeValid?: (isCodeValid: boolean) => void; + onChange: (value: string) => void; + setIsCodeValid: (isCodeValid: boolean) => void; }; -export const CodeEditor = ({ +export const SettingsServerlessFunctionCodeEditor = ({ currentFilePath, files, onChange, setIsCodeValid, height = 450, options = undefined, -}: CodeEditorProps) => { - const theme = useTheme(); - +}: SettingsServerlessFunctionCodeEditorProps) => { const { availablePackages } = useGetAvailablePackages(); const currentFile = files.find((file) => file.path === currentFilePath); @@ -47,9 +40,6 @@ export const CodeEditor = ({ editor: editor.IStandaloneCodeEditor, monaco: Monaco, ) => { - monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme)); - monaco.editor.setTheme('codeEditorTheme'); - if (files.length > 1) { files.forEach((file) => { const model = monaco.editor.getModel(monaco.Uri.file(file.path)); @@ -82,23 +72,25 @@ export const CodeEditor = ({ ); const environmentDefinition = ` - declare namespace NodeJS { - interface ProcessEnv { - ${Object.keys(environmentVariables) - .map((key) => `${key}: string;`) - .join('\n')} + declare namespace NodeJS { + interface ProcessEnv { + ${Object.keys(environmentVariables) + .map((key) => `${key}: string;`) + .join('\n')} + } } - } - - declare const process: { - env: NodeJS.ProcessEnv; - }; - `; - - monaco.languages.typescript.typescriptDefaults.addExtraLib( - environmentDefinition, - 'ts:process-env.d.ts', - ); + + declare const process: { + env: NodeJS.ProcessEnv; + }; + `; + + monaco.languages.typescript.typescriptDefaults.setExtraLibs([ + { + content: environmentDefinition, + filePath: 'ts:process-env.d.ts', + }, + ]); } await AutoTypings.create(editor, { @@ -124,25 +116,17 @@ export const CodeEditor = ({ return ( isDefined(currentFile) && isDefined(availablePackages) && ( - value && onChange?.(value)} - onValidate={handleEditorValidation} - options={{ - ...options, - overviewRulerLanes: 0, - scrollbar: { - vertical: 'hidden', - horizontal: 'hidden', - }, - minimap: { - enabled: false, - }, - }} - /> + + + ) ); }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx new file mode 100644 index 000000000000..4ad8afaee743 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +const StyledEditorContainer = styled.div` + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-top: none; + border-radius: 0 0 ${({ theme }) => theme.border.radius.sm} + ${({ theme }) => theme.border.radius.sm}; +`; + +export const SettingsServerlessFunctionCodeEditorContainer = + StyledEditorContainer; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionNewForm.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionNewForm.tsx index 46803ca08534..b50b4834c0f4 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionNewForm.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionNewForm.tsx @@ -1,9 +1,8 @@ import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; import { TextArea } from '@/ui/input/components/TextArea'; import { TextInput } from '@/ui/input/components/TextInput'; -import { Section } from '@/ui/layout/section/components/Section'; import styled from '@emotion/styled'; -import { H2Title } from 'twenty-ui'; +import { H2Title, Section } from 'twenty-ui'; const StyledInputsContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx index 4f5744019f25..a67d4d35820c 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx @@ -1,16 +1,16 @@ +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; +import { SettingsPath } from '@/types/SettingsPath'; +import styled from '@emotion/styled'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + Button, EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { IconPlus } from 'twenty-ui'; -import { Button } from '@/ui/input/button/components/Button'; -import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; -import { SettingsPath } from '@/types/SettingsPath'; -import styled from '@emotion/styled'; + IconPlus, +} from 'twenty-ui'; const StyledEmptyFunctionsContainer = styled.div` height: 60vh; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx index 5f8886871359..d9fffdb0600a 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx @@ -1,20 +1,28 @@ +import { + File, + SettingsServerlessFunctionCodeEditor, +} from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor'; +import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId'; import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { Button } from '@/ui/input/button/components/Button'; -import { CodeEditor, File } from '@/ui/input/code-editor/components/CodeEditor'; -import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader'; -import { Section } from '@/ui/layout/section/components/Section'; import { TabList } from '@/ui/layout/tab/components/TabList'; +import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; -import { H2Title, IconGitCommit, IconPlayerPlay, IconRestore } from 'twenty-ui'; +import { + Button, + CoreEditorHeader, + H2Title, + IconGitCommit, + IconPlayerPlay, + IconRestore, + Section, +} from 'twenty-ui'; import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; -import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId'; -import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; -import { useRecoilValue } from 'recoil'; const StyledTabList = styled(TabList)` border-bottom: none; @@ -77,9 +85,11 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ const HeaderTabList = ( { - return { id: file.path, title: file.path.split('/').at(-1) || '' }; - })} + tabs={files + .filter((file) => file.path !== '.env') + .map((file) => { + return { id: file.path, title: file.path.split('/').at(-1) || '' }; + })} /> ); @@ -107,7 +117,7 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ rightNodes={[ResetButton, PublishButton, TestButton]} /> {activeTabId && ( - onChange(activeTabId, newCodeValue)} diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx index a59a56384826..efcf01eca165 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx @@ -1,27 +1,28 @@ import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm'; +import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection'; import { useDeleteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useDeleteOneServerlessFunction'; import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { Button } from '@/ui/input/button/components/Button'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { Section } from '@/ui/layout/section/components/Section'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Key } from 'ts-key-enum'; -import { H2Title } from 'twenty-ui'; +import { Button, H2Title, Section } from 'twenty-ui'; import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; export const SettingsServerlessFunctionSettingsTab = ({ formValues, serverlessFunctionId, onChange, + onCodeChange, }: { formValues: ServerlessFunctionFormValues; serverlessFunctionId: string; onChange: (key: string) => (value: string) => void; + onCodeChange: (filePath: string, value: string) => void; }) => { const navigate = useNavigate(); const [isDeleteFunctionModalOpen, setIsDeleteFunctionModalOpen] = @@ -58,6 +59,10 @@ export const SettingsServerlessFunctionSettingsTab = ({ formValues={formValues} onChange={onChange} /> +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx index b2d54cbc03f9..863483c3fde4 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx @@ -1,7 +1,14 @@ -import { Section } from '@/ui/layout/section/components/Section'; -import { H2Title, IconPlayerPlay } from 'twenty-ui'; +import { + Button, + CodeEditor, + CoreEditorHeader, + H2Title, + IconPlayerPlay, + Section, +} from 'twenty-ui'; import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton'; +import { SettingsServerlessFunctionCodeEditorContainer } from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer'; import { SettingsServerlessFunctionsOutputMetadataInfo } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsOutputMetadataInfo'; import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState'; import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState'; @@ -9,9 +16,6 @@ import { settingsServerlessFunctionOutputState } from '@/settings/serverless-fun import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { Button } from '@/ui/input/button/components/Button'; -import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor'; -import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; @@ -78,37 +82,30 @@ export const SettingsServerlessFunctionTestTab = ({ />, ]} /> - + + +
]} rightNodes={[]} /> - + + +
diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts index bbcad2f1c31f..af00ef49b608 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts @@ -8,6 +8,11 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql` runtime syncStatus latestVersion + latestVersionInputSchema { + name + type + } + publishedVersions createdAt updatedAt } diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction.ts index 5b2b0325b080..b5b568f7ea85 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction.ts @@ -3,7 +3,7 @@ import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/gr export const DELETE_ONE_SERVERLESS_FUNCTION = gql` ${SERVERLESS_FUNCTION_FRAGMENT} - mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) { + mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) { deleteOneServerlessFunction(input: $input) { ...ServerlessFunctionFields } diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findManyServerlessFunctions.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findManyServerlessFunctions.ts index dfa0ca155893..37aeccc6db68 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findManyServerlessFunctions.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findManyServerlessFunctions.ts @@ -4,12 +4,8 @@ import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/gr export const FIND_MANY_SERVERLESS_FUNCTIONS = gql` ${SERVERLESS_FUNCTION_FRAGMENT} query GetManyServerlessFunctions { - serverlessFunctions(paging: { first: 100 }) { - edges { - node { - ...ServerlessFunctionFields - } - } + findManyServerlessFunctions { + ...ServerlessFunctionFields } } `; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findOneServerlessFunction.ts index f5ed4d3f273d..142c8276315c 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findOneServerlessFunction.ts @@ -3,8 +3,8 @@ import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/gr export const FIND_ONE_SERVERLESS_FUNCTION = gql` ${SERVERLESS_FUNCTION_FRAGMENT} - query GetOneServerlessFunction($id: UUID!) { - serverlessFunction(id: $id) { + query GetOneServerlessFunction($input: ServerlessFunctionIdInput!) { + findOneServerlessFunction(input: $input) { ...ServerlessFunctionFields } } diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts index 427f98466e35..5627feea7640 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts @@ -1,14 +1,14 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; +import { CREATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/createOneServerlessFunction'; +import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; +import { getOperationName } from '@apollo/client/utilities'; import { - CreateServerlessFunctionInput, CreateOneServerlessFunctionItemMutation, CreateOneServerlessFunctionItemMutationVariables, + CreateServerlessFunctionInput, } from '~/generated-metadata/graphql'; -import { getOperationName } from '@apollo/client/utilities'; -import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; -import { CREATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/createOneServerlessFunction'; export const useCreateOneServerlessFunction = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -16,7 +16,7 @@ export const useCreateOneServerlessFunction = () => { CreateOneServerlessFunctionItemMutation, CreateOneServerlessFunctionItemMutationVariables >(CREATE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const createOneServerlessFunction = async ( diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts index db1c3f5a68f3..1c3afaf03384 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts @@ -1,13 +1,13 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; -import { ApolloClient, useMutation } from '@apollo/client'; -import { getOperationName } from '@apollo/client/utilities'; import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction'; +import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; +import { useMutation } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities'; import { - DeleteServerlessFunctionInput, DeleteOneServerlessFunctionMutation, DeleteOneServerlessFunctionMutationVariables, + ServerlessFunctionIdInput, } from '~/generated-metadata/graphql'; -import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; export const useDeleteOneServerlessFunction = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -15,11 +15,11 @@ export const useDeleteOneServerlessFunction = () => { DeleteOneServerlessFunctionMutation, DeleteOneServerlessFunctionMutationVariables >(DELETE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const deleteOneServerlessFunction = async ( - input: DeleteServerlessFunctionInput, + input: ServerlessFunctionIdInput, ) => { return await mutate({ variables: { diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useExecuteOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useExecuteOneServerlessFunction.ts index 8825a68d9255..4b69a953e96e 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useExecuteOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useExecuteOneServerlessFunction.ts @@ -1,10 +1,10 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; -import { ApolloClient, useMutation } from '@apollo/client'; import { EXECUTE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/executeOneServerlessFunction'; +import { useMutation } from '@apollo/client'; import { - ExecuteServerlessFunctionInput, ExecuteOneServerlessFunctionMutation, ExecuteOneServerlessFunctionMutationVariables, + ExecuteServerlessFunctionInput, } from '~/generated-metadata/graphql'; export const useExecuteOneServerlessFunction = () => { @@ -13,7 +13,7 @@ export const useExecuteOneServerlessFunction = () => { ExecuteOneServerlessFunctionMutation, ExecuteOneServerlessFunctionMutationVariables >(EXECUTE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const executeOneServerlessFunction = async ( diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetManyServerlessFunctions.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetManyServerlessFunctions.ts index eff46a628ed8..4023745907fc 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetManyServerlessFunctions.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetManyServerlessFunctions.ts @@ -15,7 +15,6 @@ export const useGetManyServerlessFunctions = () => { client: apolloMetadataClient ?? undefined, }); return { - serverlessFunctions: - data?.serverlessFunctions?.edges.map(({ node }) => node) || [], + serverlessFunctions: data?.findManyServerlessFunctions || [], }; }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts index 8d22a217455d..1aeda466d589 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts @@ -2,11 +2,14 @@ import { useQuery } from '@apollo/client'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { FIND_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunction'; import { + ServerlessFunctionIdInput, GetOneServerlessFunctionQuery, GetOneServerlessFunctionQueryVariables, } from '~/generated-metadata/graphql'; -export const useGetOneServerlessFunction = (id: string) => { +export const useGetOneServerlessFunction = ( + input: ServerlessFunctionIdInput, +) => { const apolloMetadataClient = useApolloMetadataClient(); const { data } = useQuery< GetOneServerlessFunctionQuery, @@ -14,10 +17,10 @@ export const useGetOneServerlessFunction = (id: string) => { >(FIND_ONE_SERVERLESS_FUNCTION, { client: apolloMetadataClient ?? undefined, variables: { - id, + input, }, }); return { - serverlessFunction: data?.serverlessFunction || null, + serverlessFunction: data?.findOneServerlessFunction || null, }; }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePublishOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePublishOneServerlessFunction.ts index 02b8a3e1fece..364eefd75acc 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePublishOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePublishOneServerlessFunction.ts @@ -1,13 +1,13 @@ -import { ApolloClient, useMutation } from '@apollo/client'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { PUBLISH_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/publishOneServerlessFunction'; +import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; +import { useMutation } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities'; import { - PublishServerlessFunctionInput, PublishOneServerlessFunctionMutation, PublishOneServerlessFunctionMutationVariables, + PublishServerlessFunctionInput, } from '~/generated-metadata/graphql'; -import { getOperationName } from '@apollo/client/utilities'; -import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; export const usePublishOneServerlessFunction = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -15,7 +15,7 @@ export const usePublishOneServerlessFunction = () => { PublishOneServerlessFunctionMutation, PublishOneServerlessFunctionMutationVariables >(PUBLISH_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const publishOneServerlessFunction = async ( diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts index 9e8a13483810..2d377ac78d8e 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts @@ -29,8 +29,9 @@ export const useServerlessFunctionUpdateFormState = ( code: undefined, }); - const { serverlessFunction } = - useGetOneServerlessFunction(serverlessFunctionId); + const { serverlessFunction } = useGetOneServerlessFunction({ + id: serverlessFunctionId, + }); const { loading } = useGetOneServerlessFunctionSourceCode({ id: serverlessFunctionId, diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts index d75529ebcc97..61c0a52c4499 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts @@ -1,13 +1,13 @@ -import { ApolloClient, useMutation } from '@apollo/client'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { UPDATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/updateOneServerlessFunction'; +import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; +import { useMutation } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities'; import { - UpdateServerlessFunctionInput, UpdateOneServerlessFunctionMutation, UpdateOneServerlessFunctionMutationVariables, + UpdateServerlessFunctionInput, } from '~/generated-metadata/graphql'; -import { getOperationName } from '@apollo/client/utilities'; -import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; export const useUpdateOneServerlessFunction = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -15,7 +15,7 @@ export const useUpdateOneServerlessFunction = () => { UpdateOneServerlessFunctionMutation, UpdateOneServerlessFunctionMutationVariables >(UPDATE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient ?? ({} as ApolloClient), + client: apolloMetadataClient, }); const updateOneServerlessFunction = async ( diff --git a/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx b/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx index 2b700540dd76..f38d318b375d 100644 --- a/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx +++ b/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx @@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { Toggle } from '@/ui/input/components/Toggle'; +import { Toggle } from 'twenty-ui'; import { useUpdateWorkspaceMutation } from '~/generated/graphql'; export const ToggleImpersonate = () => { diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx index 1ce2f5affe9c..ae4a08c67d15 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx @@ -1,11 +1,13 @@ import styled from '@emotion/styled'; -import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -import { ViewType } from '@/views/types/ViewType'; const StyledContainer = styled.div` display: flex; @@ -20,32 +22,54 @@ export const SignInBackgroundMockContainer = () => { const recordIndexId = 'sign-up-mock-record-table-id'; const viewBarId = 'companies-mock'; + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + return ( - - {}} - optionsDropdownButton={ - - } - /> - - {}} - /> - + {}, + indexIdentifierUrl: () => '', + onCreateRecord: () => {}, + }} + > + + + + {}} + optionsDropdownButton={<>} + /> + + {}} + /> + + + + ); }; diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx index 2b17a655549d..9b36b1541abd 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx @@ -3,11 +3,11 @@ import { IconBuildingSkyscraper } from 'twenty-ui'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { SignInBackgroundMockContainer } from '@/sign-in-background-mock/components/SignInBackgroundMockContainer'; -import { PageAddButton } from '@/ui/layout/page/PageAddButton'; -import { PageBody } from '@/ui/layout/page/PageBody'; -import { PageContainer } from '@/ui/layout/page/PageContainer'; -import { PageHeader } from '@/ui/layout/page/PageHeader'; -import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect'; +import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; +import { PageBody } from '@/ui/layout/page/components/PageBody'; +import { PageContainer } from '@/ui/layout/page/components/PageContainer'; +import { PageHeader } from '@/ui/layout/page/components/PageHeader'; +import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect'; const StyledTableContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts index 085e9a829d10..2023b4d0e100 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts +++ b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts @@ -88,35 +88,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: null, __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [ - { - __typename: 'ActivityTargetEdge', - node: { - __typename: 'ActivityTarget', - id: '97114d7e-2a80-4401-af58-36c88e13e852', - activityId: '737a6c31-610a-457b-b087-791ac700fa46', - createdAt: '2023-11-24T13:15:03.523Z', - updatedAt: '2023-11-24T13:15:03.523Z', - companyId: '04b2e9f5-0713-40a5-8216-82802401d33e', - personId: null, - }, - }, - { - __typename: 'ActivityTargetEdge', - node: { - __typename: 'ActivityTarget', - id: 'cb29d37a-3d5e-4efb-afa3-38f4bff69912', - activityId: '3c6ea4a3-f71d-4c31-9dfa-f868a5de4091', - createdAt: '2023-11-24T13:14:57.628Z', - updatedAt: '2023-11-24T13:14:57.628Z', - companyId: '04b2e9f5-0713-40a5-8216-82802401d33e', - personId: null, - }, - }, - ], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -339,10 +310,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -489,10 +456,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -554,10 +517,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -755,10 +714,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -820,10 +775,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -885,10 +836,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -950,10 +897,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -1048,10 +991,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -1146,10 +1085,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -1211,10 +1146,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -1276,10 +1207,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, @@ -1426,10 +1353,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ secondaryLinks: [], __typename: 'Links', }, - activityTargets: { - __typename: 'ActivityTargetConnection', - edges: [], - }, annualRecurringRevenue: { __typename: 'Currency', amountMicros: null, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/tests/mockRsiValues.ts b/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts similarity index 100% rename from packages/twenty-front/src/modules/spreadsheet-import/tests/mockRsiValues.ts rename to packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx index 9908d66a94e9..217ab31d54f6 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/MatchColumnSelect.tsx @@ -1,5 +1,3 @@ -import React, { useCallback, useRef, useState } from 'react'; -import { createPortal } from 'react-dom'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { @@ -9,7 +7,9 @@ import { size, useFloating, } from '@floating-ui/react'; -import { AppTooltip } from 'twenty-ui'; +import React, { useCallback, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { AppTooltip, MenuItem, MenuItemSelect } from 'twenty-ui'; import { ReadonlyDeep } from 'type-fest'; import { useDebouncedCallback } from 'use-debounce'; @@ -18,8 +18,6 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useUpdateEffect } from '~/hooks/useUpdateEffect'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx index 5c03dc535678..a04d344a9d78 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; -import { IconX } from 'twenty-ui'; +import { IconButton, IconX } from 'twenty-ui'; import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager'; -import { IconButton } from '@/ui/input/button/components/IconButton'; import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar'; const StyledCloseButtonContainer = styled.div` diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx index 6462fcd8c666..9064cf9bfccb 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx @@ -1,7 +1,6 @@ import styled from '@emotion/styled'; -import { CircularProgressBar } from '@/ui/feedback/progress-bar/components/CircularProgressBar'; -import { MainButton } from '@/ui/input/button/components/MainButton'; +import { CircularProgressBar, MainButton } from 'twenty-ui'; import { Modal } from '@/ui/layout/modal/components/Modal'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx index dc1d67124a72..bec306a77ddb 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx @@ -1,4 +1,4 @@ -import { act, renderHook } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import { RecoilRoot, useRecoilState } from 'recoil'; import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog'; @@ -8,6 +8,7 @@ import { ImportedRow, SpreadsheetImportDialogOptions, } from '@/spreadsheet-import/types'; +import { act } from 'react'; const Wrapper = ({ children }: { children: React.ReactNode }) => ( {children} diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx index da8761b370f2..59e8d9e6fbf6 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx @@ -2,7 +2,7 @@ import { Column, FormatterProps, useRowSelection } from 'react-data-grid'; import { ImportedRow } from '@/spreadsheet-import/types'; -import { Radio } from '@/ui/input/components/Radio'; +import { Radio } from 'twenty-ui'; const SELECT_COLUMN_KEY = 'select-row'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx index a6d5165f3cad..7b9c635c0d23 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep.tsx @@ -8,8 +8,7 @@ import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/Spreadsh import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords'; import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook'; -import { Radio } from '@/ui/input/components/Radio'; -import { RadioGroup } from '@/ui/input/components/RadioGroup'; +import { Radio, RadioGroup } from 'twenty-ui'; import { Modal } from '@/ui/layout/modal/components/Modal'; import { WorkBook } from 'xlsx-ugnis'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx index c3bdf3a5d9d9..e8c3d52df053 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx @@ -3,10 +3,10 @@ import styled from '@emotion/styled'; import { useCallback, useState } from 'react'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; -import { CircularProgressBar } from '@/ui/feedback/progress-bar/components/CircularProgressBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Modal } from '@/ui/layout/modal/components/Modal'; +import { CircularProgressBar } from 'twenty-ui'; import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx index 534deb1e0b8a..e0d7ac6a2255 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx @@ -7,7 +7,7 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { MainButton } from '@/ui/input/button/components/MainButton'; +import { MainButton } from 'twenty-ui'; const StyledContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx index 21b6d034923c..12e89fd55021 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx @@ -1,36 +1,32 @@ -import styled from '@emotion/styled'; -import { - Dispatch, - SetStateAction, - useCallback, - useMemo, - useState, -} from 'react'; -// @ts-expect-error Todo: remove usage of react-data-grid` -import { RowsChangeData } from 'react-data-grid'; -import { IconTrash } from 'twenty-ui'; - import { Heading } from '@/spreadsheet-import/components/Heading'; import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable'; import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { - Columns, ColumnType, + Columns, } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; +import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; +import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; import { - ImportedStructuredRow, ImportValidationResult, + ImportedStructuredRow, } from '@/spreadsheet-import/types'; import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager'; -import { Button } from '@/ui/input/button/components/Button'; -import { Toggle } from '@/ui/input/components/Toggle'; -import { isDefined } from '~/utils/isDefined'; - -import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; -import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; import { Modal } from '@/ui/layout/modal/components/Modal'; +import styled from '@emotion/styled'; +import { + Dispatch, + SetStateAction, + useCallback, + useMemo, + useState, +} from 'react'; +// @ts-expect-error Todo: remove usage of react-data-grid` +import { RowsChangeData } from 'react-data-grid'; +import { Button, IconTrash, Toggle } from 'twenty-ui'; +import { isDefined } from '~/utils/isDefined'; import { generateColumns } from './components/columns'; import { ImportedStructuredRowMetadata } from './types'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx index f2aa7983f410..cc2f45072523 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx @@ -2,13 +2,11 @@ import styled from '@emotion/styled'; // @ts-expect-error // Todo: remove usage of react-data-grid import { Column, useRowSelection } from 'react-data-grid'; import { createPortal } from 'react-dom'; -import { AppTooltip } from 'twenty-ui'; +import { AppTooltip, Checkbox, CheckboxVariant, Toggle } from 'twenty-ui'; import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; import { Fields, ImportedStructuredRow } from '@/spreadsheet-import/types'; -import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox'; import { TextInput } from '@/ui/input/components/TextInput'; -import { Toggle } from '@/ui/input/components/Toggle'; import { isDefined } from '~/utils/isDefined'; import { ImportedStructuredRowMetadata } from '../types'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx index 47cb4e54d0b8..ac1a4da5acb8 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx @@ -1,10 +1,10 @@ import { Meta } from '@storybook/react'; +import { mockRsiValues } from '@/spreadsheet-import/__mocks__/mockRsiValues'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; import { MatchColumnsStep } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; -import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectHeader.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectHeader.stories.tsx index a87f0ce4226c..ad33171faa3f 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectHeader.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectHeader.stories.tsx @@ -1,13 +1,13 @@ import { Meta } from '@storybook/react'; +import { + headerSelectionTableFields, + mockRsiValues, +} from '@/spreadsheet-import/__mocks__/mockRsiValues'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; import { SelectHeaderStep } from '@/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; -import { - headerSelectionTableFields, - mockRsiValues, -} from '@/spreadsheet-import/tests/mockRsiValues'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectSheet.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectSheet.stories.tsx index e48542f1a089..57b5162793cb 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectSheet.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectSheet.stories.tsx @@ -1,10 +1,10 @@ import { Meta } from '@storybook/react'; +import { mockRsiValues } from '@/spreadsheet-import/__mocks__/mockRsiValues'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; import { SelectSheetStep } from '@/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; -import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Upload.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Upload.stories.tsx index 7f2b295fb545..0757b7e619d4 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Upload.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Upload.stories.tsx @@ -1,10 +1,10 @@ import { Meta } from '@storybook/react'; +import { mockRsiValues } from '@/spreadsheet-import/__mocks__/mockRsiValues'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; import { UploadStep } from '@/spreadsheet-import/steps/components/UploadStep/UploadStep'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; -import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx index 9126371d1dc2..58894c817ce0 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx @@ -1,13 +1,13 @@ import { Meta } from '@storybook/react'; -import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; -import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; -import { ValidationStep } from '@/spreadsheet-import/steps/components/ValidationStep/ValidationStep'; import { editableTableInitialData, importedColums, mockRsiValues, -} from '@/spreadsheet-import/tests/mockRsiValues'; +} from '@/spreadsheet-import/__mocks__/mockRsiValues'; +import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; +import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; +import { ValidationStep } from '@/spreadsheet-import/steps/components/ValidationStep/ValidationStep'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/support/components/SupportButton.tsx b/packages/twenty-front/src/modules/support/components/SupportButton.tsx index 9229232b6c4e..2c38dccdcf3c 100644 --- a/packages/twenty-front/src/modules/support/components/SupportButton.tsx +++ b/packages/twenty-front/src/modules/support/components/SupportButton.tsx @@ -1,9 +1,10 @@ import styled from '@emotion/styled'; -import { IconHelpCircle } from 'twenty-ui'; +import { Button, IconHelpCircle, LightIconButton } from 'twenty-ui'; import { SupportButtonSkeletonLoader } from '@/support/components/SupportButtonSkeletonLoader'; import { useSupportChat } from '@/support/hooks/useSupportChat'; -import { Button } from '@/ui/input/button/components/Button'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { useRecoilValue } from 'recoil'; const StyledButtonContainer = styled.div` display: flex; @@ -12,11 +13,18 @@ const StyledButtonContainer = styled.div` export const SupportButton = () => { const { loading, isFrontChatLoaded } = useSupportChat(); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); if (loading) { return ; } - return isFrontChatLoaded ? ( + if (!isFrontChatLoaded) { + return; + } + + return isNavigationDrawerExpanded ? ( - + <> + {!isMobile && !isInRightDrawer && ( + + {summaryCard} + {fieldsCard} + )} - + + + + + {(isMobile || isInRightDrawer) && summaryCard} + + {renderActiveTabContent()} + + {isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && ( + + + + )} + + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx index e723f0ae84ad..93a96eb46a86 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx @@ -1,5 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { ReactElement } from 'react'; import { IconComponent, Pill } from 'twenty-ui'; type TabProps = { @@ -10,7 +11,7 @@ type TabProps = { className?: string; onClick?: () => void; disabled?: boolean; - pill?: string; + pill?: string | ReactElement; }; const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>` @@ -73,7 +74,7 @@ export const Tab = ({ {Icon && } {title} - {pill && } + {pill && typeof pill === 'string' ? : pill} ); diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index 349c9cfe3edd..d0936c28fed5 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -9,13 +9,13 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { Tab } from './Tab'; -type SingleTabProps = { +export type SingleTabProps = { title: string; Icon?: IconComponent; id: string; hide?: boolean; disabled?: boolean; - pill?: string; + pill?: string | React.ReactElement; }; type TabListProps = { @@ -31,7 +31,6 @@ const StyledContainer = styled.div` display: flex; gap: ${({ theme }) => theme.spacing(2)}; height: 40px; - padding-left: ${({ theme }) => theme.spacing(2)}; user-select: none; `; @@ -53,7 +52,7 @@ export const TabList = ({ return ( - + {tabs .filter((tab) => !tab.hide) diff --git a/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx b/packages/twenty-front/src/modules/ui/layout/top-bar/components/TopBar.tsx similarity index 80% rename from packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx rename to packages/twenty-front/src/modules/ui/layout/top-bar/components/TopBar.tsx index f19e3efaf685..004236cc337f 100644 --- a/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx +++ b/packages/twenty-front/src/modules/ui/layout/top-bar/components/TopBar.tsx @@ -1,5 +1,5 @@ -import { ReactNode } from 'react'; import styled from '@emotion/styled'; +import { ReactNode } from 'react'; type TopBarProps = { className?: string; @@ -10,14 +10,16 @@ type TopBarProps = { }; const StyledContainer = styled.div` + border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`}; display: flex; + margin-left: ${({ theme }) => theme.spacing(2)}; + flex-direction: column; `; -const StyledTopBar = styled.div<{ displayBottomBorder: boolean }>` +const StyledTopBar = styled.div` align-items: center; - border-bottom: ${({ displayBottomBorder, theme }) => - displayBottomBorder ? `1px solid ${theme.border.color.light}` : 'none'}; + box-sizing: border-box; color: ${({ theme }) => theme.font.color.secondary}; display: flex; @@ -26,6 +28,7 @@ const StyledTopBar = styled.div<{ displayBottomBorder: boolean }>` height: 39px; justify-content: space-between; padding-right: ${({ theme }) => theme.spacing(2)}; + z-index: 7; `; @@ -44,10 +47,9 @@ export const TopBar = ({ leftComponent, rightComponent, bottomComponent, - displayBottomBorder = true, }: TopBarProps) => ( - + {leftComponent} {rightComponent} diff --git a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/Breadcrumb.tsx b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/Breadcrumb.tsx index a43ee3175fc2..7440c42c6bda 100644 --- a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/Breadcrumb.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/Breadcrumb.tsx @@ -1,3 +1,5 @@ +import { MobileBreadcrumb } from '@/ui/navigation/bread-crumb/components/MobileBreadcrumb'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import styled from '@emotion/styled'; import { Fragment, ReactNode } from 'react'; import { Link } from 'react-router-dom'; @@ -39,6 +41,12 @@ const StyledDivider = styled.span` `; export const Breadcrumb = ({ className, links }: BreadcrumbProps) => { + const isMobile = useIsMobile(); + + if (isMobile && links.length > 0) { + return ; + } + return ( {links.map((link, index) => { diff --git a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx new file mode 100644 index 000000000000..5df3d09cacb0 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/MobileBreadcrumb.tsx @@ -0,0 +1,81 @@ +import { useOpenSettingsMenu } from '@/navigation/hooks/useOpenSettings'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { isNonEmptyString } from '@sniptt/guards'; +import { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; +import { IconChevronLeft } from 'twenty-ui'; + +export type MobileBreadcrumbProps = { + className?: string; + links: { children: string | ReactNode; href?: string }[]; +}; + +const StyledWrapper = styled.nav` + align-items: center; + color: ${({ theme }) => theme.font.color.tertiary}; + display: grid; + font-size: ${({ theme }) => theme.font.size.md}; + grid-auto-flow: column; + grid-column-gap: ${({ theme }) => theme.spacing(1)}; + max-width: 100%; + min-width: 0; + height: ${({ theme }) => theme.spacing(8)}; +`; + +const StyledLink = styled(Link)` + color: inherit; + text-decoration: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const StyledText = styled.span` + color: inherit; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const MobileBreadcrumb = ({ + className, + links, +}: MobileBreadcrumbProps) => { + const theme = useTheme(); + + const { openSettingsMenu } = useOpenSettingsMenu(); + + const handleBackToSettingsClick = () => { + openSettingsMenu(); + }; + + const previousLink = links[links.length - 2]; + const shouldRedirectToSettings = links.length === 2; + + const text = isNonEmptyString(previousLink.children) + ? previousLink.children + : ''; + + return ( + + {shouldRedirectToSettings ? ( + <> + + + Back to Settings + + + ) : previousLink?.href ? ( + <> + + + Back to {previousLink.children} + + + ) : ( + {previousLink?.children} + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/Breadcrumb.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/Breadcrumb.stories.tsx index a655fc6b6fd3..cdc389a0a426 100644 --- a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/Breadcrumb.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/Breadcrumb.stories.tsx @@ -1,10 +1,9 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from 'twenty-ui'; +import { Breadcrumb } from '../Breadcrumb'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; -import { Breadcrumb } from '../Breadcrumb'; - const meta: Meta = { title: 'UI/Navigation/Breadcrumb/Breadcrumb', component: Breadcrumb, diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemWithOptionDropdown.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemWithOptionDropdown.tsx new file mode 100644 index 000000000000..d1196392f170 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemWithOptionDropdown.tsx @@ -0,0 +1,102 @@ +import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { useTheme } from '@emotion/react'; +import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react'; +import { + IconChevronRight, + IconComponent, + IconDotsVertical, + LightIconButton, + LightIconButtonProps, + MenuItemAccent, + MenuItemLeftContent, + StyledHoverableMenuItemBase, + StyledMenuItemLeftContent, +} from 'twenty-ui'; + +export type MenuItemIconButton = { + Wrapper?: FunctionComponent<{ iconButton: ReactElement }>; + Icon: IconComponent; + accent?: LightIconButtonProps['accent']; + onClick?: (event: MouseEvent) => void; +}; + +export type MenuItemWithOptionDropdownProps = { + accent?: MenuItemAccent; + className?: string; + dropdownContent: ReactNode; + dropdownId: string; + isIconDisplayedOnHoverOnly?: boolean; + isTooltipOpen?: boolean; + LeftIcon?: IconComponent | null; + RightIcon?: IconComponent | null; + onClick?: (event: MouseEvent) => void; + onMouseEnter?: (event: MouseEvent) => void; + onMouseLeave?: (event: MouseEvent) => void; + testId?: string; + text: ReactNode; + hasSubMenu?: boolean; +}; + +// TODO: refactor this +export const MenuItemWithOptionDropdown = ({ + accent = 'default', + className, + isIconDisplayedOnHoverOnly = true, + dropdownContent, + dropdownId, + LeftIcon, + RightIcon, + onClick, + onMouseEnter, + onMouseLeave, + testId, + text, + hasSubMenu = false, +}: MenuItemWithOptionDropdownProps) => { + const theme = useTheme(); + + const handleMenuItemClick = (event: MouseEvent) => { + if (!onClick) return; + event.preventDefault(); + event.stopPropagation(); + + onClick?.(event); + }; + + return ( + + + + +
+ + } + dropdownComponents={dropdownContent} + dropdownId={dropdownId} + dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }} + disableBlur + /> +
+ {hasSubMenu && ( + + )} +
+ ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx index 2dba91b685c8..ee7c61264102 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx @@ -5,12 +5,11 @@ import { IconList, IconSearch, IconSettings, + NavigationBar, } from 'twenty-ui'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; -import { NavigationBar } from '../NavigationBar'; - const meta: Meta = { title: 'UI/Navigation/NavigationBar/NavigationBar', component: NavigationBar, diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx index 25d934045851..3a03e32f7def 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx @@ -1,19 +1,19 @@ -import { useState } from 'react'; -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; -import { IconChevronDown } from 'twenty-ui'; - import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { Workspaces } from '@/auth/states/workspaces'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar'; +import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MulitWorkspaceDropdownId'; import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching'; import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { IconChevronDown, MenuItemSelectAvatar } from 'twenty-ui'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; const StyledLogo = styled.div<{ logo: string }>` @@ -25,7 +25,7 @@ const StyledLogo = styled.div<{ logo: string }>` width: 16px; `; -const StyledContainer = styled.div` +const StyledContainer = styled.div<{ isNavigationDrawerExpanded: boolean }>` align-items: center; cursor: pointer; color: ${({ theme }) => theme.font.color.primary}; @@ -33,12 +33,13 @@ const StyledContainer = styled.div` border: 1px solid transparent; display: flex; justify-content: space-between; - height: ${({ theme }) => theme.spacing(5)}; + height: ${({ theme, isNavigationDrawerExpanded }) => + isNavigationDrawerExpanded ? theme.spacing(5) : theme.spacing(4)}; padding: calc(${({ theme }) => theme.spacing(1)} - 1px); - width: 100%; - - gap: ${({ theme }) => theme.spacing(1)}; - + width: ${({ isNavigationDrawerExpanded }) => + isNavigationDrawerExpanded ? '100%' : 'auto'}; + gap: ${({ theme, isNavigationDrawerExpanded }) => + isNavigationDrawerExpanded ? theme.spacing(1) : '0'}; &:hover { background-color: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -51,8 +52,10 @@ const StyledLabel = styled.div` `; const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>` + align-items: center; color: ${({ disabled, theme }) => disabled ? theme.font.color.extraLight : theme.font.color.tertiary}; + display: flex; `; type MultiWorkspaceDropdownButtonProps = { @@ -77,6 +80,9 @@ export const MultiWorkspaceDropdownButton = ({ closeDropdown(); await switchWorkspace(workspaceId); }; + const [isNavigationDrawerExpanded] = useRecoilState( + isNavigationDrawerExpandedState, + ); return ( + - {currentWorkspace?.displayName ?? ''} - + + {currentWorkspace?.displayName ?? ''} + + + + } dropdownComponents={ diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx index 997441038d29..ded8a5772cc7 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx @@ -1,16 +1,16 @@ -import { css, useTheme } from '@emotion/react'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; import { ReactNode, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { MOBILE_VIEWPORT } from 'twenty-ui'; -import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; -import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { DESKTOP_NAV_DRAWER_WIDTHS } from '../constants/DesktopNavDrawerWidths'; +import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; +import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; +import { isNavigationDrawerExpandedState } from '../../states/isNavigationDrawerExpanded'; import { NavigationDrawerBackButton } from './NavigationDrawerBackButton'; import { NavigationDrawerHeader } from './NavigationDrawerHeader'; @@ -18,32 +18,31 @@ export type NavigationDrawerProps = { children: ReactNode; className?: string; footer?: ReactNode; - isSubMenu?: boolean; logo?: string; title?: string; }; -const StyledAnimatedContainer = styled(motion.div)` - display: flex; - justify-content: end; +const StyledAnimatedContainer = styled(motion.div)<{ isSettings?: boolean }>` + max-height: 100vh; + overflow: ${({ isSettings }) => (isSettings ? 'visible' : 'hidden')}; `; -const StyledContainer = styled.div<{ isSubMenu?: boolean }>` +const StyledContainer = styled.div<{ + isSettings?: boolean; + isMobile?: boolean; +}>` box-sizing: border-box; display: flex; flex-direction: column; + width: ${NAV_DRAWER_WIDTHS.menu.desktop.expanded}px; gap: ${({ theme }) => theme.spacing(3)}; height: 100%; - min-width: ${DESKTOP_NAV_DRAWER_WIDTHS.menu}px; - padding: ${({ theme }) => theme.spacing(3, 2, 4)}; - - ${({ isSubMenu, theme }) => - isSubMenu - ? css` - padding-left: ${theme.spacing(0)}; - padding-right: ${theme.spacing(8)}; - ` - : ''} + padding: ${({ theme, isSettings, isMobile }) => + isSettings + ? isMobile + ? theme.spacing(3, 8) + : theme.spacing(3, 8, 4, 0) + : theme.spacing(3, 2, 4)}; @media (max-width: ${MOBILE_VIEWPORT}px) { width: 100%; @@ -51,25 +50,29 @@ const StyledContainer = styled.div<{ isSubMenu?: boolean }>` padding-right: 20px; } `; -const StyledItemsContainer = styled.div` + +const StyledItemsContainer = styled.div<{ isSettings?: boolean }>` display: flex; flex-direction: column; margin-bottom: auto; + overflow: ${({ isSettings }) => (isSettings ? 'visible' : 'hidden')}; + flex: 1; `; export const NavigationDrawer = ({ children, className, footer, - isSubMenu, logo, title, }: NavigationDrawerProps) => { const [isHovered, setIsHovered] = useState(false); const isMobile = useIsMobile(); + const isSettingsDrawer = useIsSettingsDrawer(); const theme = useTheme(); - const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState); - const isSettingsPage = useIsSettingsPage(); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); const handleHover = () => { setIsHovered(true); @@ -79,30 +82,36 @@ export const NavigationDrawer = ({ setIsHovered(false); }; - const desktopWidth = !isNavigationDrawerOpen - ? 12 - : DESKTOP_NAV_DRAWER_WIDTHS.menu; + const desktopWidth = isNavigationDrawerExpanded + ? NAV_DRAWER_WIDTHS.menu.desktop.expanded + : NAV_DRAWER_WIDTHS.menu.desktop.collapsed; - const mobileWidth = isNavigationDrawerOpen ? '100%' : 0; + const mobileWidth = isNavigationDrawerExpanded + ? NAV_DRAWER_WIDTHS.menu.mobile.expanded + : NAV_DRAWER_WIDTHS.menu.mobile.collapsed; + + const navigationDrawerAnimate = { + width: isMobile ? mobileWidth : desktopWidth, + opacity: isNavigationDrawerExpanded || !isSettingsDrawer ? 1 : 0, + }; return ( - {isSubMenu && title ? ( + {isSettingsDrawer && title ? ( !isMobile && ) : ( )} - {children} + + {children} + {footer} diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx new file mode 100644 index 000000000000..732da1418997 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx @@ -0,0 +1,53 @@ +import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { AnimationControls, motion, TargetAndTransition } from 'framer-motion'; +import { useRecoilValue } from 'recoil'; + +const StyledAnimatedContainer = styled(motion.span)` + display: block; +`; + +export const NavigationDrawerAnimatedCollapseWrapper = ({ + children, +}: { + children: React.ReactNode; +}) => { + const theme = useTheme(); + const isSettingsPage = useIsSettingsPage(); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); + + if (isSettingsPage) { + return children; + } + + const animate: AnimationControls | TargetAndTransition = + isNavigationDrawerExpanded + ? { + opacity: 1, + width: 'auto', + height: 'auto', + pointerEvents: 'auto', + } + : { + opacity: 0, + width: 0, + height: 0, + pointerEvents: 'none', + }; + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx index 96490d7f0a7e..ec0c5bcfe061 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx @@ -1,9 +1,10 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; -import { IconX } from 'twenty-ui'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { IconX, UndecoratedLink } from 'twenty-ui'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; type NavigationDrawerBackButtonProps = { @@ -43,9 +44,22 @@ export const NavigationDrawerBackButton = ({ const theme = useTheme(); const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState); + const setIsNavigationDrawerExpanded = useSetRecoilState( + isNavigationDrawerExpandedState, + ); + const navigationDrawerExpandedMemorized = useRecoilValue( + navigationDrawerExpandedMemorizedState, + ); + return ( - + + setIsNavigationDrawerExpanded(navigationDrawerExpandedMemorized) + } + > theme.border.radius.md}; color: ${({ theme }) => theme.font.color.light}; cursor: pointer; display: flex; - height: ${({ theme }) => theme.spacing(5)}; justify-content: center; user-select: none; - width: ${({ theme }) => theme.spacing(6)}; - - &:hover { - background: ${({ theme }) => theme.background.quaternary}; - } `; type NavigationDrawerCollapseButtonProps = { @@ -33,24 +26,26 @@ export const NavigationDrawerCollapseButton = ({ className, direction = 'left', }: NavigationDrawerCollapseButtonProps) => { - const setIsNavigationDrawerOpen = useSetRecoilState( - isNavigationDrawerOpenState, + const setIsNavigationDrawerExpanded = useSetRecoilState( + isNavigationDrawerExpandedState, ); return ( - setIsNavigationDrawerOpen((previousIsOpen) => !previousIsOpen) + setIsNavigationDrawerExpanded( + (previousIsExpanded) => !previousIsExpanded, + ) } > - diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx index 03ae1fcd0ada..52477ff39f93 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx @@ -7,17 +7,21 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { isNonEmptyString } from '@sniptt/guards'; import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton'; -const StyledContainer = styled.div<{ isMultiWorkspace: boolean }>` +const StyledContainer = styled.div` align-items: center; display: flex; - gap: ${({ theme, isMultiWorkspace }) => - !isMultiWorkspace ? theme.spacing(2) : null}; height: ${({ theme }) => theme.spacing(8)}; user-select: none; `; +const StyledSingleWorkspaceContainer = styled(StyledContainer)` + gap: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(1)}; +`; const StyledLogo = styled.div<{ logo: string }>` background: url(${({ logo }) => logo}); @@ -57,21 +61,25 @@ export const NavigationDrawerHeader = ({ const isMobile = useIsMobile(); const workspaces = useRecoilValue(workspacesState); const isMultiWorkspace = workspaces !== null && workspaces.length > 1; + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); return ( - + {isMultiWorkspace ? ( ) : ( - <> + - {name} - + + {name} + + )} - - {!isMobile && ( + {!isMobile && isNavigationDrawerExpanded && ( ; +> & { isNavigationDrawerExpanded: boolean }; -const StyledItem = styled('div', { +const StyledItem = styled('button', { shouldForwardProp: (prop) => !['active', 'danger', 'soon'].includes(prop) && isPropValid(prop), })` + box-sizing: content-box; align-items: center; background: ${(props) => props.active ? props.theme.background.transparent.light : 'inherit'}; @@ -65,9 +69,8 @@ const StyledItem = styled('div', { }}; cursor: ${(props) => (props.soon ? 'default' : 'pointer')}; display: flex; - font-family: 'Inter'; + font-family: ${({ theme }) => theme.font.family}; font-size: ${({ theme }) => theme.font.size.md}; - gap: ${({ theme }) => theme.spacing(2)}; padding-bottom: ${({ theme }) => theme.spacing(1)}; padding-left: ${({ theme }) => theme.spacing(1)}; @@ -78,7 +81,12 @@ const StyledItem = styled('div', { indentationLevel === 2 ? '2px' : '0'}; pointer-events: ${(props) => (props.soon ? 'none' : 'auto')}; - width: 100%; + + width: ${(props) => + !props.isNavigationDrawerExpanded + ? `${NAV_DRAWER_WIDTHS.menu.desktop.collapsed - 24}px` + : '100%'}; + :hover { background: ${({ theme }) => theme.background.transparent.light}; color: ${(props) => @@ -96,14 +104,20 @@ const StyledItem = styled('div', { } `; -const StyledItemLabel = styled.div` +const StyledItemElementsContainer = styled.span` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledItemLabel = styled.span` font-weight: ${({ theme }) => theme.font.weight.medium}; - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; -const StyledItemCount = styled.div` +const StyledItemCount = styled.span` align-items: center; background-color: ${({ theme }) => theme.color.blue}; border-radius: ${({ theme }) => theme.border.radius.rounded}; @@ -111,14 +125,13 @@ const StyledItemCount = styled.div` display: flex; font-size: ${({ theme }) => theme.font.size.xs}; font-weight: ${({ theme }) => theme.font.weight.semiBold}; - height: 16px; justify-content: center; margin-left: auto; width: 16px; `; -const StyledKeyBoardShortcut = styled.div` +const StyledKeyBoardShortcut = styled.span` align-items: center; border-radius: 4px; color: ${({ theme }) => theme.font.color.light}; @@ -129,12 +142,15 @@ const StyledKeyBoardShortcut = styled.div` visibility: hidden; `; -const StyledNavigationDrawerItemContainer = styled.div` +const StyledNavigationDrawerItemContainer = styled.span` display: flex; - flex-grow: 1; width: 100%; `; +const StyledSpacer = styled.span` + flex-grow: 1; +`; + export const NavigationDrawerItem = ({ className, label, @@ -151,16 +167,15 @@ export const NavigationDrawerItem = ({ }: NavigationDrawerItemProps) => { const theme = useTheme(); const isMobile = useIsMobile(); + const isSettingsPage = useIsSettingsPage(); const navigate = useNavigate(); - const setIsNavigationDrawerOpen = useSetRecoilState( - isNavigationDrawerOpenState, - ); - + const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = + useRecoilState(isNavigationDrawerExpandedState); const showBreadcrumb = indentationLevel === 2; const handleItemClick = () => { if (isMobile) { - setIsNavigationDrawerOpen(false); + setIsNavigationDrawerExpanded(false); } if (isDefined(onClick)) { @@ -182,28 +197,56 @@ export const NavigationDrawerItem = ({ aria-selected={active} danger={danger} soon={soon} - as={to ? Link : 'div'} + as={to ? Link : undefined} to={to ? to : undefined} indentationLevel={indentationLevel} + isNavigationDrawerExpanded={isNavigationDrawerExpanded} > {showBreadcrumb && ( - - )} - {Icon && ( - - )} - {label} - {soon && } - {!!count && {count}} - {keyboard && ( - - {keyboard} - + + + )} + + {Icon && ( + + )} + + + {label} + + + + + {soon && ( + + + + )} + + {!!count && ( + + {count} + + )} + + {keyboard && ( + + + {keyboard} + + + )} + ); diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb.tsx index 47e8bd120864..6c3998544111 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemBreadcrumb.tsx @@ -6,9 +6,10 @@ export type NavigationDrawerItemBreadcrumbProps = { }; const StyledNavigationDrawerItemBreadcrumbContainer = styled.div` - margin-left: 7.5px; - height: 28px; + + margin-left: 7.5px; + margin-right: ${({ theme }) => theme.spacing(2)}; width: 9px; `; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer.tsx new file mode 100644 index 000000000000..550793621c0b --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer.tsx @@ -0,0 +1,52 @@ +import styled from '@emotion/styled'; +import { ReactNode } from 'react'; +import { useRecoilValue } from 'recoil'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; +import { AnimationControls, motion, TargetAndTransition } from 'framer-motion'; +import { useTheme } from '@emotion/react'; + +const StyledAnimationGroupContainer = styled(motion.div)``; + +type NavigationDrawerItemsCollapsedContainerProps = { + isGroup?: boolean; + children: ReactNode; +}; + +export const NavigationDrawerItemsCollapsedContainer = ({ + isGroup = false, + children, +}: NavigationDrawerItemsCollapsedContainerProps) => { + const theme = useTheme(); + const isSettingsPage = useIsSettingsPage(); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); + const isExpanded = isNavigationDrawerExpanded || isSettingsPage; + let animate: AnimationControls | TargetAndTransition = { + width: 'auto', + backgroundColor: 'transparent', + border: 'none', + }; + if (!isExpanded) { + animate = { width: 24 }; + if (isGroup) { + animate = { + width: 24, + backgroundColor: theme.background.transparent.lighter, + border: `1px solid ${theme.background.transparent.lighter}`, + borderRadius: theme.border.radius.sm, + }; + } + } + + return ( + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx index 2ba98503329b..afc7fe803744 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx @@ -6,6 +6,8 @@ const StyledSection = styled.div` gap: ${({ theme }) => theme.betweenSiblingsGap}; width: 100%; margin-bottom: ${({ theme }) => theme.spacing(3)}; + flex-shrink: 1; + overflow: hidden; `; export { StyledSection as NavigationDrawerSection }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx index d208dccf111a..aeeffa88d343 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx @@ -1,8 +1,10 @@ import styled from '@emotion/styled'; import { currentUserState } from '@/auth/states/currentUserState'; +import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -37,9 +39,22 @@ export const NavigationDrawerSectionTitle = ({ }: NavigationDrawerSectionTitleProps) => { const currentUser = useRecoilValue(currentUserState); const loading = useIsPrefetchLoading(); + const isNavigationDrawerExpanded = useRecoilValue( + isNavigationDrawerExpandedState, + ); + + const isSettingsPage = useIsSettingsPage(); if (loading && isDefined(currentUser)) { return ; } - return {label}; + return ( + + {label} + + ); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx index dfbe324297a4..93db01f85838 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; import { + GithubVersionLink, IconAt, IconBell, IconBuildingSkyscraper, @@ -18,18 +19,17 @@ import { import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { GithubVersionLink } from '@/ui/navigation/link/components/GithubVersionLink'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentWorkspaceMemberFavorites'; import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; +import jsonPage from '../../../../../../../package.json'; import { NavigationDrawer } from '../NavigationDrawer'; import { NavigationDrawerItem } from '../NavigationDrawerItem'; import { NavigationDrawerItemGroup } from '../NavigationDrawerItemGroup'; import { NavigationDrawerSection } from '../NavigationDrawerSection'; import { NavigationDrawerSectionTitle } from '../NavigationDrawerSectionTitle'; - const meta: Meta = { title: 'UI/Navigation/NavigationDrawer/NavigationDrawer', component: NavigationDrawer, @@ -53,6 +53,11 @@ export const Default: Story = { Icon={IconBell} soon={true} /> + @@ -144,6 +148,6 @@ export const Submenu: Story = { ), - footer: , + footer: , }, }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths.ts deleted file mode 100644 index fe5462310dce..000000000000 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const DESKTOP_NAV_DRAWER_WIDTHS = { - menu: 220, -}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/NavDrawerWidths.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/NavDrawerWidths.ts new file mode 100644 index 000000000000..3abe70079807 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/constants/NavDrawerWidths.ts @@ -0,0 +1,12 @@ +export const NAV_DRAWER_WIDTHS = { + menu: { + mobile: { + collapsed: 0, + expanded: '100%', + }, + desktop: { + collapsed: 40, + expanded: 220, + }, + }, +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts index 7a04238d1e53..b7e7abf9f325 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts @@ -1,6 +1,13 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useAuth } from '@/auth/hooks/useAuth'; +import { useSSO } from '@/auth/sign-in-up/hooks/useSSO'; +import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { + SignInUpStep, + signInUpStepState, +} from '@/auth/states/signInUpStepState'; import { tokenPairState } from '@/auth/states/tokenPairState'; import { AppPath } from '@/types/AppPath'; import { useGenerateJwtMutation } from '~/generated/graphql'; @@ -10,7 +17,13 @@ import { sleep } from '~/utils/sleep'; export const useWorkspaceSwitching = () => { const setTokenPair = useSetRecoilState(tokenPairState); const [generateJWT] = useGenerateJwtMutation(); + const { redirectToSSOLoginPage } = useSSO(); const currentWorkspace = useRecoilValue(currentWorkspaceState); + const setAvailableWorkspacesForSSOState = useSetRecoilState( + availableSSOIdentityProvidersState, + ); + const setSignInUpStep = useSetRecoilState(signInUpStepState); + const { signOut } = useAuth(); const switchWorkspace = async (workspaceId: string) => { if (currentWorkspace?.id === workspaceId) return; @@ -28,10 +41,34 @@ export const useWorkspaceSwitching = () => { throw new Error('could not create token'); } - const { tokens } = jwt.data.generateJWT; - setTokenPair(tokens); - await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly. - window.location.href = AppPath.Index; + if ( + jwt.data.generateJWT.reason === 'WORKSPACE_USE_SSO_AUTH' && + 'availableSSOIDPs' in jwt.data.generateJWT + ) { + if (jwt.data.generateJWT.availableSSOIDPs.length === 1) { + redirectToSSOLoginPage(jwt.data.generateJWT.availableSSOIDPs[0].id); + } + + if (jwt.data.generateJWT.availableSSOIDPs.length > 1) { + await signOut(); + setAvailableWorkspacesForSSOState( + jwt.data.generateJWT.availableSSOIDPs, + ); + setSignInUpStep(SignInUpStep.SSOWorkspaceSelection); + } + + return; + } + + if ( + jwt.data.generateJWT.reason !== 'WORKSPACE_USE_SSO_AUTH' && + 'authTokens' in jwt.data.generateJWT + ) { + const { tokens } = jwt.data.generateJWT.authTokens; + setTokenPair(tokens); + await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly. + window.location.href = AppPath.Index; + } }; return { switchWorkspace }; diff --git a/packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerOpenState.ts b/packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerExpanded.ts similarity index 63% rename from packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerOpenState.ts rename to packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerExpanded.ts index b5496b4a57ac..7155d06078de 100644 --- a/packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerOpenState.ts +++ b/packages/twenty-front/src/modules/ui/navigation/states/isNavigationDrawerExpanded.ts @@ -3,7 +3,7 @@ import { MOBILE_VIEWPORT } from 'twenty-ui'; const isMobile = window.innerWidth <= MOBILE_VIEWPORT; -export const isNavigationDrawerOpenState = atom({ - key: 'isNavigationDrawerOpen', +export const isNavigationDrawerExpandedState = atom({ + key: 'isNavigationDrawerExpanded', default: !isMobile, }); diff --git a/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerExpandedMemorizedState.ts b/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerExpandedMemorizedState.ts new file mode 100644 index 000000000000..cdeff16e74ac --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/states/navigationDrawerExpandedMemorizedState.ts @@ -0,0 +1,9 @@ +import { atom } from 'recoil'; +import { MOBILE_VIEWPORT } from 'twenty-ui'; + +const isMobile = window.innerWidth <= MOBILE_VIEWPORT; + +export const navigationDrawerExpandedMemorizedState = atom({ + key: 'navigationDrawerExpandedMemorized', + default: !isMobile, +}); diff --git a/packages/twenty-front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx b/packages/twenty-front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx index a9dca6c5e625..c298b3e8e1bf 100644 --- a/packages/twenty-front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/drag-select/components/DragSelect.tsx @@ -1,9 +1,9 @@ -import { RefObject } from 'react'; import { boxesIntersect, useSelectionContainer, } from '@air/react-drag-to-select'; import { useTheme } from '@emotion/react'; +import { RefObject } from 'react'; import { RGBA } from 'twenty-ui'; import { useDragSelect } from '../hooks/useDragSelect'; @@ -11,13 +11,15 @@ import { useDragSelect } from '../hooks/useDragSelect'; type DragSelectProps = { dragSelectable: RefObject; onDragSelectionChange: (id: string, selected: boolean) => void; - onDragSelectionStart?: () => void; + onDragSelectionStart?: (event: MouseEvent) => void; + onDragSelectionEnd?: (event: MouseEvent) => void; }; export const DragSelect = ({ dragSelectable, onDragSelectionChange, onDragSelectionStart, + onDragSelectionEnd, }: DragSelectProps) => { const theme = useTheme(); const { isDragSelectionStartEnabled } = useDragSelect(); @@ -37,6 +39,7 @@ export const DragSelect = ({ return true; }, onSelectionStart: onDragSelectionStart, + onSelectionEnd: onDragSelectionEnd, onSelectionChange: (box) => { const scrollAwareBox = { ...box, diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/__tests__/useGoToHotkeys.test.tsx b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/__tests__/useGoToHotkeys.test.tsx index 1ed5ddb4ca21..f7e478d2c7b8 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/__tests__/useGoToHotkeys.test.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/__tests__/useGoToHotkeys.test.tsx @@ -1,6 +1,6 @@ +import { fireEvent, renderHook } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { MemoryRouter, useLocation } from 'react-router-dom'; -import { fireEvent, renderHook } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; @@ -23,7 +23,7 @@ const renderHookConfig = { describe('useGoToHotkeys', () => { it('should navigate on hotkey trigger', () => { const { result } = renderHook(() => { - useGoToHotkeys('a', '/three'); + useGoToHotkeys({ key: 'a', location: '/three' }); const setHotkeyScope = useSetHotkeyScope(); diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts index aeb485b4c02f..d8e62312cfeb 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts @@ -5,13 +5,24 @@ import { AppHotkeyScope } from '../types/AppHotkeyScope'; import { useSequenceHotkeys } from './useSequenceScopedHotkeys'; -export const useGoToHotkeys = (key: Keys, location: string) => { +type GoToHotkeysProps = { + key: Keys; + location: string; + preNavigateFunction?: () => void; +}; + +export const useGoToHotkeys = ({ + key, + location, + preNavigateFunction, +}: GoToHotkeysProps) => { const navigate = useNavigate(); useSequenceHotkeys( 'g', key, () => { + preNavigateFunction?.(); navigate(location); }, AppHotkeyScope.Goto, diff --git a/packages/twenty-front/src/modules/ui/utilities/page-title/PageTitle.tsx b/packages/twenty-front/src/modules/ui/utilities/page-title/components/PageTitle.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/page-title/PageTitle.tsx rename to packages/twenty-front/src/modules/ui/utilities/page-title/components/PageTitle.tsx diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx index f236b78840f9..2f9530aeb661 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx @@ -1,13 +1,12 @@ +import { fireEvent, renderHook } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import { fireEvent, render, renderHook } from '@testing-library/react'; import { isDefined } from '~/utils/isDefined'; import { ClickOutsideMode, useListenClickOutside, - useListenClickOutsideByClassName, } from '../useListenClickOutside'; const containerRef = React.createRef(); @@ -77,59 +76,3 @@ describe('useListenClickOutside', () => { expect(callback).toHaveBeenCalled(); }); }); - -describe('useListenClickOutsideByClassName', () => { - it('should trigger the callback when clicking outside the specified class names', () => { - const callback = jest.fn(); - const { container } = render( -
-
Inside
-
Outside
-
, - ); - - renderHook(() => - useListenClickOutsideByClassName({ - classNames: ['wont-trigger'], - callback, - }), - ); - - act(() => { - const notClickableElement = container.querySelector('.will-trigger'); - if (isDefined(notClickableElement)) { - fireEvent.mouseDown(notClickableElement); - fireEvent.click(notClickableElement); - } - }); - - expect(callback).toHaveBeenCalled(); - }); - - it('should not trigger the callback when clicking inside the specified class names', () => { - const callback = jest.fn(); - const { container } = render( -
-
Inside
-
Outside
-
, - ); - - renderHook(() => - useListenClickOutsideByClassName({ - classNames: ['wont-trigger'], - callback, - }), - ); - - act(() => { - const notClickableElement = container.querySelector('.wont-trigger'); - if (isDefined(notClickableElement)) { - fireEvent.mouseDown(notClickableElement); - fireEvent.click(notClickableElement); - } - }); - - expect(callback).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOustideListenerStates.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOustideListenerStates.ts index dc2675440399..26e7ebca3ae9 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOustideListenerStates.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOustideListenerStates.ts @@ -1,7 +1,7 @@ import { clickOutsideListenerCallbacksComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerCallbacksComponentState'; import { clickOutsideListenerIsActivatedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsActivatedComponentState'; import { clickOutsideListenerIsMouseDownInsideComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerIsMouseDownInsideComponentState'; -import { lockedListenerIdState } from '@/ui/utilities/pointer-event/states/lockedListenerIdState'; +import { clickOutsideListenerMouseDownHappenedComponentState } from '@/ui/utilities/pointer-event/states/clickOutsideListenerMouseDownHappenedComponentState'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; @@ -22,6 +22,9 @@ export const useClickOustideListenerStates = (componentId: string) => { clickOutsideListenerIsActivatedComponentState, scopeId, ), - lockedListenerIdState, + getClickOutsideListenerMouseDownHappenedState: extractComponentState( + clickOutsideListenerMouseDownHappenedComponentState, + scopeId, + ), }; }; diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts index b69e06f8b5ab..5a693e1f7842 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts @@ -7,17 +7,14 @@ import { useListenClickOutsideV2, } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; import { ClickOutsideListenerCallback } from '@/ui/utilities/pointer-event/types/ClickOutsideListenerCallback'; -import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { toSpliced } from '~/utils/array/toSpliced'; import { isDefined } from '~/utils/isDefined'; export const useClickOutsideListener = (componentId: string) => { - // TODO: improve typing - const scopeId = getScopeIdFromComponentId(componentId) ?? ''; - const { getClickOutsideListenerIsActivatedState, getClickOutsideListenerCallbacksState, + getClickOutsideListenerMouseDownHappenedState, } = useClickOustideListenerStates(componentId); const useListenClickOutside = ({ @@ -53,8 +50,15 @@ export const useClickOutsideListener = (componentId: string) => { ({ set }) => (activated: boolean) => { set(getClickOutsideListenerIsActivatedState, activated); + + if (!activated) { + set(getClickOutsideListenerMouseDownHappenedState, false); + } }, - [getClickOutsideListenerIsActivatedState], + [ + getClickOutsideListenerIsActivatedState, + getClickOutsideListenerMouseDownHappenedState, + ], ); const registerOnClickOutsideCallback = useRecoilCallback( @@ -148,7 +152,6 @@ export const useClickOutsideListener = (componentId: string) => { }; return { - scopeId, useListenClickOutside, toggleClickOutsideListener, useRegisterClickOutsideListenerCallback, diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts index b3a879216f25..613590ed5e12 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutside.ts @@ -138,58 +138,3 @@ export const useListenClickOutside = ({ } }, [refs, callback, mode, enabled, isMouseDownInside]); }; - -export const useListenClickOutsideByClassName = ({ - classNames, - excludeClassNames, - callback, -}: { - classNames: string[]; - excludeClassNames?: string[]; - callback: () => void; -}) => { - useEffect(() => { - const handleClickOutside = (event: MouseEvent | TouchEvent) => { - if (!(event.target instanceof Node)) return; - - const clickedElement = event.target as HTMLElement; - let isClickedInside = false; - let isClickedOnExcluded = false; - let currentElement: HTMLElement | null = clickedElement; - - while (currentElement) { - const currentClassList = currentElement.classList; - - isClickedInside = classNames.some((className) => - currentClassList.contains(className), - ); - isClickedOnExcluded = - excludeClassNames?.some((className) => - currentClassList.contains(className), - ) ?? false; - - if (isClickedInside || isClickedOnExcluded) { - break; - } - - currentElement = currentElement.parentElement; - } - - if (!isClickedInside && !isClickedOnExcluded) { - callback(); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('touchend', handleClickOutside, { - capture: true, - }); - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - document.removeEventListener('touchend', handleClickOutside, { - capture: true, - }); - }; - }, [callback, classNames, excludeClassNames]); -}; diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts index 287d6b0f8e6d..0d2f84c058eb 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts @@ -10,6 +10,7 @@ export enum ClickOutsideMode { export type ClickOutsideListenerProps = { refs: Array>; + excludeClassNames?: string[]; callback: (event: MouseEvent | TouchEvent) => void; mode?: ClickOutsideMode; listenerId: string; @@ -18,6 +19,7 @@ export type ClickOutsideListenerProps = { export const useListenClickOutsideV2 = ({ refs, + excludeClassNames, callback, mode = ClickOutsideMode.compareHTMLRef, listenerId, @@ -26,6 +28,7 @@ export const useListenClickOutsideV2 = ({ const { getClickOutsideListenerIsMouseDownInsideState, getClickOutsideListenerIsActivatedState, + getClickOutsideListenerMouseDownHappenedState, } = useClickOustideListenerStates(listenerId); const handleMouseDown = useRecoilCallback( @@ -35,6 +38,8 @@ export const useListenClickOutsideV2 = ({ .getLoadable(getClickOutsideListenerIsActivatedState) .getValue(); + set(getClickOutsideListenerMouseDownHappenedState, true); + const isListening = clickOutsideListenerIsActivated && enabled; if (!isListening) { @@ -90,27 +95,63 @@ export const useListenClickOutsideV2 = ({ } }, [ + getClickOutsideListenerIsActivatedState, + enabled, mode, refs, getClickOutsideListenerIsMouseDownInsideState, - enabled, - getClickOutsideListenerIsActivatedState, + getClickOutsideListenerMouseDownHappenedState, ], ); const handleClickOutside = useRecoilCallback( ({ snapshot }) => (event: MouseEvent | TouchEvent) => { + const clickOutsideListenerIsActivated = snapshot + .getLoadable(getClickOutsideListenerIsActivatedState) + .getValue(); + + const isListening = clickOutsideListenerIsActivated && enabled; + const isMouseDownInside = snapshot .getLoadable(getClickOutsideListenerIsMouseDownInsideState) .getValue(); + const hasMouseDownHappened = snapshot + .getLoadable(getClickOutsideListenerMouseDownHappenedState) + .getValue(); + if (mode === ClickOutsideMode.compareHTMLRef) { + const clickedElement = event.target as HTMLElement; + let isClickedOnExcluded = false; + let currentElement: HTMLElement | null = clickedElement; + + while (currentElement) { + const currentClassList = currentElement.classList; + + isClickedOnExcluded = + excludeClassNames?.some((className) => + currentClassList.contains(className), + ) ?? false; + + if (isClickedOnExcluded) { + break; + } + + currentElement = currentElement.parentElement; + } + const clickedOnAtLeastOneRef = refs .filter((ref) => !!ref.current) .some((ref) => ref.current?.contains(event.target as Node)); - if (!clickedOnAtLeastOneRef && !isMouseDownInside) { + if ( + isListening && + hasMouseDownHappened && + !clickedOnAtLeastOneRef && + !isMouseDownInside && + !isClickedOnExcluded + ) { callback(event); } } @@ -146,12 +187,26 @@ export const useListenClickOutsideV2 = ({ return true; }); - if (!clickedOnAtLeastOneRef && !isMouseDownInside) { + if ( + !clickedOnAtLeastOneRef && + !isMouseDownInside && + isListening && + hasMouseDownHappened + ) { callback(event); } } }, - [mode, refs, callback, getClickOutsideListenerIsMouseDownInsideState], + [ + getClickOutsideListenerIsActivatedState, + enabled, + getClickOutsideListenerIsMouseDownInsideState, + getClickOutsideListenerMouseDownHappenedState, + mode, + refs, + excludeClassNames, + callback, + ], ); useEffect(() => { diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/states/clickOutsideListenerMouseDownHappenedComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/states/clickOutsideListenerMouseDownHappenedComponentState.ts new file mode 100644 index 000000000000..c9c9298c9cde --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/states/clickOutsideListenerMouseDownHappenedComponentState.ts @@ -0,0 +1,7 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const clickOutsideListenerMouseDownHappenedComponentState = + createComponentState({ + key: 'clickOutsideListenerMouseDownHappenedComponentState', + defaultValue: false, + }); diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/states/lockedListenerIdState.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/states/lockedListenerIdState.ts deleted file mode 100644 index c91421dae3e8..000000000000 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/states/lockedListenerIdState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const lockedListenerIdState = createState({ - key: 'lockedListenerIdState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/hooks/__tests__/useAvailableScopeId.test.tsx b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/hooks/__tests__/useAvailableScopeId.test.tsx index afb281b9a759..05a638e71727 100644 --- a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/hooks/__tests__/useAvailableScopeId.test.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/hooks/__tests__/useAvailableScopeId.test.tsx @@ -1,5 +1,5 @@ -import React from 'react'; import { renderHook } from '@testing-library/react'; +import React from 'react'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext'; diff --git a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext.ts b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext.ts index abf1b33900b4..02d447a7fd32 100644 --- a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext.ts +++ b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext.ts @@ -1,6 +1,5 @@ -import { Context, createContext } from 'react'; - import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey'; +import { Context, createContext } from 'react'; type ScopeInternalContext = Context; diff --git a/packages/twenty-front/src/modules/ui/utilities/responsive/hooks/__tests__/isMobile.test.tsx b/packages/twenty-front/src/modules/ui/utilities/responsive/hooks/__tests__/useIsMobile.test.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/responsive/hooks/__tests__/isMobile.test.tsx rename to packages/twenty-front/src/modules/ui/utilities/responsive/hooks/__tests__/useIsMobile.test.tsx diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx index d4e5c2e79f78..97d4560694fd 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx @@ -26,16 +26,16 @@ const StyledScrollWrapper = styled.div` export type ScrollWrapperProps = { children: React.ReactNode; className?: string; - hideY?: boolean; - hideX?: boolean; + enableXScroll?: boolean; + enableYScroll?: boolean; contextProviderName: ContextProviderName; }; export const ScrollWrapper = ({ children, className, - hideX, - hideY, + enableXScroll = true, + enableYScroll = true, contextProviderName, }: ScrollWrapperProps) => { const scrollableRef = useRef(null); @@ -58,8 +58,8 @@ export const ScrollWrapper = ({ options: { scrollbars: { autoHide: 'scroll' }, overflow: { - y: hideY ? 'hidden' : undefined, - x: hideX ? 'hidden' : undefined, + x: enableXScroll ? undefined : 'hidden', + y: enableYScroll ? undefined : 'hidden', }, }, events: { diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx index b6f7d2103a60..1d83d1ddd94b 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx @@ -17,7 +17,8 @@ export type ContextProviderName = | 'tabList' | 'releases' | 'test' - | 'showPageActivityContainer'; + | 'showPageActivityContainer' + | 'navigationDrawer'; const createScrollWrapperContext = (id: string) => createContext({ @@ -47,6 +48,8 @@ export const ReleasesScrollWrapperContext = createScrollWrapperContext('releases'); export const ShowPageActivityContainerScrollWrapperContext = createScrollWrapperContext('showPageActivityContainer'); +export const NavigationDrawerScrollWrapperContext = + createScrollWrapperContext('navigationDrawer'); export const TestScrollWrapperContext = createScrollWrapperContext('test'); export const getContextByProviderName = ( @@ -77,6 +80,8 @@ export const getContextByProviderName = ( return TestScrollWrapperContext; case 'showPageActivityContainer': return ShowPageActivityContainerScrollWrapperContext; + case 'navigationDrawer': + return NavigationDrawerScrollWrapperContext; default: throw new Error('Context Provider not available'); } diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentSelectorV2.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentSelectorV2.ts index 352bff92ff47..8f08559d7e25 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentSelectorV2.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentSelectorV2.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-redeclare */ +/* eslint-disable prefer-arrow/prefer-arrow-functions */ import { selectorFamily } from 'recoil'; import { ComponentInstanceStateContext } from '@/ui/utilities/state/component-state/types/ComponentInstanceStateContext'; @@ -9,19 +11,32 @@ import { SelectorGetter } from '@/ui/utilities/state/types/SelectorGetter'; import { SelectorSetter } from '@/ui/utilities/state/types/SelectorSetter'; import { isDefined } from 'twenty-ui'; -export const createComponentSelectorV2 = ({ +export function createComponentSelectorV2(options: { + key: string; + get: SelectorGetter; + componentInstanceContext: ComponentInstanceStateContext | null; +}): ComponentReadOnlySelectorV2; + +export function createComponentSelectorV2(options: { + key: string; + get: SelectorGetter; + set: SelectorSetter; + componentInstanceContext: ComponentInstanceStateContext | null; +}): ComponentSelectorV2; + +export function createComponentSelectorV2({ key, get, set, - instanceContext, + componentInstanceContext, }: { key: string; get: SelectorGetter; set?: SelectorSetter; - instanceContext: ComponentInstanceStateContext | null; -}): ComponentSelectorV2 | ComponentReadOnlySelectorV2 => { - if (isDefined(instanceContext)) { - globalComponentInstanceContextMap.set(key, instanceContext); + componentInstanceContext: ComponentInstanceStateContext | null; +}): ComponentSelectorV2 | ComponentReadOnlySelectorV2 { + if (isDefined(componentInstanceContext)) { + globalComponentInstanceContextMap.set(key, componentInstanceContext); } if (isDefined(set)) { @@ -44,4 +59,4 @@ export const createComponentSelectorV2 = ({ }), } satisfies ComponentReadOnlySelectorV2; } -}; +} diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2_alpha.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2Alpha.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2_alpha.ts rename to packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2Alpha.ts diff --git a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx index 4c51e6da446d..c41b7ff7e6cd 100644 --- a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx @@ -7,6 +7,8 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState'; import { workspacesState } from '@/auth/states/workspaces'; +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; import { detectDateFormat } from '@/localization/utils/detectDateFormat'; import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; @@ -81,10 +83,10 @@ export const UserProviderEffect = () => { : detectTimeZone(), dateFormat: isDefined(workspaceMember.dateFormat) ? getDateFormatFromWorkspaceDateFormat(workspaceMember.dateFormat) - : detectDateFormat(), + : DateFormat[detectDateFormat()], timeFormat: isDefined(workspaceMember.timeFormat) ? getTimeFormatFromWorkspaceTimeFormat(workspaceMember.timeFormat) - : detectTimeFormat(), + : TimeFormat[detectTimeFormat()], }); } diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index 76e5400c34e9..cfb964926c9f 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -8,6 +8,7 @@ export const USER_QUERY_FRAGMENT = gql` email canImpersonate supportUserHash + analyticsTinybirdJwt onboardingStatus workspaceMember { ...WorkspaceMemberQueryFragment @@ -23,6 +24,8 @@ export const USER_QUERY_FRAGMENT = gql` inviteHash allowImpersonation activationStatus + isPublicInviteLinkEnabled + hasValidEntrepriseKey featureFlags { id key diff --git a/packages/twenty-front/src/modules/views/components/AdvancedFilterChip.tsx b/packages/twenty-front/src/modules/views/components/AdvancedFilterChip.tsx new file mode 100644 index 000000000000..733084aca421 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/AdvancedFilterChip.tsx @@ -0,0 +1,27 @@ +import { IconFilterCog } from 'twenty-ui'; + +import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; +import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; +import { plural } from 'pluralize'; + +type AdvancedFilterChipProps = { + onRemove: () => void; + advancedFilterCount?: number; +}; + +export const AdvancedFilterChip = ({ + onRemove, + advancedFilterCount, +}: AdvancedFilterChipProps) => { + const labelText = 'advanced rule'; + const chipLabel = `${advancedFilterCount ?? 0} ${advancedFilterCount === 1 ? labelText : plural(labelText)}`; + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx new file mode 100644 index 000000000000..351f3c3d1c0e --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/AdvancedFilterDropdownButton.tsx @@ -0,0 +1,83 @@ +import { useCallback } from 'react'; + +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; + +import { AdvancedFilterRootLevelViewFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterRootLevelViewFilterGroup'; +import { useDeleteCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useDeleteCombinedViewFilterGroup'; +import { AdvancedFilterChip } from '@/views/components/AdvancedFilterChip'; +import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; +import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; +import { isDefined } from 'twenty-ui'; + +export const AdvancedFilterDropdownButton = () => { + const { deleteCombinedViewFilter } = useDeleteCombinedViewFilters(); + const { deleteCombinedViewFilterGroup } = useDeleteCombinedViewFilterGroup(); + + const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); + + const advancedViewFilterIds = + currentViewWithCombinedFiltersAndSorts?.viewFilters + .filter((viewFilter) => isDefined(viewFilter.viewFilterGroupId)) + .map((viewFilter) => viewFilter.id); + + const handleDropdownClickOutside = useCallback(() => {}, []); + + const handleDropdownClose = () => {}; + + const removeAdvancedFilter = useCallback(async () => { + if (!advancedViewFilterIds) { + throw new Error('No advanced view filters to remove'); + } + + const viewFilterGroupIds = + currentViewWithCombinedFiltersAndSorts?.viewFilterGroups?.map( + (viewFilter) => viewFilter.id, + ) ?? []; + + for (const viewFilterGroupId of viewFilterGroupIds) { + await deleteCombinedViewFilterGroup(viewFilterGroupId); + } + + for (const viewFilterId of advancedViewFilterIds) { + await deleteCombinedViewFilter(viewFilterId); + } + }, [ + advancedViewFilterIds, + deleteCombinedViewFilter, + deleteCombinedViewFilterGroup, + currentViewWithCombinedFiltersAndSorts?.viewFilterGroups, + ]); + + const outermostViewFilterGroupId = + currentViewWithCombinedFiltersAndSorts?.viewFilterGroups.find( + (viewFilterGroup) => !viewFilterGroup.parentViewFilterGroupId, + )?.id; + + if (!outermostViewFilterGroupId) { + return null; + } + + return ( + + } + dropdownComponents={ + + } + dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }} + dropdownOffset={{ y: 8, x: 0 }} + dropdownPlacement="bottom-start" + dropdownMenuWidth={800} + onClickOutside={handleDropdownClickOutside} + onClose={handleDropdownClose} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index b8b9fe56ffa4..67d068a16138 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect } from 'react'; -import { MultipleFiltersDropdownContent } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { FilterOperand } from '@/object-record/object-filter-dropdown/types/FilterOperand'; @@ -11,6 +10,7 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/ import { EditableFilterChip } from '@/views/components/EditableFilterChip'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { isDefined } from '~/utils/isDefined'; @@ -98,7 +98,7 @@ export const EditableFilterDropdownButton = ({ } dropdownComponents={ - } diff --git a/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx index f306f29bd7d5..ced095e33adb 100644 --- a/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx @@ -1,14 +1,14 @@ -import { contextStoreCurrentViewIdState } from '@/context-store/states/contextStoreCurrentViewIdState'; +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem'; import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; import { isUndefined } from '@sniptt/guards'; import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDefined } from '~/utils/isDefined'; @@ -39,8 +39,8 @@ export const QueryParamsViewIdEffect = () => { objectMetadataItemId?.id, lastVisitedObjectMetadataItemId, ); - const setContextStoreCurrentViewId = useSetRecoilState( - contextStoreCurrentViewIdState, + const setContextStoreCurrentViewId = useSetRecoilComponentStateV2( + contextStoreCurrentViewIdComponentState, ); // // TODO: scope view bar per view id if possible diff --git a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx index 1b55e6a32790..55c2a77f3c59 100644 --- a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx @@ -40,6 +40,7 @@ const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>` font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.medium}; padding: ${({ theme }) => theme.spacing(0.5) + ' ' + theme.spacing(2)}; + margin-left: ${({ theme }) => theme.spacing(2)}; user-select: none; white-space: nowrap; diff --git a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx index ffe89f6b6b60..e9001faf7508 100644 --- a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx +++ b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx @@ -1,12 +1,15 @@ import styled from '@emotion/styled'; -import { IconChevronDown, IconPlus } from 'twenty-ui'; +import { + Button, + ButtonGroup, + IconChevronDown, + IconPlus, + MenuItem, +} from 'twenty-ui'; -import { Button } from '@/ui/input/button/components/Button'; -import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx index 724b80e461c3..59642d58a301 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx @@ -5,7 +5,7 @@ import { ObjectFilterDropdownButton } from '@/object-record/object-filter-dropdo import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { TopBar } from '@/ui/layout/top-bar/TopBar'; +import { TopBar } from '@/ui/layout/top-bar/components/TopBar'; import { QueryParamsFiltersEffect } from '@/views/components/QueryParamsFiltersEffect'; import { QueryParamsViewIdEffect } from '@/views/components/QueryParamsViewIdEffect'; import { ViewBarEffect } from '@/views/components/ViewBarEffect'; @@ -60,7 +60,6 @@ export const ViewBar = ({ leftComponent={ loading ? : } - displayBottomBorder={false} rightComponent={ <> viewFilter.variant && viewFilter.variant !== 'default', + (viewFilter) => + viewFilter.variant && + viewFilter.variant !== 'default' && + !viewFilter.viewFilterGroupId, ); const defaultViewFilters = currentViewWithCombinedFiltersAndSorts.viewFilters.filter( - (viewFilter) => !viewFilter.variant || viewFilter.variant === 'default', + (viewFilter) => + (!viewFilter.variant || viewFilter.variant === 'default') && + !viewFilter.viewFilterGroupId, ); return { @@ -166,6 +172,10 @@ export const ViewBarDetails = ({ return null; } + const showAdvancedFilterDropdownButton = + currentViewWithCombinedFiltersAndSorts?.viewFilterGroups && + currentViewWithCombinedFiltersAndSorts?.viewFilterGroups.length > 0; + return ( @@ -199,6 +209,7 @@ export const ViewBarDetails = ({ )} + {showAdvancedFilterDropdownButton && } {mapViewFiltersToFilters( defaultViewFilters, availableFilterDefinitions, diff --git a/packages/twenty-front/src/modules/views/components/ViewBarPageTitle.tsx b/packages/twenty-front/src/modules/views/components/ViewBarPageTitle.tsx index c7ab13c549b5..758a83b733be 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarPageTitle.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarPageTitle.tsx @@ -1,6 +1,6 @@ import { useParams } from 'react-router-dom'; -import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { capitalize } from '~/utils/string/capitalize'; diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx index d8ba50b090b7..15f3caa60b54 100644 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx @@ -10,6 +10,7 @@ import { IconEye, IconEyeOff, IconInfoCircle, + MenuItemDraggable, useIcons, } from 'twenty-ui'; @@ -19,7 +20,6 @@ import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableIt import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader'; -import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/views/components/ViewGroupsVisibilityDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewGroupsVisibilityDropdownSection.tsx new file mode 100644 index 000000000000..795639f51c43 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/ViewGroupsVisibilityDropdownSection.tsx @@ -0,0 +1,191 @@ +import { + DropResult, + OnDragEndResponder, + ResponderProvided, +} from '@hello-pangea/dnd'; +import { useRef } from 'react'; +import { IconEye, IconEyeOff, MenuItemDraggable, Tag } from 'twenty-ui'; + +import { + RecordGroupDefinition, + RecordGroupDefinitionType, +} from '@/object-record/record-group/types/RecordGroupDefinition'; +import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; +import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader'; +import { isDefined } from '~/utils/isDefined'; + +type ViewGroupsVisibilityDropdownSectionProps = { + viewGroups: RecordGroupDefinition[]; + isDraggable: boolean; + onDragEnd?: OnDragEndResponder; + onVisibilityChange: (viewGroup: RecordGroupDefinition) => void; + title: string; + showSubheader: boolean; + showDragGrip: boolean; +}; + +export const ViewGroupsVisibilityDropdownSection = ({ + viewGroups, + isDraggable, + onDragEnd, + onVisibilityChange, + title, + showSubheader = true, + showDragGrip, +}: ViewGroupsVisibilityDropdownSectionProps) => { + const handleOnDrag = (result: DropResult, provided: ResponderProvided) => { + onDragEnd?.(result, provided); + }; + + const getIconButtons = (index: number, viewGroup: RecordGroupDefinition) => { + const iconButtons = [ + { + Icon: viewGroup.isVisible ? IconEyeOff : IconEye, + onClick: () => onVisibilityChange(viewGroup), + }, + ].filter(isDefined); + + return iconButtons.length ? iconButtons : undefined; + }; + + const noValueViewGroups = + viewGroups.filter( + (viewGroup) => viewGroup.type === RecordGroupDefinitionType.NoValue, + ) ?? []; + + const viewGroupsWithoutNoValueGroups = viewGroups.filter( + (viewGroup) => viewGroup.type !== RecordGroupDefinitionType.NoValue, + ); + + const ref = useRef(null); + + return ( +
+ {showSubheader && ( + {title} + )} + + {!!viewGroups.length && ( + <> + {!isDraggable ? ( + viewGroupsWithoutNoValueGroups.map( + (viewGroup, viewGroupIndex) => ( + + } + iconButtons={getIconButtons(viewGroupIndex, viewGroup)} + accent={showDragGrip ? 'placeholder' : 'default'} + showGrip={showDragGrip} + isDragDisabled={!isDraggable} + /> + ), + ) + ) : ( + + {viewGroupsWithoutNoValueGroups.map( + (viewGroup, viewGroupIndex) => ( + + } + iconButtons={getIconButtons( + viewGroupIndex, + viewGroup, + )} + accent={showDragGrip ? 'placeholder' : 'default'} + showGrip={showDragGrip} + isDragDisabled={!isDraggable} + /> + } + /> + ), + )} + + } + /> + )} + {noValueViewGroups.map((viewGroup) => ( + + } + accent={showDragGrip ? 'placeholder' : 'default'} + showGrip={true} + isDragDisabled={true} + isHoverDisabled + /> + ))} + + )} + +
+ ); +}; diff --git a/packages/twenty-front/src/modules/views/constants/AdvancedFilterDropdownId.ts b/packages/twenty-front/src/modules/views/constants/AdvancedFilterDropdownId.ts new file mode 100644 index 000000000000..133be7b8d303 --- /dev/null +++ b/packages/twenty-front/src/modules/views/constants/AdvancedFilterDropdownId.ts @@ -0,0 +1 @@ +export const ADVANCED_FILTER_DROPDOWN_ID = 'advanced-filter'; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts index 15327a303579..fce290183f7c 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts @@ -1,5 +1,5 @@ -import { useCallback } from 'react'; import { useApolloClient } from '@apollo/client'; +import { useCallback } from 'react'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroupRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroupRecords.ts new file mode 100644 index 000000000000..dad022492c0c --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroupRecords.ts @@ -0,0 +1,214 @@ +import { useApolloClient } from '@apollo/client'; +import { useCallback } from 'react'; + +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect'; +import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; +import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation'; +import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { GraphQLView } from '@/views/types/GraphQLView'; +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import { isDefined } from 'twenty-ui'; + +export const usePersistViewFilterGroupRecords = () => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.ViewFilterGroup, + }); + + const getRecordFromCache = useGetRecordFromCache({ + objectNameSingular: CoreObjectNameSingular.ViewFilterGroup, + }); + + const { destroyOneRecordMutation } = useDestroyOneRecordMutation({ + objectNameSingular: CoreObjectNameSingular.ViewFilterGroup, + }); + + const { createOneRecordMutation } = useCreateOneRecordMutation({ + objectNameSingular: CoreObjectNameSingular.ViewFilterGroup, + }); + + const { updateOneRecordMutation } = useUpdateOneRecordMutation({ + objectNameSingular: CoreObjectNameSingular.ViewFilterGroup, + }); + + const { objectMetadataItems } = useObjectMetadataItems(); + + const apolloClient = useApolloClient(); + + const createViewFilterGroupRecord = useCallback( + async (viewFilterGroup: ViewFilterGroup, view: GraphQLView) => { + const result = await apolloClient.mutate<{ + createViewFilterGroup: ViewFilterGroup; + }>({ + mutation: createOneRecordMutation, + variables: { + input: { + id: viewFilterGroup.id, + viewId: view.id, + parentViewFilterGroupId: viewFilterGroup.parentViewFilterGroupId, + logicalOperator: viewFilterGroup.logicalOperator, + positionInViewFilterGroup: + viewFilterGroup.positionInViewFilterGroup, + }, + }, + update: (cache, { data }) => { + const record = data?.createViewFilterGroup; + if (!record) return; + + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: [record], + objectMetadataItems, + }); + }, + }); + + if (!result.data) { + throw new Error('Failed to create view filter group'); + } + + return { newRecordId: result.data.createViewFilterGroup.id }; + }, + [ + apolloClient, + createOneRecordMutation, + objectMetadataItem, + objectMetadataItems, + ], + ); + + const createViewFilterGroupRecords = useCallback( + async (viewFilterGroupsToCreate: ViewFilterGroup[], view: GraphQLView) => { + if (!viewFilterGroupsToCreate.length) return []; + + const oldToNewId = new Map(); + + for (const viewFilterGroupToCreate of viewFilterGroupsToCreate) { + const newParentViewFilterGroupId = isDefined( + viewFilterGroupToCreate.parentViewFilterGroupId, + ) + ? (oldToNewId.get(viewFilterGroupToCreate.parentViewFilterGroupId) ?? + viewFilterGroupToCreate.parentViewFilterGroupId) + : undefined; + + const { newRecordId } = await createViewFilterGroupRecord( + { + ...viewFilterGroupToCreate, + parentViewFilterGroupId: newParentViewFilterGroupId, + }, + view, + ); + + oldToNewId.set(viewFilterGroupToCreate.id, newRecordId); + } + + const newRecordIds = viewFilterGroupsToCreate.map((viewFilterGroup) => { + const newId = oldToNewId.get(viewFilterGroup.id); + if (!newId) { + throw new Error('Failed to create view filter group'); + } + return newId; + }); + + return newRecordIds; + }, + [createViewFilterGroupRecord], + ); + + const updateViewFilterGroupRecords = useCallback( + (viewFilterGroupsToUpdate: ViewFilterGroup[]) => { + if (!viewFilterGroupsToUpdate.length) return; + return Promise.all( + viewFilterGroupsToUpdate.map((viewFilterGroup) => + apolloClient.mutate<{ updateViewFilterGroup: ViewFilterGroup }>({ + mutation: updateOneRecordMutation, + variables: { + idToUpdate: viewFilterGroup.id, + input: { + parentViewFilterGroupId: + viewFilterGroup.parentViewFilterGroupId, + logicalOperator: viewFilterGroup.logicalOperator, + positionInViewFilterGroup: + viewFilterGroup.positionInViewFilterGroup, + }, + }, + update: (cache, { data }) => { + const record = data?.updateViewFilterGroup; + if (!record) return; + const cachedRecord = getRecordFromCache(record.id); + + if (!cachedRecord) return; + + triggerUpdateRecordOptimisticEffect({ + cache, + objectMetadataItem, + currentRecord: cachedRecord, + updatedRecord: record, + objectMetadataItems, + }); + }, + }), + ), + ); + }, + [ + apolloClient, + getRecordFromCache, + objectMetadataItem, + objectMetadataItems, + updateOneRecordMutation, + ], + ); + + const deleteViewFilterGroupRecords = useCallback( + (viewFilterGroupIdsToDelete: string[]) => { + if (!viewFilterGroupIdsToDelete.length) return; + return Promise.all( + viewFilterGroupIdsToDelete.map((viewFilterGroupId) => + apolloClient.mutate<{ destroyViewFilterGroup: ViewFilterGroup }>({ + mutation: destroyOneRecordMutation, + variables: { + idToDestroy: viewFilterGroupId, + }, + update: (cache, { data }) => { + const record = data?.destroyViewFilterGroup; + + if (!record) return; + + const cachedRecord = getRecordFromCache(record.id, cache); + + if (!cachedRecord) return; + + triggerDestroyRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDestroy: [cachedRecord], + objectMetadataItems, + }); + }, + }), + ), + ); + }, + [ + apolloClient, + destroyOneRecordMutation, + getRecordFromCache, + objectMetadataItem, + objectMetadataItems, + ], + ); + + return { + createViewFilterGroupRecords, + updateViewFilterGroupRecords, + deleteViewFilterGroupRecords, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts index 488b33c31bd9..b9067aefe2c6 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts @@ -50,11 +50,13 @@ export const usePersistViewFilterRecords = () => { mutation: createOneRecordMutation, variables: { input: { + id: viewFilter.id, fieldMetadataId: viewFilter.fieldMetadataId, viewId: view.id, value: viewFilter.value, displayValue: viewFilter.displayValue, operand: viewFilter.operand, + viewFilterGroupId: viewFilter.viewFilterGroupId, }, }, update: (cache, { data }) => { diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts new file mode 100644 index 000000000000..5582c771f3f9 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts @@ -0,0 +1,118 @@ +import { useApolloClient } from '@apollo/client'; +import { useCallback } from 'react'; +import { v4 } from 'uuid'; + +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; +import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; +import { GraphQLView } from '@/views/types/GraphQLView'; +import { ViewGroup } from '@/views/types/ViewGroup'; + +export const usePersistViewGroupRecords = () => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.ViewGroup, + }); + + const { createOneRecordMutation } = useCreateOneRecordMutation({ + objectNameSingular: CoreObjectNameSingular.ViewGroup, + }); + + const { updateOneRecordMutation } = useUpdateOneRecordMutation({ + objectNameSingular: CoreObjectNameSingular.ViewGroup, + }); + + const { objectMetadataItems } = useObjectMetadataItems(); + + const apolloClient = useApolloClient(); + + const createViewGroupRecords = useCallback( + (viewGroupsToCreate: ViewGroup[], view: GraphQLView) => { + if (!viewGroupsToCreate.length) return; + + return Promise.all( + viewGroupsToCreate.map((viewGroup) => + apolloClient.mutate({ + mutation: createOneRecordMutation, + variables: { + input: { + fieldMetadataId: viewGroup.fieldMetadataId, + viewId: view.id, + isVisible: viewGroup.isVisible, + position: viewGroup.position, + id: v4(), + fieldValue: viewGroup.fieldValue, + }, + }, + update: (cache, { data }) => { + const record = data?.['createViewGroup']; + if (!record) return; + + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: [record], + objectMetadataItems, + }); + }, + }), + ), + ); + }, + [ + apolloClient, + createOneRecordMutation, + objectMetadataItem, + objectMetadataItems, + ], + ); + + const updateViewGroupRecords = useCallback( + async (viewGroupsToUpdate: ViewGroup[]) => { + if (!viewGroupsToUpdate.length) return; + + const mutationPromises = viewGroupsToUpdate.map((viewGroup) => + apolloClient.mutate<{ updateViewGroup: ViewGroup }>({ + mutation: updateOneRecordMutation, + variables: { + idToUpdate: viewGroup.id, + input: { + isVisible: viewGroup.isVisible, + position: viewGroup.position, + }, + }, + // Avoid cache being updated with stale data + fetchPolicy: 'no-cache', + }), + ); + + const mutationResults = await Promise.all(mutationPromises); + + // FixMe: Using triggerCreateRecordsOptimisticEffect is actaully causing multiple records to be created + mutationResults.forEach(({ data }) => { + const record = data?.['updateViewGroup']; + + if (!record) return; + + apolloClient.cache.modify({ + id: apolloClient.cache.identify({ + __typename: 'ViewGroup', + id: record.id, + }), + fields: { + isVisible: () => record.isVisible, + position: () => record.position, + }, + }); + }); + }, + [apolloClient, updateOneRecordMutation], + ); + + return { + createViewGroupRecords, + updateViewGroupRecords, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFiltersAndSorts.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFiltersAndSorts.ts deleted file mode 100644 index 4f760b173c98..000000000000 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFiltersAndSorts.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords'; -import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; - -import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache'; -import { ViewFilter } from '@/views/types/ViewFilter'; -import { ViewSort } from '@/views/types/ViewSort'; -import { isDefined } from '~/utils/isDefined'; - -export const useCreateViewFiltersAndSorts = () => { - const { getViewFromCache } = useGetViewFromCache(); - - const { createViewSortRecords } = usePersistViewSortRecords(); - - const { createViewFilterRecords } = usePersistViewFilterRecords(); - - const createViewFiltersAndSorts = async ( - viewIdToCreateOn: string, - filtersToCreate: ViewFilter[], - sortsToCreate: ViewSort[], - ) => { - const view = await getViewFromCache(viewIdToCreateOn); - - if (!isDefined(view)) { - return; - } - - await createViewSortRecords(sortsToCreate, view); - await createViewFilterRecords(filtersToCreate, view); - }; - - return { - createViewFiltersAndSorts, - }; -}; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index fcf8a6c81647..c936ab87b37d 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -1,9 +1,14 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords'; -import { useCreateViewFiltersAndSorts } from '@/views/hooks/useCreateViewFiltersAndSorts'; +import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords'; +import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords'; +import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords'; +import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; +import { useGetViewFilterGroupsCombined } from '@/views/hooks/useGetCombinedViewFilterGroups'; import { useGetViewFiltersCombined } from '@/views/hooks/useGetCombinedViewFilters'; import { useGetViewSortsCombined } from '@/views/hooks/useGetCombinedViewSorts'; import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache'; @@ -11,6 +16,9 @@ import { currentViewIdComponentState } from '@/views/states/currentViewIdCompone import { isPersistingViewFieldsComponentState } from '@/views/states/isPersistingViewFieldsComponentState'; import { GraphQLView } from '@/views/types/GraphQLView'; import { View } from '@/views/types/View'; +import { ViewGroup } from '@/views/types/ViewGroup'; +import { ViewType } from '@/views/types/ViewType'; +import { useContext } from 'react'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-ui'; import { v4 } from 'uuid'; @@ -35,11 +43,21 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { const { createViewFieldRecords } = usePersistViewFieldRecords(); - const { createViewFiltersAndSorts } = useCreateViewFiltersAndSorts(); - const { getViewSortsCombined } = useGetViewSortsCombined(viewBarComponentId); const { getViewFiltersCombined } = useGetViewFiltersCombined(viewBarComponentId); + const { getViewFilterGroupsCombined } = + useGetViewFilterGroupsCombined(viewBarComponentId); + + const { createViewSortRecords } = usePersistViewSortRecords(); + + const { createViewGroupRecords } = usePersistViewGroupRecords(); + + const { createViewFilterRecords } = usePersistViewFilterRecords(); + + const { createViewFilterGroupRecords } = usePersistViewFilterGroupRecords(); + + const { objectMetadataItem } = useContext(RecordIndexRootPropsContext); const createViewFromCurrentView = useRecoilCallback( ({ snapshot, set }) => @@ -68,9 +86,9 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { } // Here we might instead want to get view from unsaved filters ? - const view = await getViewFromCache(currentViewId); + const sourceView = await getViewFromCache(currentViewId); - if (!isDefined(view)) { + if (!isDefined(sourceView)) { return; } @@ -78,43 +96,86 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { const newView = await createOneRecord({ id: id ?? v4(), - name: name ?? view.name, - icon: icon ?? view.icon, + name: name ?? sourceView.name, + icon: icon ?? sourceView.icon, key: null, kanbanFieldMetadataId: - kanbanFieldMetadataId ?? view.kanbanFieldMetadataId, - type: type ?? view.type, - objectMetadataId: view.objectMetadataId, + kanbanFieldMetadataId ?? sourceView.kanbanFieldMetadataId, + type: type ?? sourceView.type, + objectMetadataId: sourceView.objectMetadataId, }); if (isUndefinedOrNull(newView)) { throw new Error('Failed to create view'); } - await createViewFieldRecords(view.viewFields, newView); + await createViewFieldRecords(sourceView.viewFields, newView); + + if (type === ViewType.Kanban) { + if (!isDefined(kanbanFieldMetadataId)) { + throw new Error('Kanban view must have a kanban field'); + } + + const viewGroupsToCreate = + objectMetadataItem?.fields + ?.find((field) => field.id === kanbanFieldMetadataId) + ?.options?.map( + (option, index) => + ({ + id: v4(), + __typename: 'ViewGroup', + fieldMetadataId: kanbanFieldMetadataId, + fieldValue: option.value, + isVisible: true, + position: index, + }) satisfies ViewGroup, + ) ?? []; + + viewGroupsToCreate.push({ + __typename: 'ViewGroup', + id: v4(), + fieldValue: '', + position: viewGroupsToCreate.length, + isVisible: true, + fieldMetadataId: kanbanFieldMetadataId, + } satisfies ViewGroup); + + await createViewGroupRecords(viewGroupsToCreate, newView); + } if (shouldCopyFiltersAndSorts === true) { - const sourceViewCombinedFilters = getViewFiltersCombined(view.id); - const sourceViewCombinedSorts = getViewSortsCombined(view.id); + const sourceViewCombinedFilterGroups = getViewFilterGroupsCombined( + sourceView.id, + ); + const sourceViewCombinedFilters = getViewFiltersCombined( + sourceView.id, + ); + const sourceViewCombinedSorts = getViewSortsCombined(sourceView.id); - await createViewFiltersAndSorts( - newView.id, - sourceViewCombinedFilters, - sourceViewCombinedSorts, + await createViewSortRecords(sourceViewCombinedSorts, newView); + await createViewFilterRecords(sourceViewCombinedFilters, newView); + await createViewFilterGroupRecords( + sourceViewCombinedFilterGroups, + newView, ); } set(isPersistingViewFieldsCallbackState, false); }, [ + objectMetadataItem, + createViewSortRecords, + createViewFilterRecords, createOneRecord, createViewFieldRecords, getViewSortsCombined, getViewFiltersCombined, + getViewFilterGroupsCombined, currentViewIdCallbackState, getViewFromCache, isPersistingViewFieldsCallbackState, - createViewFiltersAndSorts, + createViewGroupRecords, + createViewFilterGroupRecords, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useGetCombinedViewFilterGroups.ts b/packages/twenty-front/src/modules/views/hooks/useGetCombinedViewFilterGroups.ts new file mode 100644 index 000000000000..e235fd79d04a --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useGetCombinedViewFilterGroups.ts @@ -0,0 +1,67 @@ +import { useRecoilCallback } from 'recoil'; + +import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState'; +import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState'; +import { View } from '@/views/types/View'; +import { getCombinedViewFilterGroups } from '@/views/utils/getCombinedViewFilterGroups'; +import { isDefined } from '~/utils/isDefined'; + +export const useGetViewFilterGroupsCombined = (viewBarComponentId?: string) => { + const { records: views } = usePrefetchedData(PrefetchKey.AllViews); + + const unsavedToUpsertViewFilterGroupsCallbackState = + useRecoilComponentCallbackStateV2( + unsavedToUpsertViewFilterGroupsComponentFamilyState, + viewBarComponentId, + ); + + const unsavedToDeleteViewFilterGroupIdsCallbackState = + useRecoilComponentCallbackStateV2( + unsavedToDeleteViewFilterGroupIdsComponentFamilyState, + viewBarComponentId, + ); + + const getViewFilterGroupsCombined = useRecoilCallback( + ({ snapshot }) => + (viewId: string) => { + const view = views.find((view) => view.id === viewId); + + if (!isDefined(view)) { + throw new Error( + `Cannot get view with id ${viewId}, because it cannot be found in client cache data.`, + ); + } + + const unsavedToUpsertViewFilterGroups = getSnapshotValue( + snapshot, + unsavedToUpsertViewFilterGroupsCallbackState({ viewId: view.id }), + ); + + const unsavedToDeleteViewFilterGroupIds = getSnapshotValue( + snapshot, + unsavedToDeleteViewFilterGroupIdsCallbackState({ viewId: view.id }), + ); + + const combinedViewFilterGroups = getCombinedViewFilterGroups( + view.viewFilterGroups ?? [], + unsavedToUpsertViewFilterGroups, + unsavedToDeleteViewFilterGroupIds, + ); + + return combinedViewFilterGroups; + }, + [ + views, + unsavedToDeleteViewFilterGroupIdsCallbackState, + unsavedToUpsertViewFilterGroupsCallbackState, + ], + ); + + return { + getViewFilterGroupsCombined, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useGetCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useGetCurrentView.ts index 1ea911ba9024..80d1d4c7b84f 100644 --- a/packages/twenty-front/src/modules/views/hooks/useGetCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useGetCurrentView.ts @@ -9,12 +9,15 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; import { isCurrentViewKeyIndexComponentState } from '@/views/states/isCurrentViewIndexComponentState'; +import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState'; import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState'; import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState'; +import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState'; import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState'; import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState'; import { viewObjectMetadataIdComponentState } from '@/views/states/viewObjectMetadataIdComponentState'; import { View } from '@/views/types/View'; +import { getCombinedViewFilterGroups } from '@/views/utils/getCombinedViewFilterGroups'; import { getCombinedViewFilters } from '@/views/utils/getCombinedViewFilters'; import { getCombinedViewSorts } from '@/views/utils/getCombinedViewSorts'; import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; @@ -70,6 +73,12 @@ export const useGetCurrentView = (viewBarInstanceId?: string) => { instanceId, ); + const unsavedToUpsertViewFilterGroups = useRecoilComponentFamilyValueV2( + unsavedToUpsertViewFilterGroupsComponentFamilyState, + { viewId }, + instanceId, + ); + const unsavedToUpsertViewSorts = useRecoilComponentFamilyValueV2( unsavedToUpsertViewSortsComponentFamilyState, { viewId }, @@ -82,6 +91,12 @@ export const useGetCurrentView = (viewBarInstanceId?: string) => { instanceId, ); + const unsavedToDeleteViewFilterGroupIds = useRecoilComponentFamilyValueV2( + unsavedToDeleteViewFilterGroupIdsComponentFamilyState, + { viewId }, + instanceId, + ); + const unsavedToDeleteViewSortIds = useRecoilComponentFamilyValueV2( unsavedToDeleteViewSortIdsComponentFamilyState, { viewId }, @@ -104,6 +119,11 @@ export const useGetCurrentView = (viewBarInstanceId?: string) => { unsavedToUpsertViewFilters, unsavedToDeleteViewFilterIds, ), + viewFilterGroups: getCombinedViewFilterGroups( + currentView.viewFilterGroups ?? [], + unsavedToUpsertViewFilterGroups, + unsavedToDeleteViewFilterGroupIds, + ), viewSorts: getCombinedViewSorts( currentView.viewSorts, unsavedToUpsertViewSorts, diff --git a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts index f5431bc45a58..d40afd1064a9 100644 --- a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts @@ -2,6 +2,7 @@ import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFi import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews'; import { getQueryVariablesFromView } from '@/views/utils/getQueryVariablesFromView'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({ objectMetadataItem, @@ -19,10 +20,15 @@ export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({ objectMetadataItem, }); + const isArrayAndJsonFilterEnabled = useIsFeatureEnabled( + 'IS_ARRAY_AND_JSON_FILTER_ENABLED', + ); + const { filter, orderBy } = getQueryVariablesFromView({ fieldMetadataItems: activeFieldMetadataItems, objectMetadataItem, view, + isArrayAndJsonFilterEnabled, }); return { diff --git a/packages/twenty-front/src/modules/views/hooks/useResetUnsavedViewStates.ts b/packages/twenty-front/src/modules/views/hooks/useResetUnsavedViewStates.ts index f3d3039bec92..e325a98e0df2 100644 --- a/packages/twenty-front/src/modules/views/hooks/useResetUnsavedViewStates.ts +++ b/packages/twenty-front/src/modules/views/hooks/useResetUnsavedViewStates.ts @@ -1,6 +1,8 @@ import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState'; import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState'; import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState'; +import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState'; import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState'; import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState'; import { useRecoilCallback } from 'recoil'; @@ -18,6 +20,12 @@ export const useResetUnsavedViewStates = (viewBarInstanceId?: string) => { viewBarInstanceId, ); + const unsavedToDeleteViewFilterGroupIdsCallbackState = + useRecoilComponentCallbackStateV2( + unsavedToDeleteViewFilterGroupIdsComponentFamilyState, + viewBarInstanceId, + ); + const setUnsavedToUpsertViewFiltersCallbackState = useRecoilComponentCallbackStateV2( unsavedToUpsertViewFiltersComponentFamilyState, @@ -30,19 +38,29 @@ export const useResetUnsavedViewStates = (viewBarInstanceId?: string) => { viewBarInstanceId, ); + const unsavedToUpsertViewFilterGroupsCallbackState = + useRecoilComponentCallbackStateV2( + unsavedToUpsertViewFilterGroupsComponentFamilyState, + viewBarInstanceId, + ); + const resetUnsavedViewStates = useRecoilCallback( ({ set }) => (viewId: string) => { + set(unsavedToDeleteViewFilterGroupIdsCallbackState({ viewId }), []); set(setUnsavedToDeleteViewFilterIdsCallbackState({ viewId }), []); set(setUnsavedToDeleteViewSortIdsCallbackState({ viewId }), []); + set(unsavedToUpsertViewFilterGroupsCallbackState({ viewId }), []); set(setUnsavedToUpsertViewFiltersCallbackState({ viewId }), []); set(unsavedToUpsertViewSortsCallbackState({ viewId }), []); }, [ unsavedToUpsertViewSortsCallbackState, setUnsavedToUpsertViewFiltersCallbackState, + unsavedToUpsertViewFilterGroupsCallbackState, setUnsavedToDeleteViewSortIdsCallbackState, setUnsavedToDeleteViewFilterIdsCallbackState, + unsavedToDeleteViewFilterGroupIdsCallbackState, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts index 3b7fdeaebdf0..f2f94e240e15 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts @@ -2,13 +2,16 @@ import { useRecoilCallback } from 'recoil'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords'; import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords'; import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache'; import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates'; import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; +import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState'; import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState'; import { unsavedToDeleteViewSortIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewSortIdsComponentFamilyState'; +import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState'; import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState'; import { unsavedToUpsertViewSortsComponentFamilyState } from '@/views/states/unsavedToUpsertViewSortsComponentFamilyState'; import { isDefined } from '~/utils/isDefined'; @@ -48,6 +51,18 @@ export const useSaveCurrentViewFiltersAndSorts = ( viewBarComponentId, ); + const unsavedToUpsertViewFilterGroupsCallbackState = + useRecoilComponentCallbackStateV2( + unsavedToUpsertViewFilterGroupsComponentFamilyState, + viewBarComponentId, + ); + + const unsavedToDeleteViewFilterGroupIdsCallbackState = + useRecoilComponentCallbackStateV2( + unsavedToDeleteViewFilterGroupIdsComponentFamilyState, + viewBarComponentId, + ); + const { createViewSortRecords, updateViewSortRecords, @@ -60,6 +75,12 @@ export const useSaveCurrentViewFiltersAndSorts = ( deleteViewFilterRecords, } = usePersistViewFilterRecords(); + const { + createViewFilterGroupRecords, + deleteViewFilterGroupRecords, + updateViewFilterGroupRecords, + } = usePersistViewFilterGroupRecords(); + const { resetUnsavedViewStates } = useResetUnsavedViewStates(viewBarComponentId); @@ -131,14 +152,14 @@ export const useSaveCurrentViewFiltersAndSorts = ( const viewFiltersToCreate = unsavedToUpsertViewFilters.filter( (viewFilter) => !view.viewFilters.some( - (vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId, + (viewFilterToFilter) => viewFilterToFilter.id === viewFilter.id, ), ); const viewFiltersToUpdate = unsavedToUpsertViewFilters.filter( (viewFilter) => view.viewFilters.some( - (vf) => vf.fieldMetadataId === viewFilter.fieldMetadataId, + (viewFilterToFilter) => viewFilterToFilter.id === viewFilter.id, ), ); @@ -156,6 +177,55 @@ export const useSaveCurrentViewFiltersAndSorts = ( ], ); + const saveViewFilterGroups = useRecoilCallback( + ({ snapshot }) => + async (viewId: string) => { + const unsavedToDeleteViewFilterGroupIds = getSnapshotValue( + snapshot, + unsavedToDeleteViewFilterGroupIdsCallbackState({ viewId }), + ); + + const unsavedToUpsertViewFilterGroups = getSnapshotValue( + snapshot, + unsavedToUpsertViewFilterGroupsCallbackState({ viewId }), + ); + + const view = await getViewFromCache(viewId); + + if (isUndefinedOrNull(view)) { + return; + } + + const viewFilterGroupsToCreate = unsavedToUpsertViewFilterGroups.filter( + (viewFilterGroup) => + !view.viewFilterGroups?.some( + (viewFilterGroupToFilter) => + viewFilterGroupToFilter.id === viewFilterGroup.id, + ), + ); + + const viewFilterGroupsToUpdate = unsavedToUpsertViewFilterGroups.filter( + (viewFilterGroup) => + view.viewFilterGroups?.some( + (viewFilterGroupToFilter) => + viewFilterGroupToFilter.id === viewFilterGroup.id, + ), + ); + + await createViewFilterGroupRecords(viewFilterGroupsToCreate, view); + await updateViewFilterGroupRecords(viewFilterGroupsToUpdate); + await deleteViewFilterGroupRecords(unsavedToDeleteViewFilterGroupIds); + }, + [ + getViewFromCache, + createViewFilterGroupRecords, + deleteViewFilterGroupRecords, + unsavedToDeleteViewFilterGroupIdsCallbackState, + unsavedToUpsertViewFilterGroupsCallbackState, + updateViewFilterGroupRecords, + ], + ); + const saveCurrentViewFilterAndSorts = useRecoilCallback( ({ snapshot }) => async (viewIdFromProps?: string) => { @@ -169,6 +239,7 @@ export const useSaveCurrentViewFiltersAndSorts = ( const viewId = viewIdFromProps ?? currentViewId; + await saveViewFilterGroups(viewId); await saveViewFilters(viewId); await saveViewSorts(viewId); @@ -179,6 +250,7 @@ export const useSaveCurrentViewFiltersAndSorts = ( resetUnsavedViewStates, saveViewFilters, saveViewSorts, + saveViewFilterGroups, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts new file mode 100644 index 000000000000..384b2628414f --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts @@ -0,0 +1,96 @@ +import { useRecoilCallback } from 'recoil'; + +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords'; +import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache'; +import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; +import { ViewGroup } from '@/views/types/ViewGroup'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; + +export const useSaveCurrentViewGroups = (viewBarComponentId?: string) => { + const { createViewGroupRecords, updateViewGroupRecords } = + usePersistViewGroupRecords(); + + const { getViewFromCache } = useGetViewFromCache(); + + const currentViewIdCallbackState = useRecoilComponentCallbackStateV2( + currentViewIdComponentState, + viewBarComponentId, + ); + + const saveViewGroups = useRecoilCallback( + ({ snapshot }) => + async (viewGroupsToSave: ViewGroup[]) => { + const currentViewId = snapshot + .getLoadable(currentViewIdCallbackState) + .getValue(); + + if (!currentViewId) { + return; + } + + const view = await getViewFromCache(currentViewId); + + if (isUndefinedOrNull(view)) { + return; + } + + const currentViewGroups = view.viewGroups; + + const viewGroupsToUpdate = viewGroupsToSave + .map((viewGroupToSave) => { + const existingField = currentViewGroups.find( + (currentViewGroup) => + currentViewGroup.fieldValue === viewGroupToSave.fieldValue, + ); + + if (isUndefinedOrNull(existingField)) { + return undefined; + } + + if ( + isDeeplyEqual( + { + position: existingField.position, + isVisible: existingField.isVisible, + }, + { + position: viewGroupToSave.position, + isVisible: viewGroupToSave.isVisible, + }, + ) + ) { + return undefined; + } + + return { ...viewGroupToSave, id: existingField.id }; + }) + .filter(isDefined); + + const viewGroupsToCreate = viewGroupsToSave.filter( + (viewFieldToSave) => + !currentViewGroups.some( + (currentViewGroup) => + currentViewGroup.fieldValue === viewFieldToSave.fieldValue, + ), + ); + + await Promise.all([ + createViewGroupRecords(viewGroupsToCreate, view), + updateViewGroupRecords(viewGroupsToUpdate), + ]); + }, + [ + createViewGroupRecords, + currentViewIdCallbackState, + getViewFromCache, + updateViewGroupRecords, + ], + ); + + return { + saveViewGroups, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useUpsertCombinedViewFilters.ts b/packages/twenty-front/src/modules/views/hooks/useUpsertCombinedViewFilters.ts index 7c5e2d99b956..bc4667e5841c 100644 --- a/packages/twenty-front/src/modules/views/hooks/useUpsertCombinedViewFilters.ts +++ b/packages/twenty-front/src/modules/views/hooks/useUpsertCombinedViewFilters.ts @@ -8,6 +8,7 @@ import { currentViewIdComponentState } from '@/views/states/currentViewIdCompone import { unsavedToDeleteViewFilterIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterIdsComponentFamilyState'; import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState'; import { ViewFilter } from '@/views/types/ViewFilter'; +import { shouldReplaceFilter } from '@/views/utils/shouldReplaceFilter'; import { isDefined } from '~/utils/isDefined'; export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => { @@ -59,19 +60,16 @@ export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => { } const matchingFilterInCurrentView = currentView.viewFilters.find( - (viewFilter) => - viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId, + (viewFilter) => shouldReplaceFilter(viewFilter, upsertedFilter), ); const matchingFilterInUnsavedFilters = unsavedToUpsertViewFilters.find( - (viewFilter) => - viewFilter.fieldMetadataId === upsertedFilter.fieldMetadataId, + (viewFilter) => shouldReplaceFilter(viewFilter, upsertedFilter), ); if (isDefined(matchingFilterInUnsavedFilters)) { const updatedFilters = unsavedToUpsertViewFilters.map((viewFilter) => - viewFilter.fieldMetadataId === - matchingFilterInUnsavedFilters.fieldMetadataId + shouldReplaceFilter(viewFilter, matchingFilterInUnsavedFilters) ? { ...viewFilter, ...upsertedFilter, id: viewFilter.id } : viewFilter, ); diff --git a/packages/twenty-front/src/modules/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState.ts b/packages/twenty-front/src/modules/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState.ts new file mode 100644 index 000000000000..c9609c830915 --- /dev/null +++ b/packages/twenty-front/src/modules/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState.ts @@ -0,0 +1,9 @@ +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const unsavedToDeleteViewFilterGroupIdsComponentFamilyState = + createComponentFamilyStateV2({ + key: 'unsavedToDeleteViewFilterGroupIdsComponentFamilyState', + defaultValue: [], + componentInstanceContext: ViewComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState.ts b/packages/twenty-front/src/modules/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState.ts new file mode 100644 index 000000000000..a43aae2f16c5 --- /dev/null +++ b/packages/twenty-front/src/modules/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState.ts @@ -0,0 +1,10 @@ +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; + +export const unsavedToUpsertViewFilterGroupsComponentFamilyState = + createComponentFamilyStateV2({ + key: 'unsavedToUpsertViewFilterGroupsComponentFamilyState', + defaultValue: [], + componentInstanceContext: ViewComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/views/states/unsavedToUpsertViewFiltersComponentFamilyState.ts b/packages/twenty-front/src/modules/views/states/unsavedToUpsertViewFiltersComponentFamilyState.ts index bf48b10708cf..f0f8b380ff35 100644 --- a/packages/twenty-front/src/modules/views/states/unsavedToUpsertViewFiltersComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/views/states/unsavedToUpsertViewFiltersComponentFamilyState.ts @@ -1,6 +1,6 @@ import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -import { ViewFilter } from '../types/ViewFilter'; +import { ViewFilter } from '@/views/types/ViewFilter'; export const unsavedToUpsertViewFiltersComponentFamilyState = createComponentFamilyStateV2({ diff --git a/packages/twenty-front/src/modules/views/types/GraphQLView.ts b/packages/twenty-front/src/modules/views/types/GraphQLView.ts index c657f83b5749..cb8729f91564 100644 --- a/packages/twenty-front/src/modules/views/types/GraphQLView.ts +++ b/packages/twenty-front/src/modules/views/types/GraphQLView.ts @@ -1,5 +1,7 @@ import { ViewField } from '@/views/types/ViewField'; import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import { ViewGroup } from '@/views/types/ViewGroup'; import { ViewKey } from '@/views/types/ViewKey'; import { ViewSort } from '@/views/types/ViewSort'; import { ViewType } from '@/views/types/ViewType'; @@ -14,7 +16,9 @@ export type GraphQLView = { isCompact: boolean; viewFields: ViewField[]; viewFilters: ViewFilter[]; + viewFilterGroups?: ViewFilterGroup[]; viewSorts: ViewSort[]; + viewGroups: ViewGroup[]; position: number; icon: string; }; diff --git a/packages/twenty-front/src/modules/views/types/View.ts b/packages/twenty-front/src/modules/views/types/View.ts index a3c9cac58b83..111c5a5002ed 100644 --- a/packages/twenty-front/src/modules/views/types/View.ts +++ b/packages/twenty-front/src/modules/views/types/View.ts @@ -1,5 +1,7 @@ import { ViewField } from '@/views/types/ViewField'; import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; +import { ViewGroup } from '@/views/types/ViewGroup'; import { ViewKey } from '@/views/types/ViewKey'; import { ViewSort } from '@/views/types/ViewSort'; import { ViewType } from '@/views/types/ViewType'; @@ -12,7 +14,9 @@ export type View = { objectMetadataId: string; isCompact: boolean; viewFields: ViewField[]; + viewGroups: ViewGroup[]; viewFilters: ViewFilter[]; + viewFilterGroups?: ViewFilterGroup[]; viewSorts: ViewSort[]; kanbanFieldMetadataId: string; position: number; diff --git a/packages/twenty-front/src/modules/views/types/ViewFilter.ts b/packages/twenty-front/src/modules/views/types/ViewFilter.ts index f5175cf41433..a44cab7aee67 100644 --- a/packages/twenty-front/src/modules/views/types/ViewFilter.ts +++ b/packages/twenty-front/src/modules/views/types/ViewFilter.ts @@ -12,5 +12,7 @@ export type ViewFilter = { createdAt?: string; updatedAt?: string; viewId?: string; + viewFilterGroupId?: string; + positionInViewFilterGroup?: number | null; definition?: FilterDefinition; }; diff --git a/packages/twenty-front/src/modules/views/types/ViewFilterGroup.ts b/packages/twenty-front/src/modules/views/types/ViewFilterGroup.ts new file mode 100644 index 000000000000..b17719946eef --- /dev/null +++ b/packages/twenty-front/src/modules/views/types/ViewFilterGroup.ts @@ -0,0 +1,10 @@ +import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator'; + +export type ViewFilterGroup = { + __typename: 'ViewFilterGroup'; + id: string; + viewId: string; + parentViewFilterGroupId?: string | null; + logicalOperator: ViewFilterGroupLogicalOperator; + positionInViewFilterGroup?: number | null; +}; diff --git a/packages/twenty-front/src/modules/views/types/ViewFilterGroupLogicalOperator.ts b/packages/twenty-front/src/modules/views/types/ViewFilterGroupLogicalOperator.ts new file mode 100644 index 000000000000..ad82a363cd13 --- /dev/null +++ b/packages/twenty-front/src/modules/views/types/ViewFilterGroupLogicalOperator.ts @@ -0,0 +1,4 @@ +export enum ViewFilterGroupLogicalOperator { + AND = 'AND', + OR = 'OR', +} diff --git a/packages/twenty-front/src/modules/views/types/ViewGroup.ts b/packages/twenty-front/src/modules/views/types/ViewGroup.ts new file mode 100644 index 000000000000..9f0cd3822298 --- /dev/null +++ b/packages/twenty-front/src/modules/views/types/ViewGroup.ts @@ -0,0 +1,8 @@ +export type ViewGroup = { + __typename: 'ViewGroup'; + id: string; + fieldMetadataId: string; + isVisible: boolean; + fieldValue: string; + position: number; +}; diff --git a/packages/twenty-front/src/modules/views/utils/getCombinedViewFilterGroups.ts b/packages/twenty-front/src/modules/views/utils/getCombinedViewFilterGroups.ts new file mode 100644 index 000000000000..c2ee1af3485b --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/getCombinedViewFilterGroups.ts @@ -0,0 +1,38 @@ +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; + +export const getCombinedViewFilterGroups = ( + viewFilterGroups: ViewFilterGroup[], + unsavedToUpsertViewFilterGroups: ViewFilterGroup[], + unsavedToDeleteViewFilterGroupIds: string[], +): ViewFilterGroup[] => { + const toCreateViewFilterGroups = unsavedToUpsertViewFilterGroups.filter( + (toUpsertViewFilterGroup) => + !viewFilterGroups.some( + (viewFilterGroup) => viewFilterGroup.id === toUpsertViewFilterGroup.id, + ), + ); + + const toUpdateViewFilterGroups = unsavedToUpsertViewFilterGroups.filter( + (toUpsertViewFilterGroup) => + viewFilterGroups.some( + (viewFilterGroup) => viewFilterGroup.id === toUpsertViewFilterGroup.id, + ), + ); + + const combinedViewFilterGroups = viewFilterGroups + .filter( + (viewFilterGroup) => + !unsavedToDeleteViewFilterGroupIds.includes(viewFilterGroup.id), + ) + .map((viewFilterGroup) => { + const toUpdateViewFilterGroup = toUpdateViewFilterGroups.find( + (toUpdateViewFilterGroup) => + toUpdateViewFilterGroup.id === viewFilterGroup.id, + ); + + return toUpdateViewFilterGroup ?? viewFilterGroup; + }) + .concat(toCreateViewFilterGroups); + + return combinedViewFilterGroups; +}; diff --git a/packages/twenty-front/src/modules/views/utils/getCombinedViewFilters.ts b/packages/twenty-front/src/modules/views/utils/getCombinedViewFilters.ts index 1e172505dd96..7b81fcb152b4 100644 --- a/packages/twenty-front/src/modules/views/utils/getCombinedViewFilters.ts +++ b/packages/twenty-front/src/modules/views/utils/getCombinedViewFilters.ts @@ -8,24 +8,19 @@ export const getCombinedViewFilters = ( const toCreateViewFilters = toUpsertViewFilters.filter( (toUpsertViewFilter) => !viewFilters.some( - (viewFilter) => - viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId, + (viewFilter) => viewFilter.id === toUpsertViewFilter.id, ), ); const toUpdateViewFilters = toUpsertViewFilters.filter((toUpsertViewFilter) => - viewFilters.some( - (viewFilter) => - viewFilter.fieldMetadataId === toUpsertViewFilter.fieldMetadataId, - ), + viewFilters.some((viewFilter) => viewFilter.id === toUpsertViewFilter.id), ); const combinedViewFilters = viewFilters .filter((viewFilter) => !toDeleteViewFilterIds.includes(viewFilter.id)) .map((viewFilter) => { const toUpdateViewFilter = toUpdateViewFilters.find( - (toUpdateViewFilter) => - toUpdateViewFilter.fieldMetadataId === viewFilter.fieldMetadataId, + (toUpdateViewFilter) => toUpdateViewFilter.id === viewFilter.id, ); return toUpdateViewFilter ?? viewFilter; diff --git a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts index 7120068f8421..229929252af1 100644 --- a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts +++ b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts @@ -3,7 +3,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { View } from '@/views/types/View'; import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; @@ -13,10 +13,12 @@ export const getQueryVariablesFromView = ({ view, fieldMetadataItems, objectMetadataItem, + isArrayAndJsonFilterEnabled, }: { view: View | null | undefined; fieldMetadataItems: FieldMetadataItem[]; objectMetadataItem: ObjectMetadataItem; + isArrayAndJsonFilterEnabled: boolean; }) => { if (!isDefined(view)) { return { @@ -25,19 +27,21 @@ export const getQueryVariablesFromView = ({ }; } - const { viewFilters, viewSorts } = view; + const { viewFilterGroups, viewFilters, viewSorts } = view; const filterDefinitions = formatFieldMetadataItemsAsFilterDefinitions({ fields: fieldMetadataItems, + isArrayAndJsonFilterEnabled, }); const sortDefinitions = formatFieldMetadataItemsAsSortDefinitions({ fields: fieldMetadataItems, }); - const filter = turnObjectDropdownFilterIntoQueryFilter( + const filter = computeViewRecordGqlOperationFilter( mapViewFiltersToFilters(viewFilters, filterDefinitions), objectMetadataItem?.fields ?? [], + viewFilterGroups ?? [], ); const orderBy = turnSortsIntoOrderBy( diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts b/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts new file mode 100644 index 000000000000..b9251945195c --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts @@ -0,0 +1,17 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { ViewGroup } from '@/views/types/ViewGroup'; + +export const mapRecordGroupDefinitionsToViewGroups = ( + groupDefinitions: RecordGroupDefinition[], +): ViewGroup[] => { + return groupDefinitions.map( + (groupDefinition): ViewGroup => ({ + __typename: 'ViewGroup', + id: groupDefinition.id, + fieldMetadataId: groupDefinition.fieldMetadataId, + position: groupDefinition.position, + isVisible: groupDefinition.isVisible ?? true, + fieldValue: groupDefinition.value ?? '', + }), + ); +}; diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts index 773815c7ca58..2551637a0223 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts @@ -23,6 +23,8 @@ export const mapViewFiltersToFilters = ( value: viewFilter.value, displayValue: viewFilter.displayValue, operand: viewFilter.operand, + viewFilterGroupId: viewFilter.viewFilterGroupId, + positionInViewFilterGroup: viewFilter.positionInViewFilterGroup, definition: viewFilter.definition ?? availableFilterDefinition, }; }) diff --git a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts new file mode 100644 index 000000000000..3767abe6a65d --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts @@ -0,0 +1,79 @@ +import { isDefined } from '~/utils/isDefined'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { + RecordGroupDefinition, + RecordGroupDefinitionType, +} from '@/object-record/record-group/types/RecordGroupDefinition'; +import { ViewGroup } from '@/views/types/ViewGroup'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const mapViewGroupsToRecordGroupDefinitions = ({ + objectMetadataItem, + viewGroups, +}: { + objectMetadataItem: ObjectMetadataItem; + viewGroups: ViewGroup[]; +}): RecordGroupDefinition[] => { + if (viewGroups?.length === 0) { + return []; + } + + const fieldMetadataId = viewGroups?.[0]?.fieldMetadataId; + const selectFieldMetadataItem = objectMetadataItem.fields.find( + (field) => + field.id === fieldMetadataId && field.type === FieldMetadataType.Select, + ); + + if (!selectFieldMetadataItem) { + return []; + } + + if (!selectFieldMetadataItem.options) { + throw new Error( + `Select Field ${objectMetadataItem.nameSingular} has no options`, + ); + } + + const recordGroupDefinitionsFromViewGroups = viewGroups + .map((viewGroup) => { + const selectedOption = selectFieldMetadataItem.options?.find( + (option) => option.value === viewGroup.fieldValue, + ); + + if (!selectedOption) return null; + + return { + id: viewGroup.id, + fieldMetadataId: viewGroup.fieldMetadataId, + type: RecordGroupDefinitionType.Value, + title: selectedOption.label, + value: selectedOption.value, + color: selectedOption.color, + position: viewGroup.position, + isVisible: viewGroup.isVisible, + } as RecordGroupDefinition; + }) + .filter(isDefined) + .sort((a, b) => a.position - b.position); + + if (selectFieldMetadataItem.isNullable === true) { + const noValueColumn = { + id: 'no-value', + title: 'No Value', + type: RecordGroupDefinitionType.NoValue, + value: null, + position: + recordGroupDefinitionsFromViewGroups + .map((option) => option.position) + .reduce((a, b) => Math.max(a, b), 0) + 1, + isVisible: true, + fieldMetadataId: selectFieldMetadataItem.id, + color: 'transparent', + } satisfies RecordGroupDefinition; + + return [...recordGroupDefinitionsFromViewGroups, noValueColumn]; + } + + return recordGroupDefinitionsFromViewGroups; +}; diff --git a/packages/twenty-front/src/modules/views/utils/shouldReplaceFilter.ts b/packages/twenty-front/src/modules/views/utils/shouldReplaceFilter.ts new file mode 100644 index 000000000000..ac183045153e --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/shouldReplaceFilter.ts @@ -0,0 +1,18 @@ +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { isDefined } from 'twenty-ui'; + +export const shouldReplaceFilter = ( + oldFilter: Pick, + newFilter: Pick, +) => { + const isNewFilterAdvancedFilter = isDefined(newFilter.viewFilterGroupId); + + if (isNewFilterAdvancedFilter) { + return newFilter.id === oldFilter.id; + } else { + return ( + newFilter.fieldMetadataId === oldFilter.fieldMetadataId && + !oldFilter.viewFilterGroupId + ); + } +}; diff --git a/packages/twenty-front/src/modules/views/utils/sortViewFilterGroupsOutermostFirst.ts b/packages/twenty-front/src/modules/views/utils/sortViewFilterGroupsOutermostFirst.ts new file mode 100644 index 000000000000..7df13e95b8b5 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/sortViewFilterGroupsOutermostFirst.ts @@ -0,0 +1,15 @@ +import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; + +export const sortViewFilterGroupsOutermostFirst = ( + viewFilterGroups: ViewFilterGroup[], + parentViewFilterGroupId?: string, +): ViewFilterGroup[] => { + const childGroups = viewFilterGroups.filter( + (group) => group.parentViewFilterGroupId === parentViewFilterGroupId, + ); + + return childGroups.flatMap((group) => [ + group, + ...sortViewFilterGroupsOutermostFirst(viewFilterGroups, group.id), + ]); +}; diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/computeVariableDateViewFilterValue.ts similarity index 83% rename from packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts rename to packages/twenty-front/src/modules/views/view-filter-value/utils/computeVariableDateViewFilterValue.ts index 1b09bc91348b..d34e4b7f1b35 100644 --- a/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/computeVariableDateViewFilterValue.ts @@ -1,7 +1,7 @@ import { VariableDateViewFilterValueDirection, VariableDateViewFilterValueUnit, -} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +} from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; export const computeVariableDateViewFilterValue = ( direction: VariableDateViewFilterValueDirection, diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts similarity index 100% rename from packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts rename to packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts similarity index 91% rename from packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts rename to packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts index 34afbb46ad1a..c5c4e064507b 100644 --- a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts @@ -1,7 +1,7 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { resolveNumberViewFilterValue } from '@/views/utils/view-filter-value/resolveNumberViewFilterValue'; +import { resolveNumberViewFilterValue } from '@/views/view-filter-value/utils/resolveNumberViewFilterValue'; import { resolveDateViewFilterValue, ResolvedDateViewFilterValue, diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveNumberViewFilterValue.ts similarity index 100% rename from packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts rename to packages/twenty-front/src/modules/views/view-filter-value/utils/resolveNumberViewFilterValue.ts diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx index c3ce24018b9e..84d32dc19365 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx @@ -4,8 +4,8 @@ import { IconLayoutKanban, IconTable, IconX } from 'twenty-ui'; import { IconPicker } from '@/ui/input/components/IconPicker'; import { Select } from '@/ui/input/components/Select'; +import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; -import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; @@ -120,11 +120,11 @@ export const ViewPickerContentCreateMode = () => { disableBlur onClose={() => setHotkeyScope(ViewsHotkeyScope.ListDropdown)} /> - { + onChange={(value) => { setViewPickerIsDirty(true); - setViewPickerInputName(event.target.value); + setViewPickerInputName(value); }} autoFocus /> diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEditMode.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEditMode.tsx index 3246651fb1fe..c478dbfb36b9 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEditMode.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentEditMode.tsx @@ -2,8 +2,8 @@ import { Key } from 'ts-key-enum'; import { IconChevronLeft } from 'twenty-ui'; import { IconPicker } from '@/ui/input/components/IconPicker'; +import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; -import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; @@ -79,11 +79,11 @@ export const ViewPickerContentEditMode = () => { disableBlur onClose={() => setHotkeyScope(ViewsHotkeyScope.ListDropdown)} /> - { + onChange={(value) => { setViewPickerIsDirty(true); - setViewPickerInputName(event.target.value); + setViewPickerInputName(value); }} autoFocus /> diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx index 2fb0cb4f42c4..b2bb66204f20 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx @@ -1,4 +1,3 @@ -import { Button } from '@/ui/input/button/components/Button'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewType } from '@/views/types/ViewType'; import { useCreateViewFromCurrentState } from '@/views/view-picker/hooks/useCreateViewFromCurrentState'; @@ -8,6 +7,7 @@ import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState'; import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState'; import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState'; +import { Button } from 'twenty-ui'; export const ViewPickerCreateButton = () => { const { availableFieldsForKanban, navigateToSelectSettings } = diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerEditButton.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerEditButton.tsx index a2f67dcabb16..16df5ea4a3c7 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerEditButton.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerEditButton.tsx @@ -1,4 +1,3 @@ -import { Button } from '@/ui/input/button/components/Button'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewType } from '@/views/types/ViewType'; import { useCreateViewFromCurrentState } from '@/views/view-picker/hooks/useCreateViewFromCurrentState'; @@ -8,6 +7,7 @@ import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState'; import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState'; import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState'; +import { Button } from 'twenty-ui'; export const ViewPickerEditButton = () => { const { availableFieldsForKanban, navigateToSelectSettings } = diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx index 80d3f329a83a..00200448860d 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx @@ -1,15 +1,21 @@ import styled from '@emotion/styled'; import { DropResult } from '@hello-pangea/dnd'; import { MouseEvent, useCallback } from 'react'; -import { IconLock, IconPencil, IconPlus, useIcons } from 'twenty-ui'; +import { + IconLock, + IconPencil, + IconPlus, + LightIconButtonAccent, + MenuItem, + MenuItemDraggable, + useIcons, +} from 'twenty-ui'; import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useChangeView } from '@/views/hooks/useChangeView'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; @@ -60,8 +66,6 @@ export const ViewPickerListContent = () => { const { getIcon } = useIcons(); - const indexView = viewsOnCurrentObject.find((view) => view.key === 'INDEX'); - const handleDragEnd = useCallback( (result: DropResult) => { if (!result.destination) return; @@ -81,33 +85,29 @@ export const ViewPickerListContent = () => { return ( <> - {indexView && ( - handleViewSelect(indexView.id)} - LeftIcon={getIcon(indexView.icon)} - text={indexView.name} - accent="placeholder" - isDragDisabled - /> - )} indexView?.id !== view.id) - .map((view, index) => ( - ( + handleViewSelect(view.id)} + LeftIcon={getIcon(view.icon)} + text={view.name} + /> + ) : ( { Icon: IconPencil, onClick: (event: MouseEvent) => handleEditViewButtonClick(event, view.id), + accent: 'tertiary' as LightIconButtonAccent, }, ].filter(isDefined)} - isIconDisplayedOnHoverOnly={ - indexView?.id === view.id ? false : true - } + isIconDisplayedOnHoverOnly={true} onClick={() => handleViewSelect(view.id)} LeftIcon={getIcon(view.icon)} text={view.name} /> - } - /> - ))} + ) + } + /> + ))} /> diff --git a/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx b/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx new file mode 100644 index 000000000000..d66b4629a732 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx @@ -0,0 +1,62 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { buildShowPageURL } from '@/object-record/record-show/utils/buildShowPageURL'; +import { + ConfirmationModal, + StyledCenteredButton, +} from '@/ui/layout/modal/components/ConfirmationModal'; +import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState'; +import { WorkflowVersion } from '@/workflow/types/Workflow'; +import { useRecoilState } from 'recoil'; + +export const OverrideWorkflowDraftConfirmationModal = ({ + draftWorkflowVersionId, + workflowVersionUpdateInput, +}: { + draftWorkflowVersionId: string; + workflowVersionUpdateInput: Pick; +}) => { + const [ + openOverrideWorkflowDraftConfirmationModal, + setOpenOverrideWorkflowDraftConfirmationModal, + ] = useRecoilState(openOverrideWorkflowDraftConfirmationModalState); + + const { updateOneRecord: updateOneWorkflowVersion } = + useUpdateOneRecord({ + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + }); + + const handleOverrideDraft = async () => { + await updateOneWorkflowVersion({ + idToUpdate: draftWorkflowVersionId, + updateOneRecordInput: workflowVersionUpdateInput, + }); + }; + + return ( + <> + { + setOpenOverrideWorkflowDraftConfirmationModal(false); + }} + variant="secondary" + title="Go to Draft" + fullWidth + /> + } + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx index c2000974e5f9..2e741bcf37cc 100644 --- a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx @@ -1,9 +1,9 @@ -import { Button } from '@/ui/input/button/components/Button'; import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion'; import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion'; import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { + Button, IconPlayerPlay, IconPlayerStop, IconPower, diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx index 79cbe7c27f77..03c9c5a520c2 100644 --- a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx @@ -1,14 +1,21 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; -import { Button } from '@/ui/input/button/components/Button'; +import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal'; import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion'; import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion'; import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion'; import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; +import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState'; import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow'; -import { IconPencil, IconPlayerStop, IconPower, isDefined } from 'twenty-ui'; +import { useSetRecoilState } from 'recoil'; +import { + Button, + IconPencil, + IconPlayerStop, + IconPower, + isDefined, +} from 'twenty-ui'; export const RecordShowPageWorkflowVersionHeader = ({ workflowVersionId, @@ -46,6 +53,8 @@ export const RecordShowPageWorkflowVersionHeader = ({ skip: !isDefined(workflowVersion), limit: 1, }); + const draftWorkflowVersion: WorkflowVersion | undefined = + draftWorkflowVersions[0]; const showUseAsDraftButton = !loadingDraftWorkflowVersions && @@ -57,7 +66,7 @@ export const RecordShowPageWorkflowVersionHeader = ({ workflowVersionRelatedWorkflowQuery.record.lastPublishedVersionId; const hasAlreadyDraftVersion = - !loadingDraftWorkflowVersions && draftWorkflowVersions.length > 0; + !loadingDraftWorkflowVersions && isDefined(draftWorkflowVersion); const isWaitingForWorkflowVersion = !isDefined(workflowVersion); @@ -65,10 +74,9 @@ export const RecordShowPageWorkflowVersionHeader = ({ const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion(); const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); - const { updateOneRecord: updateOneWorkflowVersion } = - useUpdateOneRecord({ - objectNameSingular: CoreObjectNameSingular.WorkflowVersion, - }); + const setOpenOverrideWorkflowDraftConfirmationModal = useSetRecoilState( + openOverrideWorkflowDraftConfirmationModalState, + ); return ( <> @@ -80,13 +88,7 @@ export const RecordShowPageWorkflowVersionHeader = ({ disabled={isWaitingForWorkflowVersion} onClick={async () => { if (hasAlreadyDraftVersion) { - await updateOneWorkflowVersion({ - idToUpdate: draftWorkflowVersions[0].id, - updateOneRecordInput: { - trigger: workflowVersion.trigger, - steps: workflowVersion.steps, - }, - }); + setOpenOverrideWorkflowDraftConfirmationModal(true); } else { await createNewWorkflowVersion({ workflowId: workflowVersion.workflow.id, @@ -125,6 +127,16 @@ export const RecordShowPageWorkflowVersionHeader = ({ }} /> ) : null} + + {isDefined(workflowVersion) && isDefined(draftWorkflowVersion) ? ( + + ) : null} ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx index 094cc99e0996..0d13a0b394f7 100644 --- a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectActionContent.tsx @@ -1,8 +1,8 @@ -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { ACTIONS } from '@/workflow/constants/Actions'; import { useCreateStep } from '@/workflow/hooks/useCreateStep'; import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; import styled from '@emotion/styled'; +import { MenuItem } from 'twenty-ui'; const StyledActionListContainer = styled.div` display: flex; @@ -24,18 +24,16 @@ export const RightDrawerWorkflowSelectActionContent = ({ }); return ( - <> - - {ACTIONS.map((action) => ( - { - return createStep(action.type); - }} - /> - ))} - - + + {ACTIONS.map((action) => ( + { + return createStep(action.type); + }} + /> + ))} + ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerType.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerType.tsx new file mode 100644 index 000000000000..7eb10fc6ecb2 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerType.tsx @@ -0,0 +1,16 @@ +import { RightDrawerWorkflowSelectTriggerTypeContent } from '@/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent'; +import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; +import { workflowIdState } from '@/workflow/states/workflowIdState'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const RightDrawerWorkflowSelectTriggerType = () => { + const workflowId = useRecoilValue(workflowIdState); + const workflow = useWorkflowWithCurrentVersion(workflowId); + + if (!isDefined(workflow)) { + return null; + } + + return ; +}; diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx new file mode 100644 index 000000000000..abe47dd1ebea --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowSelectTriggerTypeContent.tsx @@ -0,0 +1,58 @@ +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId'; +import { TRIGGER_TYPES } from '@/workflow/constants/TriggerTypes'; +import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger'; +import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; +import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; +import { getTriggerDefaultDefinition } from '@/workflow/utils/getTriggerDefaultDefinition'; +import styled from '@emotion/styled'; +import { useSetRecoilState } from 'recoil'; +import { MenuItem } from 'twenty-ui'; + +const StyledActionListContainer = styled.div` + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; + + padding-block: ${({ theme }) => theme.spacing(1)}; + padding-inline: ${({ theme }) => theme.spacing(2)}; +`; + +export const RightDrawerWorkflowSelectTriggerTypeContent = ({ + workflow, +}: { + workflow: WorkflowWithCurrentVersion; +}) => { + const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow }); + + const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); + + const { openRightDrawer } = useRightDrawer(); + const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); + + return ( + + {TRIGGER_TYPES.map((action) => ( + { + await updateTrigger( + getTriggerDefaultDefinition({ + type: action.type, + activeObjectMetadataItems, + }), + ); + + setWorkflowSelectedNode(TRIGGER_STEP_ID); + + openRightDrawer(RightDrawerPages.WorkflowStepEdit); + }} + /> + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx index 8484a29e7eaf..9d8c6b39b303 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx @@ -66,7 +66,9 @@ const StyledStepNodeLabel = styled.div<{ variant?: Variant }>` font-weight: ${({ theme }) => theme.font.weight.medium}; column-gap: ${({ theme }) => theme.spacing(2)}; color: ${({ variant, theme }) => - variant === 'placeholder' ? theme.font.color.extraLight : null}; + variant === 'placeholder' + ? theme.font.color.extraLight + : theme.font.color.primary}; `; const StyledSourceHandle = styled(Handle)` diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx index ad383a527b8c..ae68427e7922 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx @@ -1,5 +1,9 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { CREATE_STEP_STEP_ID } from '@/workflow/constants/CreateStepStepId'; +import { EMPTY_TRIGGER_STEP_ID } from '@/workflow/constants/EmptyTriggerStepId'; import { useStartNodeCreation } from '@/workflow/hooks/useStartNodeCreation'; import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection'; import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; @@ -13,6 +17,8 @@ export const WorkflowDiagramCanvasEditableEffect = () => { const { startNodeCreation } = useStartNodeCreation(); const { openRightDrawer, closeRightDrawer } = useRightDrawer(); + const setHotkeyScope = useSetHotkeyScope(); + const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); const handleSelectionChange = useCallback( @@ -26,7 +32,14 @@ export const WorkflowDiagramCanvasEditableEffect = () => { return; } - const isCreateStepNode = selectedNode.type === 'create-step'; + const isEmptyTriggerNode = selectedNode.type === EMPTY_TRIGGER_STEP_ID; + if (isEmptyTriggerNode) { + openRightDrawer(RightDrawerPages.WorkflowStepSelectTriggerType); + + return; + } + + const isCreateStepNode = selectedNode.type === CREATE_STEP_STEP_ID; if (isCreateStepNode) { if (selectedNode.data.nodeType !== 'create-step') { throw new Error('Expected selected node to be a create step node.'); @@ -38,9 +51,11 @@ export const WorkflowDiagramCanvasEditableEffect = () => { } setWorkflowSelectedNode(selectedNode.id); + setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); openRightDrawer(RightDrawerPages.WorkflowStepEdit); }, [ + setHotkeyScope, closeRightDrawer, openRightDrawer, setWorkflowSelectedNode, diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonlyEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonlyEffect.tsx index 17cf9ae1df2e..e823ce2c6d53 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonlyEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonlyEffect.tsx @@ -1,5 +1,7 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection'; import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; import { WorkflowDiagramNode } from '@/workflow/types/WorkflowDiagram'; @@ -11,6 +13,7 @@ import { isDefined } from 'twenty-ui'; export const WorkflowDiagramCanvasReadonlyEffect = () => { const { openRightDrawer, closeRightDrawer } = useRightDrawer(); const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); + const setHotkeyScope = useSetHotkeyScope(); const handleSelectionChange = useCallback( ({ nodes }: OnSelectionChangeParams) => { @@ -24,9 +27,15 @@ export const WorkflowDiagramCanvasReadonlyEffect = () => { } setWorkflowSelectedNode(selectedNode.id); + setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); openRightDrawer(RightDrawerPages.WorkflowStepView); }, - [closeRightDrawer, openRightDrawer, setWorkflowSelectedNode], + [ + closeRightDrawer, + openRightDrawer, + setWorkflowSelectedNode, + setHotkeyScope, + ], ); useOnSelectionChange({ diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx index 2e1b1328a0b1..ec0614037889 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx @@ -1,7 +1,6 @@ -import { IconButton } from '@/ui/input/button/components/IconButton'; import styled from '@emotion/styled'; import { Handle, Position } from '@xyflow/react'; -import { IconPlus } from 'twenty-ui'; +import { IconButton, IconPlus } from 'twenty-ui'; export const StyledTargetHandle = styled(Handle)` visibility: hidden; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEffect.tsx index 8b714cc7613c..a95b709e160e 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEffect.tsx @@ -27,7 +27,9 @@ export const WorkflowDiagramEffect = ({ workflowDiagramState, ); - const nextWorkflowDiagram = getWorkflowVersionDiagram(currentVersion); + const nextWorkflowDiagram = addCreateStepNodes( + getWorkflowVersionDiagram(currentVersion), + ); let mergedWorkflowDiagram = nextWorkflowDiagram; if (isDefined(previousWorkflowDiagram)) { @@ -37,11 +39,7 @@ export const WorkflowDiagramEffect = ({ ); } - const workflowDiagramWithCreateStepNodes = addCreateStepNodes( - mergedWorkflowDiagram, - ); - - set(workflowDiagramState, workflowDiagramWithCreateStepNodes); + set(workflowDiagramState, mergedWorkflowDiagram); }; }, [], diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx index 0fc4d8591051..9cef1fbb47bd 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx @@ -3,7 +3,7 @@ import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCode, IconMail, IconPlaylistAdd } from 'twenty-ui'; +import { IconCode, IconHandMove, IconMail, IconPlaylistAdd } from 'twenty-ui'; const StyledStepNodeLabelIconContainer = styled.div` align-items: center; @@ -26,17 +26,30 @@ export const WorkflowDiagramStepNodeBase = ({ const renderStepIcon = () => { switch (data.nodeType) { case 'trigger': { - return ( - - - - ); - } - case 'condition': { - return null; + switch (data.triggerType) { + case 'DATABASE_EVENT': { + return ( + + + + ); + } + case 'MANUAL': { + return ( + + + + ); + } + } + + return assertUnreachable(data.triggerType); } case 'action': { switch (data.actionType) { diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx index cb8290fd732f..579e3c923491 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx @@ -1,4 +1,3 @@ -import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; import { WorkflowDiagramStepNodeBase } from '@/workflow/components/WorkflowDiagramStepNodeBase'; import { useDeleteOneStep } from '@/workflow/hooks/useDeleteOneStep'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; @@ -6,7 +5,7 @@ import { workflowIdState } from '@/workflow/states/workflowIdState'; import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined'; import { useRecoilValue } from 'recoil'; -import { IconTrash } from 'twenty-ui'; +import { FloatingIconButton, IconTrash } from 'twenty-ui'; export const WorkflowDiagramStepNodeEditable = ({ id, diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx index be2a495202ac..4dafbc3b4aa2 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx @@ -1,28 +1,19 @@ -import { TextArea } from '@/ui/input/components/TextArea'; -import { TextInput } from '@/ui/input/components/TextInput'; -import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditActionFormBase'; +import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope'; +import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth'; +import { Select, SelectOption } from '@/ui/input/components/Select'; +import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase'; +import { VariableTagInput } from '@/workflow/search-variables/components/VariableTagInput'; +import { workflowIdState } from '@/workflow/states/workflowIdState'; import { WorkflowSendEmailStep } from '@/workflow/types/Workflow'; import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { useRecoilValue } from 'recoil'; import { IconMail, IconPlus, isDefined } from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; -import { Select, SelectOption } from '@/ui/input/components/Select'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; -import { useRecoilValue } from 'recoil'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; -import { workflowIdState } from '@/workflow/states/workflowIdState'; -import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope'; - -const StyledTriggerSettings = styled.div` - padding: ${({ theme }) => theme.spacing(6)}; - display: flex; - flex-direction: column; - row-gap: ${({ theme }) => theme.spacing(4)}; -`; type WorkflowEditActionFormSendEmailProps = | { @@ -37,6 +28,7 @@ type WorkflowEditActionFormSendEmailProps = type SendEmailFormData = { connectedAccountId: string; + email: string; subject: string; body: string; }; @@ -46,13 +38,15 @@ export const WorkflowEditActionFormSendEmail = ( ) => { const theme = useTheme(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); + const { triggerApisOAuth } = useTriggerApisOAuth(); + const workflowId = useRecoilValue(workflowIdState); const redirectUrl = `/object/workflow/${workflowId}`; const form = useForm({ defaultValues: { connectedAccountId: '', + email: '', subject: '', body: '', }, @@ -73,7 +67,7 @@ export const WorkflowEditActionFormSendEmail = ( !isDefined(scopes) || !isDefined(scopes.find((scope) => scope === GMAIL_SEND_SCOPE)) ) { - await triggerGoogleApisOAuth({ + await triggerApisOAuth('google', { redirectLocation: redirectUrl, loginHint: connectedAccount.handle, }); @@ -83,10 +77,11 @@ export const WorkflowEditActionFormSendEmail = ( useEffect(() => { form.setValue( 'connectedAccountId', - props.action.settings.connectedAccountId ?? '', + props.action.settings.input.connectedAccountId ?? '', ); - form.setValue('subject', props.action.settings.subject ?? ''); - form.setValue('body', props.action.settings.body ?? ''); + form.setValue('email', props.action.settings.input.email ?? ''); + form.setValue('subject', props.action.settings.input.subject ?? ''); + form.setValue('body', props.action.settings.input.body ?? ''); }, [props.action.settings, form]); const saveAction = useDebouncedCallback( @@ -99,9 +94,12 @@ export const WorkflowEditActionFormSendEmail = ( ...props.action, settings: { ...props.action.settings, - connectedAccountId: formData.connectedAccountId, - subject: formData.subject, - body: formData.body, + input: { + connectedAccountId: formData.connectedAccountId, + email: formData.email, + subject: formData.subject, + body: formData.body, + }, }, }); @@ -134,12 +132,12 @@ export const WorkflowEditActionFormSendEmail = ( }; if ( - isDefined(props.action.settings.connectedAccountId) && - props.action.settings.connectedAccountId !== '' + isDefined(props.action.settings.input.connectedAccountId) && + props.action.settings.input.connectedAccountId !== '' ) { filter.or.push({ id: { - eq: props.action.settings.connectedAccountId, + eq: props.action.settings.input.connectedAccountId, }, }); } @@ -168,70 +166,85 @@ export const WorkflowEditActionFormSendEmail = ( return ( !loading && ( - } - actionTitle="Send Email" - actionType="Email" + } + headerTitle="Send Email" + headerType="Email" > - - ( -