diff --git a/.github/workflows/cd-deploy-main.yaml b/.github/workflows/cd-deploy-main.yaml
index 9b6217c5ab30..5959d0abd9c6 100644
--- a/.github/workflows/cd-deploy-main.yaml
+++ b/.github/workflows/cd-deploy-main.yaml
@@ -5,6 +5,7 @@ on:
- main
jobs:
deploy-main:
+ timeout-minutes: 3
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
diff --git a/.github/workflows/cd-deploy-tag.yaml b/.github/workflows/cd-deploy-tag.yaml
index 17bee3d1a34f..cb07fe44687e 100644
--- a/.github/workflows/cd-deploy-tag.yaml
+++ b/.github/workflows/cd-deploy-tag.yaml
@@ -5,6 +5,7 @@ on:
- 'v*'
jobs:
deploy-tag:
+ timeout-minutes: 3
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
diff --git a/.github/workflows/ci-chrome-extension.yaml b/.github/workflows/ci-chrome-extension.yaml
index a78be9415597..63c7094f7044 100644
--- a/.github/workflows/ci-chrome-extension.yaml
+++ b/.github/workflows/ci-chrome-extension.yaml
@@ -3,19 +3,16 @@ 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
jobs:
chrome-extension-build:
+ timeout-minutes: 15
runs-on: ubuntu-latest
env:
VITE_SERVER_BASE_URL: http://localhost:3000
@@ -26,7 +23,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-e2e.yml.bak b/.github/workflows/ci-e2e.yml.bak
new file mode 100644
index 000000000000..7bba72f1e77b
--- /dev/null
+++ b/.github/workflows/ci-e2e.yml.bak
@@ -0,0 +1,52 @@
+name: CI E2E Tests
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - '**'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ timeout-minutes: 30
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - 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/**
+ playwright.config.ts
+
+ - name: Skip if no relevant changes
+ if: steps.changed-files.outputs.any_changed == 'false'
+ run: echo "No relevant changes detected. Marking as valid."
+
+ - name: Install dependencies
+ if: steps.changed-files.outputs.any_changed == 'true'
+ uses: ./.github/workflows/actions/yarn-install
+ - 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()
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml
index 595081c0a973..910286ff1beb 100644
--- a/.github/workflows/ci-front.yaml
+++ b/.github/workflows/ci-front.yaml
@@ -3,21 +3,16 @@ 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
jobs:
front-sb-build:
+ timeout-minutes: 30
runs-on: ubuntu-latest
env:
REACT_APP_SERVER_BASE_URL: http://localhost:3000
@@ -29,48 +24,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
- timeout-minutes: 60
- needs: front-sb-build
- strategy:
- matrix:
- storybook_scope: [pages, modules]
- env:
- REACT_APP_SERVER_BASE_URL: http://localhost:3000
- NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
- steps:
- - name: Fetch local actions
- uses: actions/checkout@v4
- - name: Install dependencies
- uses: ./.github/workflows/actions/yarn-install
- - name: Install Playwright
- run: cd packages/twenty-front && npx playwright install
- - name: Front / Restore Storybook Task Cache
- uses: ./.github/workflows/actions/task-cache
- with:
- tag: scope:frontend
- tasks: storybook:build
- - name: Front / Write .env
- run: npx nx reset:env twenty-front
- - name: Run storybook tests
- run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }}
- front-sb-test-shipfox:
+ timeout-minutes: 30
runs-on: shipfox-8vcpu-ubuntu-2204
- timeout-minutes: 60
needs: front-sb-build
strategy:
matrix:
@@ -81,37 +71,72 @@ 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
- timeout-minutes: 60
+ timeout-minutes: 30
+ runs-on: shipfox-8vcpu-ubuntu-2204
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:
+ timeout-minutes: 30
if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push'
needs: front-sb-build
runs-on: ubuntu-latest
@@ -123,21 +148,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: |
+ 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:
+ timeout-minutes: 30
runs-on: ubuntu-latest
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
@@ -153,20 +195,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-release-create.yaml b/.github/workflows/ci-release-create.yaml
index 068d054cb30b..736c57352597 100644
--- a/.github/workflows/ci-release-create.yaml
+++ b/.github/workflows/ci-release-create.yaml
@@ -15,6 +15,7 @@ on:
jobs:
create_pr:
+ timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout
diff --git a/.github/workflows/ci-release-merge.yaml b/.github/workflows/ci-release-merge.yaml
index 96b719318672..5b0bce577501 100644
--- a/.github/workflows/ci-release-merge.yaml
+++ b/.github/workflows/ci-release-merge.yaml
@@ -6,6 +6,7 @@ on:
jobs:
tag_and_release:
+ timeout-minutes: 10
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
steps:
diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml
index 074d63fdda40..5d7a35244f01 100644
--- a/.github/workflows/ci-server.yaml
+++ b/.github/workflows/ci-server.yaml
@@ -3,21 +3,16 @@ 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
jobs:
server-setup:
+ timeout-minutes: 30
runs-on: ubuntu-latest
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
@@ -38,25 +33,42 @@ 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:
+ timeout-minutes: 30
runs-on: ubuntu-latest
needs: server-setup
env:
@@ -66,19 +78,33 @@ 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
tasks: test
server-integration-test:
+ timeout-minutes: 30
runs-on: ubuntu-latest
needs: server-setup
services:
@@ -100,17 +126,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..d7202857a7b4 100644
--- a/.github/workflows/ci-test-docker-compose.yaml
+++ b/.github/workflows/ci-test-docker-compose.yaml
@@ -1,20 +1,31 @@
name: 'Test Docker Compose'
on:
pull_request:
- paths:
- - 'packages/twenty-docker/**'
+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
+ timeout-minutes: 10
runs-on: ubuntu-latest
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 +42,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..cdeb97af15c7
--- /dev/null
+++ b/.github/workflows/ci-tinybird.yaml
@@ -0,0 +1,35 @@
+name: CI Tinybird
+on:
+ push:
+ branches:
+ - main
+
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ ci:
+ timeout-minutes: 10
+ 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-utils.yaml b/.github/workflows/ci-utils.yaml
index fccfca98d8ab..7a7ef38042dc 100644
--- a/.github/workflows/ci-utils.yaml
+++ b/.github/workflows/ci-utils.yaml
@@ -19,6 +19,7 @@ concurrency:
jobs:
danger-js:
+ timeout-minutes: 3
runs-on: ubuntu-latest
if: github.event.action != 'closed'
steps:
@@ -31,6 +32,7 @@ jobs:
DANGER_GITHUB_API_TOKEN: ${{ github.token }}
congratulate:
+ timeout-minutes: 3
runs-on: ubuntu-latest
if: github.event.action == 'closed' && github.event.pull_request.merged == true
steps:
diff --git a/.github/workflows/ci-website.yaml b/.github/workflows/ci-website.yaml
index d79345f3bf53..a7300430288f 100644
--- a/.github/workflows/ci-website.yaml
+++ b/.github/workflows/ci-website.yaml
@@ -1,21 +1,20 @@
name: CI Website
+timeout-minutes: 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
jobs:
website-build:
+ timeout-minutes: 3
runs-on: ubuntu-latest
services:
postgres:
@@ -27,13 +26,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
deleted file mode 100644
index cffb50287629..000000000000
--- a/.github/workflows/playwright.yml.bak
+++ /dev/null
@@ -1,27 +0,0 @@
-name: Playwright Tests
-on:
- push:
- branches: [ main, master ]
- pull_request:
- branches: [ main, master ]
-jobs:
- test:
- timeout-minutes: 60
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
- with:
- node-version: lts/*
- - name: Install dependencies
- run: npm install -g yarn && yarn
- - name: Install Playwright Browsers
- run: yarn playwright install --with-deps
- - name: Run Playwright tests
- run: yarn test:e2e companies
- - uses: actions/upload-artifact@v4
- if: always()
- with:
- name: playwright-report
- path: playwright-report/
- retention-days: 30
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/README.md b/README.md
index 86ed0fe51a6f..d75db65e1504 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,3 @@
-
-
-
-
-
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 9f1f55ae767e..000000000000
--- a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md
+++ /dev/null
@@ -1,31 +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)
-
-» 14-October-2024 by [AliYar-Khan](https://oss.gg/AliYar-Khan) poster Link: [poster](https://x.com/Mr_Programmer14/status/1845888855183884352)
-
-» 16-October-2024 by [Harsh BHat](https://oss.gg/harshsbhat) poster Link: [poster](https://x.com/HarshBhatX/status/1846233330435477531)
-
-» 17-October-2024 by [Atharva Deshmukh](https://oss.gg/Atharva-3000) poster Link: [poster](https://x.com/0x_atharva/status/1846915861191577697)
-
----
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 d67c49b64154..000000000000
--- a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md
+++ /dev/null
@@ -1,32 +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)
-
-» 16-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) Logo Link: [logo](https://drive.google.com/file/d/1jmqwNvlSyWSY1-pCG63TAtDvCoVa8xg-/view?usp=sharing) » tweet Link: [tweet](https://x.com/HarshBhatX/status/1846234658712772977)
-
-» 17-October-2024 by [shlok-py](https://oss.gg/shlok-py) Logo Link: [logo](https://drive.google.com/file/d/1BakHRLJul6DcNbLyeOXgJO9Ap4DpUxO9/view?usp=sharing) » tweet Link: [tweet](https://x.com/koirala_shlok/status/1846910669658247201)
-
-
----
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 a0e1421bfd99..000000000000
--- a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md
+++ /dev/null
@@ -1,48 +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
-
-» 16-October-2024 by Harsh Bhat
-» Link to Tweet: https://x.com/HarshBhatX/status/1846252536241508392
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 07d2e067c708..000000000000
--- a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md
+++ /dev/null
@@ -1,31 +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
-
-» 16-October-2024 by Harsh Bhat
-» Link to Tweet: https://x.com/HarshBhatX/status/1846075312691413066
----
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 041c73ef081e..000000000000
--- a/oss-gg/twenty-side-quest/4-meme-magic.md
+++ /dev/null
@@ -1,37 +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)
-
-» 16-October-2024 by Harsh Bhat
-» Link to Tweet: https://x.com/HarshBhatX/status/1844698253104709899
----
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 320ffa9015db..000000000000
--- a/oss-gg/twenty-side-quest/5-gif-magic.md
+++ /dev/null
@@ -1,37 +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
-
-» 15-October-2024 by Ali Yar Khan
-» Link to gif: https://giphy.com/gifs/Q3f7T107wSsMJlT7aj
-
-» 16-October-2024 by Harsh Bhat
-» Link to gif: https://giphy.com/gifs/oss-twentycrm-mgoYSDrjIalUL7XJzm
----
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 c1a7a9d3bae8..b10e09876e52 100644
--- a/packages/twenty-docker/.env.example
+++ b/packages/twenty-docker/.env.example
@@ -8,10 +8,7 @@ REDIS_URL=redis://redis:6379
SERVER_URL=http://localhost:3000
# 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 8800f4f3f3b9..41d80dabc398 100644
--- a/packages/twenty-docker/docker-compose.yml
+++ b/packages/twenty-docker/docker-compose.yml
@@ -35,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
@@ -67,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 99e5c60132ed..857729788675 100644
--- a/packages/twenty-docker/k8s/manifests/deployment-server.yaml
+++ b/packages/twenty-docker/k8s/manifests/deployment-server.yaml
@@ -55,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 92d0322e5930..eb1938ba6dda 100644
--- a/packages/twenty-docker/k8s/manifests/deployment-worker.yaml
+++ b/packages/twenty-docker/k8s/manifests/deployment-worker.yaml
@@ -42,26 +42,11 @@ spec:
value: "redis"
- name: "REDIS_URL"
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
- - 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
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 0f643f5c6d80..5276d574319e 100644
--- a/packages/twenty-docker/k8s/terraform/deployment-server.tf
+++ b/packages/twenty-docker/k8s/terraform/deployment-server.tf
@@ -91,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"
@@ -100,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 163f02c4977e..aa68fd3af2da 100644
--- a/packages/twenty-docker/k8s/terraform/deployment-worker.tf
+++ b/packages/twenty-docker/k8s/terraform/deployment-worker.tf
@@ -78,7 +78,7 @@ resource "kubernetes_deployment" "twentycrm_worker" {
}
env {
- name = "ACCESS_TOKEN_SECRET"
+ name = "APP_SECRET"
value_from {
secret_key_ref {
name = "tokens"
@@ -87,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-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 `First name` instead of `First name`
+ this.lastNameInput = page.locator("//input[@placeholder='Last name']"); // may fail if placeholder is `Last 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..5d8e10a5ec05 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 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 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 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 6947f8aa34a4..b26b39ddc122 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -38,6 +38,16 @@ export type Analytics = {
success: Scalars['Boolean']['output'];
};
+export type AnalyticsTinybirdJwtMap = {
+ __typename?: 'AnalyticsTinybirdJwtMap';
+ getPageviewsAnalytics: Scalars['String']['output'];
+ getServerlessFunctionDuration: Scalars['String']['output'];
+ getServerlessFunctionErrorCount: Scalars['String']['output'];
+ getServerlessFunctionSuccessRate: Scalars['String']['output'];
+ getUsersAnalytics: Scalars['String']['output'];
+ getWebhookAnalytics: Scalars['String']['output'];
+};
+
export type ApiConfig = {
__typename?: 'ApiConfig';
mutationMaximumAffectedRecords: Scalars['Float']['output'];
@@ -71,6 +81,7 @@ export type AuthProviders = {
magicLink: Scalars['Boolean']['output'];
microsoft: Scalars['Boolean']['output'];
password: Scalars['Boolean']['output'];
+ sso: Scalars['Boolean']['output'];
};
export type AuthToken = {
@@ -148,6 +159,7 @@ export enum CaptchaDriverType {
export type ClientConfig = {
__typename?: 'ClientConfig';
+ analyticsEnabled: Scalars['Boolean']['output'];
api: ApiConfig;
authProviders: AuthProviders;
billing: Billing;
@@ -160,6 +172,11 @@ export type ClientConfig = {
support: Support;
};
+export type ComputeStepOutputSchemaInput = {
+ /** Step JSON format */
+ step: Scalars['JSON']['input'];
+};
+
export type CreateAppTokenInput = {
expiresAt: Scalars['DateTime']['input'];
};
@@ -186,6 +203,7 @@ export type CreateObjectInput = {
description?: InputMaybe;
icon?: InputMaybe;
imageIdentifierFieldMetadataId?: InputMaybe;
+ isLabelSyncedWithName?: InputMaybe;
isRemote?: InputMaybe;
labelIdentifierFieldMetadataId?: InputMaybe;
labelPlural: Scalars['String']['input'];
@@ -194,6 +212,7 @@ export type CreateObjectInput = {
nameSingular: Scalars['String']['input'];
primaryKeyColumnType?: InputMaybe;
primaryKeyFieldMetadataSettings?: InputMaybe;
+ shortcut?: InputMaybe;
};
export type CreateOneAppTokenInput = {
@@ -270,9 +289,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 */
@@ -283,6 +306,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 */
@@ -372,6 +409,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'];
@@ -385,6 +436,33 @@ export type FullName = {
lastName: 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'];
@@ -392,6 +470,11 @@ export type GetServerlessFunctionSourceCodeInput = {
version?: Scalars['String']['input'];
};
+export enum IdpType {
+ Oidc = 'OIDC',
+ Saml = 'SAML'
+}
+
export type IndexConnection = {
__typename?: 'IndexConnection';
/** Array of edges. */
@@ -461,12 +544,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;
@@ -474,16 +560,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;
@@ -551,6 +641,16 @@ export type MutationCheckoutSessionArgs = {
};
+export type MutationComputeStepOutputSchemaArgs = {
+ input: ComputeStepOutputSchemaInput;
+};
+
+
+export type MutationCreateOidcIdentityProviderArgs = {
+ input: SetupOidcSsoInput;
+};
+
+
export type MutationCreateOneAppTokenArgs = {
input: CreateOneAppTokenInput;
};
@@ -581,6 +681,11 @@ export type MutationCreateOneServerlessFunctionArgs = {
};
+export type MutationCreateSamlIdentityProviderArgs = {
+ input: SetupSamlSsoInput;
+};
+
+
export type MutationDeactivateWorkflowVersionArgs = {
workflowVersionId: Scalars['String']['input'];
};
@@ -607,7 +712,12 @@ export type MutationDeleteOneRemoteServerArgs = {
export type MutationDeleteOneServerlessFunctionArgs = {
- input: DeleteServerlessFunctionInput;
+ input: ServerlessFunctionIdInput;
+};
+
+
+export type MutationDeleteSsoIdentityProviderArgs = {
+ input: DeleteSsoInput;
};
@@ -616,6 +726,11 @@ export type MutationDeleteWorkspaceInvitationArgs = {
};
+export type MutationEditSsoIdentityProviderArgs = {
+ input: EditSsoInput;
+};
+
+
export type MutationEmailPasswordResetLinkArgs = {
email: Scalars['String']['input'];
};
@@ -633,6 +748,11 @@ export type MutationExecuteOneServerlessFunctionArgs = {
};
+export type MutationFindAvailableSsoIdentityProvidersArgs = {
+ input: FindAvailableSsoidpInput;
+};
+
+
export type MutationGenerateApiKeyTokenArgs = {
apiKeyId: Scalars['String']['input'];
expiresAt: Scalars['String']['input'];
@@ -644,6 +764,11 @@ export type MutationGenerateJwtArgs = {
};
+export type MutationGetAuthorizationUrlArgs = {
+ input: GetAuthorizationUrlInput;
+};
+
+
export type MutationImpersonateArgs = {
userId: Scalars['String']['input'];
};
@@ -852,7 +977,9 @@ export type Query = {
fields: FieldConnection;
findDistantTablesWithStatus: Array;
findManyRemoteServersByType: Array;
+ findManyServerlessFunctions: Array;
findOneRemoteServerById: RemoteServer;
+ findOneServerlessFunction: ServerlessFunction;
findWorkspaceFromInviteHash: Workspace;
findWorkspaceInvitations: Array;
getAvailablePackages: Scalars['JSON']['output'];
@@ -865,12 +992,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;
};
@@ -917,6 +1043,11 @@ export type QueryFindOneRemoteServerByIdArgs = {
};
+export type QueryFindOneServerlessFunctionArgs = {
+ input: ServerlessFunctionIdInput;
+};
+
+
export type QueryFindWorkspaceFromInviteHashArgs = {
inviteHash: Scalars['String']['input'];
};
@@ -992,18 +1123,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'];
};
@@ -1091,6 +1210,12 @@ export type RunWorkflowVersionInput = {
workflowVersionId: Scalars['String']['input'];
};
+export enum SsoIdentityProviderStatus {
+ Active = 'Active',
+ Error = 'Error',
+ Inactive = 'Inactive'
+}
+
export type SendInvitationsOutput = {
__typename?: 'SendInvitationsOutput';
errors: Array;
@@ -1112,28 +1237,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 */
@@ -1152,22 +1263,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',
@@ -1179,6 +1279,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',
@@ -1327,11 +1452,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 = {
@@ -1368,11 +1495,13 @@ export type UpdateWorkspaceInput = {
displayName?: InputMaybe;
domainName?: InputMaybe;
inviteHash?: InputMaybe;
+ isPublicInviteLinkEnabled?: InputMaybe;
logo?: InputMaybe;
};
export type User = {
__typename?: 'User';
+ analyticsTinybirdJwts?: Maybe;
canImpersonate: Scalars['Boolean']['output'];
createdAt: Scalars['DateTime']['output'];
defaultAvatarUrl?: Maybe;
@@ -1465,8 +1594,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'];
@@ -1539,6 +1670,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'];
@@ -1657,6 +1794,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;
@@ -1664,6 +1802,7 @@ export type Object = {
labelSingular: Scalars['String']['output'];
namePlural: Scalars['String']['output'];
nameSingular: Scalars['String']['output'];
+ shortcut?: Maybe;
updatedAt: Scalars['DateTime']['output'];
};
@@ -1850,23 +1989,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, latestVersionInputSchema?: any | null, publishedVersions: Array, createdAt: any, updatedAt: any };
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, latestVersionInputSchema?: any | null, publishedVersions: Array, createdAt: any, updatedAt: any } };
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, latestVersionInputSchema?: any | null, publishedVersions: Array, createdAt: any, updatedAt: any } };
export type ExecuteOneServerlessFunctionMutationVariables = Exact<{
input: ExecuteServerlessFunctionInput;
@@ -1880,14 +2019,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, latestVersionInputSchema?: any | null, publishedVersions: Array, createdAt: any, updatedAt: any } };
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, latestVersionInputSchema?: any | null, publishedVersions: Array, createdAt: any, updatedAt: any } };
export type FindManyAvailablePackagesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -1897,14 +2036,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, latestVersionInputSchema?: any | null, publishedVersions: Array, createdAt: any, updatedAt: any }> };
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, latestVersionInputSchema?: any | null, publishedVersions: Array, createdAt: any, updatedAt: any } };
export type FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{
input: GetServerlessFunctionSourceCodeInput;
@@ -1915,7 +2054,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"}},{"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;
@@ -1933,13 +2072,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"}},{"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"}},{"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"}},{"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"}},{"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"}},{"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"}},{"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 7f053fc6b10f..de35163609bc 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -31,6 +31,16 @@ export type Analytics = {
success: Scalars['Boolean'];
};
+export type AnalyticsTinybirdJwtMap = {
+ __typename?: 'AnalyticsTinybirdJwtMap';
+ getPageviewsAnalytics: Scalars['String'];
+ getServerlessFunctionDuration: Scalars['String'];
+ getServerlessFunctionErrorCount: Scalars['String'];
+ getServerlessFunctionSuccessRate: Scalars['String'];
+ getUsersAnalytics: Scalars['String'];
+ getWebhookAnalytics: Scalars['String'];
+};
+
export type ApiConfig = {
__typename?: 'ApiConfig';
mutationMaximumAffectedRecords: Scalars['Float'];
@@ -64,6 +74,7 @@ export type AuthProviders = {
magicLink: Scalars['Boolean'];
microsoft: Scalars['Boolean'];
password: Scalars['Boolean'];
+ sso: Scalars['Boolean'];
};
export type AuthToken = {
@@ -154,6 +165,11 @@ export type ClientConfig = {
support: Support;
};
+export type ComputeStepOutputSchemaInput = {
+ /** Step JSON format */
+ step: Scalars['JSON'];
+};
+
export type CreateServerlessFunctionInput = {
description?: InputMaybe;
name: Scalars['String'];
@@ -175,9 +191,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 */
@@ -188,6 +208,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 */
@@ -277,12 +311,53 @@ 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 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'];
@@ -290,6 +365,11 @@ export type GetServerlessFunctionSourceCodeInput = {
version?: Scalars['String'];
};
+export enum IdpType {
+ Oidc = 'OIDC',
+ Saml = 'SAML'
+}
+
export type IndexConnection = {
__typename?: 'IndexConnection';
/** Array of edges. */
@@ -359,23 +439,30 @@ export type Mutation = {
authorizeApp: AuthorizeApp;
challenge: LoginToken;
checkoutSession: SessionEntity;
+ computeStepOutputSchema: Scalars['JSON'];
+ createOIDCIdentityProvider: SetupSsoOutput;
createOneAppToken: AppToken;
createOneObject: Object;
createOneServerlessFunction: 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;
@@ -438,11 +525,26 @@ export type MutationCheckoutSessionArgs = {
};
+export type MutationComputeStepOutputSchemaArgs = {
+ input: ComputeStepOutputSchemaInput;
+};
+
+
+export type MutationCreateOidcIdentityProviderArgs = {
+ input: SetupOidcSsoInput;
+};
+
+
export type MutationCreateOneServerlessFunctionArgs = {
input: CreateServerlessFunctionInput;
};
+export type MutationCreateSamlIdentityProviderArgs = {
+ input: SetupSamlSsoInput;
+};
+
+
export type MutationDeactivateWorkflowVersionArgs = {
workflowVersionId: Scalars['String'];
};
@@ -454,7 +556,12 @@ export type MutationDeleteOneObjectArgs = {
export type MutationDeleteOneServerlessFunctionArgs = {
- input: DeleteServerlessFunctionInput;
+ input: ServerlessFunctionIdInput;
+};
+
+
+export type MutationDeleteSsoIdentityProviderArgs = {
+ input: DeleteSsoInput;
};
@@ -463,6 +570,11 @@ export type MutationDeleteWorkspaceInvitationArgs = {
};
+export type MutationEditSsoIdentityProviderArgs = {
+ input: EditSsoInput;
+};
+
+
export type MutationEmailPasswordResetLinkArgs = {
email: Scalars['String'];
};
@@ -480,6 +592,11 @@ export type MutationExecuteOneServerlessFunctionArgs = {
};
+export type MutationFindAvailableSsoIdentityProvidersArgs = {
+ input: FindAvailableSsoidpInput;
+};
+
+
export type MutationGenerateApiKeyTokenArgs = {
apiKeyId: Scalars['String'];
expiresAt: Scalars['String'];
@@ -491,6 +608,11 @@ export type MutationGenerateJwtArgs = {
};
+export type MutationGetAuthorizationUrlArgs = {
+ input: GetAuthorizationUrlInput;
+};
+
+
export type MutationImpersonateArgs = {
userId: Scalars['String'];
};
@@ -670,6 +792,8 @@ export type Query = {
clientConfig: ClientConfig;
currentUser: User;
currentWorkspace: Workspace;
+ findManyServerlessFunctions: Array;
+ findOneServerlessFunction: ServerlessFunction;
findWorkspaceFromInviteHash: Workspace;
findWorkspaceInvitations: Array;
getAvailablePackages: Scalars['JSON'];
@@ -682,10 +806,9 @@ export type Query = {
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
index: Index;
indexMetadatas: IndexConnection;
+ listSSOIdentityProvidersByWorkspaceId: Array;
object: Object;
objects: ObjectConnection;
- serverlessFunction: ServerlessFunction;
- serverlessFunctions: ServerlessFunctionConnection;
validatePasswordResetToken: ValidatePasswordResetToken;
};
@@ -706,6 +829,11 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = {
};
+export type QueryFindOneServerlessFunctionArgs = {
+ input: ServerlessFunctionIdInput;
+};
+
+
export type QueryFindWorkspaceFromInviteHashArgs = {
inviteHash: Scalars['String'];
};
@@ -822,6 +950,12 @@ export type RunWorkflowVersionInput = {
workflowVersionId: Scalars['String'];
};
+export enum SsoIdentityProviderStatus {
+ Active = 'Active',
+ Error = 'Error',
+ Inactive = 'Inactive'
+}
+
export type SendInvitationsOutput = {
__typename?: 'SendInvitationsOutput';
errors: Array;
@@ -843,28 +977,14 @@ export type ServerlessFunction = {
description?: Maybe;
id: Scalars['UUID'];
latestVersion?: Maybe;
+ latestVersionInputSchema?: Maybe;
name: Scalars['String'];
+ publishedVersions: Array;
runtime: 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 */
@@ -883,6 +1003,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',
@@ -894,6 +1019,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',
@@ -1027,11 +1177,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 = {
@@ -1053,11 +1205,13 @@ export type UpdateWorkspaceInput = {
displayName?: InputMaybe;
domainName?: InputMaybe;
inviteHash?: InputMaybe;
+ isPublicInviteLinkEnabled?: InputMaybe;
logo?: InputMaybe;
};
export type User = {
__typename?: 'User';
+ analyticsTinybirdJwts?: Maybe;
canImpersonate: Scalars['Boolean'];
createdAt: Scalars['DateTime'];
defaultAvatarUrl?: Maybe;
@@ -1140,8 +1294,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'];
@@ -1214,6 +1370,12 @@ export enum WorkspaceMemberTimeFormatEnum {
System = 'SYSTEM'
}
+export type WorkspaceNameAndId = {
+ __typename?: 'WorkspaceNameAndId';
+ displayName?: Maybe;
+ id: Scalars['String'];
+};
+
export type Field = {
__typename?: 'field';
createdAt: Scalars['DateTime'];
@@ -1332,6 +1494,7 @@ export type Object = {
indexMetadatas: ObjectIndexMetadatasConnection;
isActive: Scalars['Boolean'];
isCustom: Scalars['Boolean'];
+ isLabelSyncedWithName: Scalars['Boolean'];
isRemote: Scalars['Boolean'];
isSystem: Scalars['Boolean'];
labelIdentifierFieldMetadataId?: Maybe;
@@ -1339,6 +1502,7 @@ export type Object = {
labelSingular: Scalars['String'];
namePlural: Scalars['String'];
nameSingular: Scalars['String'];
+ shortcut?: Maybe;
updatedAt: Scalars['DateTime'];
};
@@ -1470,6 +1634,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'];
@@ -1495,6 +1661,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'];
@@ -1508,19 +1681,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, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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'];
@@ -1553,7 +1733,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, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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'];
@@ -1600,14 +1780,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, analyticsEnabled: 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 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 CreateOidcIdentityProviderMutationVariables = Exact<{
+ input: SetupOidcSsoInput;
+}>;
+
+
+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 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, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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; }>;
@@ -1624,7 +1837,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, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, 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'];
@@ -1633,6 +1846,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'];
}>;
@@ -1640,6 +1860,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'];
}>;
@@ -1802,6 +2029,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
@@ -1825,6 +2064,14 @@ export const UserQueryFragmentFragmentDoc = gql`
email
canImpersonate
supportUserHash
+ analyticsTinybirdJwts {
+ getWebhookAnalytics
+ getPageviewsAnalytics
+ getUsersAnalytics
+ getServerlessFunctionDuration
+ getServerlessFunctionSuccessRate
+ getServerlessFunctionErrorCount
+ }
onboardingStatus
workspaceMember {
...WorkspaceMemberQueryFragment
@@ -1840,6 +2087,8 @@ export const UserQueryFragmentFragmentDoc = gql`
inviteHash
allowImpersonation
activationStatus
+ isPublicInviteLinkEnabled
+ hasValidEntrepriseKey
featureFlags {
id
key
@@ -2236,6 +2485,39 @@ export function useEmailPasswordResetLinkMutation(baseOptions?: Apollo.MutationH
export type EmailPasswordResetLinkMutationHookResult = ReturnType;
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) {
@@ -2273,12 +2555,26 @@ export type GenerateApiKeyTokenMutationOptions = Apollo.BaseMutationOptions;
/**
@@ -2339,6 +2635,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) {
@@ -2757,6 +3088,7 @@ export const GetClientConfigDocument = gql`
google
password
microsoft
+ sso
}
billing {
isBillingEnabled
@@ -2846,6 +3178,188 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta
export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType;
export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult;
export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions;
+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
+ }
+}
+ `;
+
+/**
+ * __useListSsoIdentityProvidersByWorkspaceIdQuery__
+ *
+ * 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 } = useListSsoIdentityProvidersByWorkspaceIdQuery({
+ * variables: {
+ * },
+ * });
+ */
+export function useListSsoIdentityProvidersByWorkspaceIdQuery(baseOptions?: Apollo.QueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(ListSsoIdentityProvidersByWorkspaceIdDocument, options);
+ }
+export function useListSsoIdentityProvidersByWorkspaceIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(ListSsoIdentityProvidersByWorkspaceIdDocument, options);
+ }
+export type ListSsoIdentityProvidersByWorkspaceIdQueryHookResult = ReturnType;
+export type ListSsoIdentityProvidersByWorkspaceIdLazyQueryHookResult = ReturnType;
+export type ListSsoIdentityProvidersByWorkspaceIdQueryResult = Apollo.QueryResult;
export const DeleteUserAccountDocument = gql`
mutation DeleteUserAccount {
deleteUser {
@@ -2974,6 +3488,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)
@@ -3005,6 +3550,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/hooks/useUserOrMetadataLoading.ts b/packages/twenty-front/src/loading/hooks/useUserOrMetadataLoading.ts
deleted file mode 100644
index b35cdb3dd02d..000000000000
--- a/packages/twenty-front/src/loading/hooks/useUserOrMetadataLoading.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { useRecoilValue } from 'recoil';
-
-import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
-import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
-
-export const useUserOrMetadataLoading = () => {
- 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..515852428a20 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,115 @@
+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 { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
+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 contextStoreCurrentObjectMetadataId = useRecoilValue(
- contextStoreCurrentObjectMetadataIdState,
+ const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
+ contextStoreTargetedRecordsRuleComponentState,
);
- const { objectMetadataItem } = useObjectMetadataItemById({
- objectId: contextStoreCurrentObjectMetadataId,
- });
+ const contextStoreFilters = useRecoilComponentValueV2(
+ contextStoreFiltersComponentState,
+ );
- const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
- useState(false);
+ const graphqlFilter = computeContextStoreFilters(
+ contextStoreTargetedRecordsRule,
+ contextStoreFilters,
+ objectMetadataItem,
+ );
- const { deleteTableData } = useDeleteTableData({
- objectNameSingular: objectMetadataItem?.nameSingular ?? '',
- recordIndexId: objectMetadataItem?.namePlural ?? '',
+ const { fetchAllRecordIds } = useFetchAllRecordIds({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ filter: graphqlFilter,
});
- const handleDeleteClick = useCallback(() => {
- deleteTableData(contextStoreTargetedRecordIds);
- }, [deleteTableData, contextStoreTargetedRecordIds]);
+ const { closeRightDrawer } = useRightDrawer();
+
+ const handleDeleteClick = useCallback(async () => {
+ const recordIdsToDelete = await fetchAllRecordIds();
- const isRemoteObject = objectMetadataItem?.isRemote ?? false;
+ resetTableRowSelection();
- const numberOfSelectedRecords = contextStoreTargetedRecordIds.length;
+ for (const recordIdToDelete of recordIdsToDelete) {
+ const foundFavorite = favorites?.find(
+ (favorite) => favorite.recordId === recordIdToDelete,
+ );
+
+ 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 +117,25 @@ export const DeleteRecordsActionEffect = ({
handleDeleteClick()}
+ onConfirmClick={() => {
+ handleDeleteClick();
+ onActionExecutedCallback?.();
+ if (isInRightDrawer) {
+ closeRightDrawer();
+ }
+ }}
deleteButtonText={`Delete ${
- numberOfSelectedRecords > 1 ? 'Records' : 'Record'
+ contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record'
}`}
/>
),
@@ -80,14 +143,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..7d64ec72c0de 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,67 @@
-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 noSelectionRecordActionEffects = [ExportRecordsActionEffect];
+
+const singleRecordActionEffects = [
+ ManageFavoritesActionEffect,
+ DeleteRecordsActionEffect,
+];
+
+const multipleRecordActionEffects = [
+ ExportRecordsActionEffect,
+ 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 === 0
+ ? noSelectionRecordActionEffects
+ : contextStoreNumberOfSelectedRecords === 1
+ ? singleRecordActionEffects
+ : multipleRecordActionEffects;
+
+ return (
+ <>
+ {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 4b61fa58eadb..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 = [
- ManageFavoritesActionEffect,
- ExportRecordsActionEffect,
- DeleteRecordsActionEffect,
- ];
- 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/ActionMenuBarEntry.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx
similarity index 52%
rename from packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx
rename to packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx
index 02802ec4a616..ffa52d20590b 100644
--- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx
@@ -2,31 +2,24 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
-import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
-type ActionMenuBarEntryProps = {
+type RecordIndexActionMenuBarEntryProps = {
entry: ActionMenuEntry;
};
-const StyledButton = styled.div<{ accent: MenuItemAccent }>`
+const StyledButton = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm};
- color: ${(props) =>
- props.accent === 'danger'
- ? props.theme.color.red
- : props.theme.font.color.secondary};
+ color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
justify-content: center;
padding: ${({ theme }) => theme.spacing(2)};
- transition: background 0.1s ease;
+ transition: background ${({ theme }) => theme.animation.duration.fast} ease;
user-select: none;
&:hover {
- background: ${({ theme, accent }) =>
- accent === 'danger'
- ? theme.background.danger
- : theme.background.tertiary};
+ background: ${({ theme }) => theme.background.tertiary};
}
`;
@@ -35,13 +28,12 @@ const StyledButtonLabel = styled.div`
margin-left: ${({ theme }) => theme.spacing(1)};
`;
-export const ActionMenuBarEntry = ({ entry }: ActionMenuBarEntryProps) => {
+export const RecordIndexActionMenuBarEntry = ({
+ entry,
+}: RecordIndexActionMenuBarEntryProps) => {
const theme = useTheme();
return (
- entry.onClick?.()}
- >
+ 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/RecordShowRightDrawerActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx
new file mode 100644
index 000000000000..ba964f27a8d8
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx
@@ -0,0 +1,32 @@
+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 { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown';
+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 RecordShowRightDrawerActionMenu = () => {
+ const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
+ contextStoreCurrentObjectMetadataIdComponentState,
+ );
+
+ return (
+ <>
+ {contextStoreCurrentObjectMetadataId && (
+ {},
+ }}
+ >
+
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx
new file mode 100644
index 000000000000..ed3c1ee2277f
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/RightDrawerActionMenuDropdown.tsx
@@ -0,0 +1,87 @@
+import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
+import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
+import { RightDrawerActionMenuDropdownHotkeyScope } from '@/action-menu/types/RightDrawerActionMenuDropdownHotkeyScope';
+import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
+import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
+import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { useTheme } from '@emotion/react';
+import { Key } from 'ts-key-enum';
+import { Button, MenuItem } from 'twenty-ui';
+
+export const RightDrawerActionMenuDropdown = () => {
+ const actionMenuEntries = useRecoilComponentValueV2(
+ actionMenuEntriesComponentSelector,
+ );
+
+ const actionMenuId = useAvailableComponentInstanceIdOrThrow(
+ ActionMenuComponentInstanceContext,
+ );
+
+ const { closeDropdown, openDropdown } = useDropdownV2();
+
+ const theme = useTheme();
+
+ useScopedHotkeys(
+ [Key.Escape, 'ctrl+o,meta+o'],
+ () => {
+ closeDropdown(
+ getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId),
+ );
+ },
+ RightDrawerActionMenuDropdownHotkeyScope.RightDrawerActionMenuDropdown,
+ [closeDropdown],
+ );
+
+ useScopedHotkeys(
+ ['ctrl+o,meta+o'],
+ () => {
+ openDropdown(
+ getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId),
+ );
+ },
+ RightDrawerHotkeyScope.RightDrawer,
+ [openDropdown],
+ );
+
+ return (
+ }
+ dropdownPlacement="top-end"
+ dropdownOffset={{
+ y: parseInt(theme.spacing(2)),
+ }}
+ dropdownComponents={
+
+ {actionMenuEntries.map((item, index) => (
+
+ }
+ />
+ );
+};
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..4a86046bd508
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx
@@ -0,0 +1,140 @@
+import { expect, jest } from '@storybook/jest';
+import { Meta, StoryObj } from '@storybook/react';
+import { RecoilRoot } from 'recoil';
+
+import { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown';
+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/RightDrawerActionMenuDropdown',
+ component: RightDrawerActionMenuDropdown,
+ 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);
+
+ let actionButton = await canvas.findByText('Actions');
+ await userEvent.click(actionButton);
+
+ const deleteButton = await canvas.findByText('Delete');
+ await userEvent.click(deleteButton);
+
+ actionButton = await canvas.findByText('Actions');
+ await userEvent.click(actionButton);
+
+ const addToFavoritesButton = await canvas.findByText('Add to favorites');
+ await userEvent.click(addToFavoritesButton);
+
+ actionButton = await canvas.findByText('Actions');
+ await userEvent.click(actionButton);
+
+ 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/types/RightDrawerActionMenuDropdownHotkeyScope.ts b/packages/twenty-front/src/modules/action-menu/types/RightDrawerActionMenuDropdownHotkeyScope.ts
new file mode 100644
index 000000000000..74505c320f16
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/types/RightDrawerActionMenuDropdownHotkeyScope.ts
@@ -0,0 +1,3 @@
+export enum RightDrawerActionMenuDropdownHotkeyScope {
+ RightDrawerActionMenuDropdown = 'right-drawer-action-menu-dropdown',
+}
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/__tests__/getRightDrawerActionMenuDropdownIdFromActionMenuId.test.ts b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getRightDrawerActionMenuDropdownIdFromActionMenuId.test.ts
new file mode 100644
index 000000000000..209bdf99afb6
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/utils/__tests__/getRightDrawerActionMenuDropdownIdFromActionMenuId.test.ts
@@ -0,0 +1,9 @@
+import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '../getRightDrawerActionMenuDropdownIdFromActionMenuId';
+
+describe('getRightDrawerActionMenuDropdownIdFromActionMenuId', () => {
+ it('should return the right drawer action menu dropdown id', () => {
+ expect(
+ getRightDrawerActionMenuDropdownIdFromActionMenuId('action-menu-id'),
+ ).toBe('right-drawer-action-menu-dropdown-action-menu-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/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId.ts b/packages/twenty-front/src/modules/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId.ts
new file mode 100644
index 000000000000..8e1d49133d50
--- /dev/null
+++ b/packages/twenty-front/src/modules/action-menu/utils/getRightDrawerActionMenuDropdownIdFromActionMenuId.ts
@@ -0,0 +1,5 @@
+export const getRightDrawerActionMenuDropdownIdFromActionMenuId = (
+ actionMenuId: string,
+) => {
+ return `right-drawer-action-menu-dropdown-${actionMenuId}`;
+};
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..bff2bef3cb1b 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,
() => {
@@ -411,6 +350,10 @@ export const RichTextEditor = ({
editor.focus();
},
RightDrawerHotkeyScope.RightDrawer,
+ [],
+ {
+ preventDefault: false,
+ },
);
const handleBlockEditorFocus = () => {
@@ -427,7 +370,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 83%
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..a19f0af6a7e0 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';
@@ -26,8 +26,6 @@ const StyledTimelineContainer = styled.div`
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: flex-start;
-
- width: calc(100% - ${({ theme }) => theme.spacing(8)});
`;
export const EventList = ({ events, targetableObject }: EventListProps) => {
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 87%
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..e78e4a136d0f 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,15 +2,16 @@ 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 { MOBILE_VIEWPORT } from 'twenty-ui';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@@ -62,6 +63,7 @@ const StyledSummary = styled.summary`
flex: 1;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
+ width: 100%;
`;
const StyledItemContainer = styled.div<{ isMarginBottom?: boolean }>`
@@ -77,6 +79,9 @@ const StyledItemContainer = styled.div<{ isMarginBottom?: boolean }>`
`;
const StyledItemTitleDate = styled.div`
+ @media (max-width: ${MOBILE_VIEWPORT}px) {
+ display: none;
+ }
align-items: flex-start;
padding-top: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.font.color.tertiary};
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 84%
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..9695f98c58f2 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,20 @@ 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';
+ MOBILE_VIEWPORT,
+} from 'twenty-ui';
const StyledMainContainer = styled.div`
align-items: flex-start;
@@ -31,6 +32,11 @@ const StyledMainContainer = styled.div`
padding-right: ${({ theme }) => theme.spacing(6)};
padding-left: ${({ theme }) => theme.spacing(6)};
gap: ${({ theme }) => theme.spacing(4)};
+
+ @media (max-width: ${MOBILE_VIEWPORT}px) {
+ padding-right: ${({ theme }) => theme.spacing(1)};
+ padding-left: ${({ theme }) => theme.spacing(1)};
+ }
`;
export const TimelineActivities = ({
@@ -46,7 +52,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 92%
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..47f95270d6a4 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';
@@ -16,6 +16,10 @@ const StyledLinkedActivity = styled.span`
color: ${({ theme }) => theme.font.color.primary};
cursor: pointer;
text-decoration: underline;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
`;
export const StyledEventRowItemText = styled.span`
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 89%
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..267e56f2d83c 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, MOBILE_VIEWPORT } from 'twenty-ui';
type EventCardProps = {
children: React.ReactNode;
@@ -16,6 +16,10 @@ const StyledCardContainer = styled.div`
width: 400px;
padding: ${({ theme }) => theme.spacing(2)} 0px
${({ theme }) => theme.spacing(1)} 0px;
+
+ @media (max-width: ${MOBILE_VIEWPORT}px) {
+ width: 300px;
+ }
`;
const StyledCard = styled(Card)`
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 85%
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..e212b15c5576 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';
@@ -20,7 +20,10 @@ const StyledEventFieldDiffContainer = styled.div`
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
height: 24px;
- width: 380px;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
`;
const StyledEmptyValue = styled.div`
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/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx b/packages/twenty-front/src/modules/analytics/components/AnalyticsActivityGraph.tsx
similarity index 67%
rename from packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx
rename to packages/twenty-front/src/modules/analytics/components/AnalyticsActivityGraph.tsx
index 9626c6712eef..1a124ad90c52 100644
--- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx
+++ b/packages/twenty-front/src/modules/analytics/components/AnalyticsActivityGraph.tsx
@@ -1,23 +1,19 @@
-import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip';
-import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
-import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
+import { WebhookAnalyticsTooltip } from '@/analytics/components/WebhookAnalyticsTooltip';
+import { ANALYTICS_GRAPH_DESCRIPTION_MAP } from '@/analytics/constants/AnalyticsGraphDescriptionMap';
+import { ANALYTICS_GRAPH_TITLE_MAP } from '@/analytics/constants/AnalyticsGraphTitleMap';
+import { useGraphData } from '@/analytics/hooks/useGraphData';
+import { analyticsGraphDataComponentState } from '@/analytics/states/analyticsGraphDataComponentState';
+import { AnalyticsComponentProps as AnalyticsActivityGraphProps } from '@/analytics/types/AnalyticsComponentProps';
+import { computeAnalyticsGraphDataFunction } from '@/analytics/utils/computeAnalyticsGraphDataFunction';
import { Select } from '@/ui/input/components/Select';
+import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ResponsiveLine } from '@nivo/line';
import { Section } from '@react-email/components';
-import { useState } from 'react';
-import { useRecoilValue, useSetRecoilState } from 'recoil';
+import { useId, useState } from 'react';
import { H2Title } from 'twenty-ui';
-export type NivoLineInput = {
- id: string | number;
- color?: string;
- data: Array<{
- x: number | string | Date;
- y: number | string | Date;
- }>;
-};
const StyledGraphContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
@@ -33,34 +29,38 @@ const StyledTitleContainer = styled.div`
justify-content: space-between;
`;
-type SettingsDevelopersWebhookUsageGraphProps = {
- webhookId: string;
-};
-
-export const SettingsDevelopersWebhookUsageGraph = ({
- webhookId,
-}: SettingsDevelopersWebhookUsageGraphProps) => {
- const webhookGraphData = useRecoilValue(webhookGraphDataState);
- const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
+export const AnalyticsActivityGraph = ({
+ recordId,
+ endpointName,
+}: AnalyticsActivityGraphProps) => {
+ const [analyticsGraphData, setAnalyticsGraphData] = useRecoilComponentStateV2(
+ analyticsGraphDataComponentState,
+ );
const theme = useTheme();
const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
'7D' | '1D' | '12H' | '4H'
>('7D');
- const { fetchGraphData } = useGraphData(webhookId);
+ const { fetchGraphData } = useGraphData({
+ recordId,
+ endpointName,
+ });
+
+ const transformDataFunction = computeAnalyticsGraphDataFunction(endpointName);
+ const dropdownId = useId();
return (
<>
- {webhookGraphData.length ? (
+ {analyticsGraphData.length ? (
diff --git a/packages/twenty-front/src/modules/analytics/components/AnalyticsGraphEffect.tsx b/packages/twenty-front/src/modules/analytics/components/AnalyticsGraphEffect.tsx
new file mode 100644
index 000000000000..e74838882667
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/components/AnalyticsGraphEffect.tsx
@@ -0,0 +1,32 @@
+import { useGraphData } from '@/analytics/hooks/useGraphData';
+import { analyticsGraphDataComponentState } from '@/analytics/states/analyticsGraphDataComponentState';
+import { AnalyticsComponentProps as AnalyticsGraphEffectProps } from '@/analytics/types/AnalyticsComponentProps';
+import { computeAnalyticsGraphDataFunction } from '@/analytics/utils/computeAnalyticsGraphDataFunction';
+import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
+import { useState } from 'react';
+
+export const AnalyticsGraphEffect = ({
+ recordId,
+ endpointName,
+}: AnalyticsGraphEffectProps) => {
+ const setAnalyticsGraphData = useSetRecoilComponentStateV2(
+ analyticsGraphDataComponentState,
+ );
+
+ const transformDataFunction = computeAnalyticsGraphDataFunction(endpointName);
+ const [isLoaded, setIsLoaded] = useState(false);
+
+ const { fetchGraphData } = useGraphData({
+ recordId,
+ endpointName,
+ });
+
+ if (!isLoaded) {
+ fetchGraphData('7D').then((graphInput) => {
+ setAnalyticsGraphData(transformDataFunction(graphInput));
+ });
+ setIsLoaded(true);
+ }
+
+ return <>>;
+};
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx b/packages/twenty-front/src/modules/analytics/components/WebhookAnalyticsTooltip.tsx
similarity index 94%
rename from packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx
rename to packages/twenty-front/src/modules/analytics/components/WebhookAnalyticsTooltip.tsx
index 40925c5d3830..a39f560000f5 100644
--- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx
+++ b/packages/twenty-front/src/modules/analytics/components/WebhookAnalyticsTooltip.tsx
@@ -58,12 +58,12 @@ const StyledDataDefinition = styled.div`
const StyledSpan = styled.span`
color: ${({ theme }) => theme.font.color.primary};
`;
-type SettingsDevelopersWebhookTooltipProps = {
+type WebhookAnalyticsTooltipProps = {
point: Point;
};
-export const SettingsDevelopersWebhookTooltip = ({
+export const WebhookAnalyticsTooltip = ({
point,
-}: SettingsDevelopersWebhookTooltipProps): ReactElement => {
+}: WebhookAnalyticsTooltipProps): ReactElement => {
const { timeFormat, timeZone } = useContext(UserContext);
const windowInterval = new Date(point.data.x);
const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
diff --git a/packages/twenty-front/src/modules/analytics/constants/AnalyticsEndpointTypeMap.ts b/packages/twenty-front/src/modules/analytics/constants/AnalyticsEndpointTypeMap.ts
new file mode 100644
index 000000000000..77310b6ab0ab
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/constants/AnalyticsEndpointTypeMap.ts
@@ -0,0 +1,10 @@
+import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';
+
+export const ANALYTICS_ENDPOINT_TYPE_MAP: AnalyticsTinybirdJwtMap = {
+ getWebhookAnalytics: 'webhook',
+ getPageviewsAnalytics: 'pageviews',
+ getUsersAnalytics: 'users',
+ getServerlessFunctionDuration: 'function',
+ getServerlessFunctionSuccessRate: 'function',
+ getServerlessFunctionErrorCount: 'function',
+};
diff --git a/packages/twenty-front/src/modules/analytics/constants/AnalyticsGraphDescriptionMap.ts b/packages/twenty-front/src/modules/analytics/constants/AnalyticsGraphDescriptionMap.ts
new file mode 100644
index 000000000000..397645eb0cb9
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/constants/AnalyticsGraphDescriptionMap.ts
@@ -0,0 +1,10 @@
+import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';
+
+export const ANALYTICS_GRAPH_DESCRIPTION_MAP: AnalyticsTinybirdJwtMap = {
+ getWebhookAnalytics: 'See your webhook activity over time',
+ getPageviewsAnalytics: 'See your Page Views activity over time',
+ getUsersAnalytics: 'See your Users activity over time',
+ getServerlessFunctionDuration: 'See your function duration over time',
+ getServerlessFunctionSuccessRate: 'See your function success rate over time',
+ getServerlessFunctionErrorCount: 'See your function error count over time',
+};
diff --git a/packages/twenty-front/src/modules/analytics/constants/AnalyticsGraphOptionMap.ts b/packages/twenty-front/src/modules/analytics/constants/AnalyticsGraphOptionMap.ts
new file mode 100644
index 000000000000..d28b61c5ff20
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/constants/AnalyticsGraphOptionMap.ts
@@ -0,0 +1,6 @@
+export const ANALYTICS_GRAPH_OPTION_MAP = {
+ '7D': { granularity: 'day' },
+ '1D': { granularity: 'hour' },
+ '12H': { granularity: 'hour' },
+ '4H': { granularity: 'hour' },
+};
diff --git a/packages/twenty-front/src/modules/analytics/constants/AnalyticsGraphTitleMap.ts b/packages/twenty-front/src/modules/analytics/constants/AnalyticsGraphTitleMap.ts
new file mode 100644
index 000000000000..edcaf12ee974
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/constants/AnalyticsGraphTitleMap.ts
@@ -0,0 +1,10 @@
+import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';
+
+export const ANALYTICS_GRAPH_TITLE_MAP: AnalyticsTinybirdJwtMap = {
+ getWebhookAnalytics: 'Activity',
+ getPageviewsAnalytics: 'Page Views',
+ getUsersAnalytics: 'Users',
+ getServerlessFunctionDuration: 'Duration (ms)',
+ getServerlessFunctionSuccessRate: 'Success Rate (%)',
+ getServerlessFunctionErrorCount: 'Error Count',
+};
diff --git a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx
new file mode 100644
index 000000000000..98c74d1a0f94
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx
@@ -0,0 +1,87 @@
+import { useAnalyticsTinybirdJwts } from '@/analytics/hooks/useAnalyticsTinybirdJwts';
+import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
+import { act, renderHook } from '@testing-library/react';
+import { useSetRecoilState } from 'recoil';
+import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
+
+const Wrapper = getJestMetadataAndApolloMocksWrapper({
+ apolloMocks: [],
+});
+
+describe('useAnalyticsTinybirdJwts', () => {
+ const JWT_NAME = 'getWebhookAnalytics';
+ const TEST_JWT_TOKEN = 'test-jwt-token';
+
+ it('should return undefined when no user is logged in', () => {
+ const { result } = renderHook(
+ () => {
+ const setCurrentUserState = useSetRecoilState(currentUserState);
+ return {
+ hook: useAnalyticsTinybirdJwts(JWT_NAME),
+ setCurrentUserState,
+ };
+ },
+ { wrapper: Wrapper },
+ );
+
+ act(() => {
+ result.current.setCurrentUserState(null);
+ });
+
+ expect(result.current.hook).toBeUndefined();
+ });
+
+ it('should return the correct JWT token when available', () => {
+ const { result } = renderHook(
+ () => {
+ const setCurrentUserState = useSetRecoilState(currentUserState);
+ return {
+ hook: useAnalyticsTinybirdJwts(JWT_NAME),
+ setCurrentUserState,
+ };
+ },
+ { wrapper: Wrapper },
+ );
+
+ act(() => {
+ result.current.setCurrentUserState({
+ id: '1',
+ email: 'test@test.com',
+ canImpersonate: false,
+ userVars: {},
+ analyticsTinybirdJwts: {
+ [JWT_NAME]: TEST_JWT_TOKEN,
+ },
+ } as CurrentUser);
+ });
+
+ expect(result.current.hook).toBe(TEST_JWT_TOKEN);
+ });
+
+ it('should return undefined when JWT token is not available', () => {
+ const { result } = renderHook(
+ () => {
+ const setCurrentUserState = useSetRecoilState(currentUserState);
+ return {
+ hook: useAnalyticsTinybirdJwts(JWT_NAME),
+ setCurrentUserState,
+ };
+ },
+ { wrapper: Wrapper },
+ );
+
+ act(() => {
+ result.current.setCurrentUserState({
+ id: '1',
+ email: 'test@test.com',
+ canImpersonate: false,
+ userVars: {},
+ analyticsTinybirdJwts: {
+ getPageviewsAnalytics: TEST_JWT_TOKEN,
+ },
+ } as CurrentUser);
+ });
+
+ expect(result.current.hook).toBeUndefined();
+ });
+});
diff --git a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useGraphData.test.tsx b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useGraphData.test.tsx
new file mode 100644
index 000000000000..f3c8c2670774
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useGraphData.test.tsx
@@ -0,0 +1,87 @@
+import { useAnalyticsTinybirdJwts } from '@/analytics/hooks/useAnalyticsTinybirdJwts';
+import { useGraphData } from '@/analytics/hooks/useGraphData';
+import { fetchGraphDataOrThrow } from '@/analytics/utils/fetchGraphDataOrThrow';
+import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { renderHook } from '@testing-library/react';
+jest.mock('@/analytics/hooks/useAnalyticsTinybirdJwts');
+jest.mock('@/analytics/utils/fetchGraphDataOrThrow');
+jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
+
+describe('useGraphData', () => {
+ const mockEnqueueSnackBar = jest.fn();
+ const mockUseSnackBar = jest.fn().mockReturnValue({
+ enqueueSnackBar: mockEnqueueSnackBar,
+ });
+
+ const mockUseAnalyticsTinybirdJwts = jest.fn();
+ const mockFetchGraphDataOrThrow = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useSnackBar as jest.MockedFunction).mockImplementation(
+ mockUseSnackBar,
+ );
+ (
+ useAnalyticsTinybirdJwts as jest.MockedFunction<
+ typeof useAnalyticsTinybirdJwts
+ >
+ ).mockImplementation(mockUseAnalyticsTinybirdJwts);
+ (
+ fetchGraphDataOrThrow as jest.MockedFunction
+ ).mockImplementation(mockFetchGraphDataOrThrow);
+ });
+
+ it('should fetch graph data successfully', async () => {
+ const mockJwt = 'mock-jwt';
+ const mockRecordId = 'mock-record-id';
+ const mockEndpointName = 'getWebhookAnalytics';
+ const mockGraphData = [{ x: '2023-01-01', y: 100 }];
+
+ mockUseAnalyticsTinybirdJwts.mockReturnValue(mockJwt);
+ mockFetchGraphDataOrThrow.mockResolvedValue(mockGraphData);
+
+ const { result } = renderHook(() =>
+ useGraphData({ recordId: mockRecordId, endpointName: mockEndpointName }),
+ );
+ const { fetchGraphData } = result.current;
+
+ const data = await fetchGraphData('7D');
+ expect(data).toEqual(mockGraphData);
+ expect(mockFetchGraphDataOrThrow).toHaveBeenCalledWith({
+ recordId: mockRecordId,
+ windowLength: '7D',
+ tinybirdJwt: mockJwt,
+ endpointName: mockEndpointName,
+ });
+ expect(mockEnqueueSnackBar).not.toHaveBeenCalled();
+ });
+ it('should handle errors when fetching graph data', async () => {
+ const mockRecordId = 'mock-record-id';
+ const mockEndpointName = 'getWebhookAnalytics';
+ const mockError = new Error('Something went wrong');
+
+ mockUseAnalyticsTinybirdJwts.mockReturnValue('');
+ mockFetchGraphDataOrThrow.mockRejectedValue(mockError);
+
+ const { result } = renderHook(() =>
+ useGraphData({ recordId: mockRecordId, endpointName: mockEndpointName }),
+ );
+ const { fetchGraphData } = result.current;
+
+ const data = await fetchGraphData('7D');
+ expect(data).toEqual([]);
+ expect(mockFetchGraphDataOrThrow).toHaveBeenCalledWith({
+ recordId: mockRecordId,
+ windowLength: '7D',
+ tinybirdJwt: '',
+ endpointName: mockEndpointName,
+ });
+ expect(mockEnqueueSnackBar).toHaveBeenCalledWith(
+ 'Something went wrong while fetching webhook usage: Something went wrong',
+ {
+ variant: SnackBarVariant.Error,
+ },
+ );
+ });
+});
diff --git a/packages/twenty-front/src/modules/analytics/hooks/useAnalyticsTinybirdJwts.ts b/packages/twenty-front/src/modules/analytics/hooks/useAnalyticsTinybirdJwts.ts
new file mode 100644
index 000000000000..64142ab8b816
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/hooks/useAnalyticsTinybirdJwts.ts
@@ -0,0 +1,16 @@
+import { useRecoilValue } from 'recoil';
+
+import { currentUserState } from '@/auth/states/currentUserState';
+import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';
+
+export const useAnalyticsTinybirdJwts = (
+ jwtName: keyof AnalyticsTinybirdJwtMap,
+): string | undefined => {
+ const currentUser = useRecoilValue(currentUserState);
+
+ if (!currentUser) {
+ return undefined;
+ }
+
+ return currentUser.analyticsTinybirdJwts?.[jwtName];
+};
diff --git a/packages/twenty-front/src/modules/analytics/hooks/useGraphData.tsx b/packages/twenty-front/src/modules/analytics/hooks/useGraphData.tsx
new file mode 100644
index 000000000000..d112cee0e856
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/hooks/useGraphData.tsx
@@ -0,0 +1,42 @@
+import { useAnalyticsTinybirdJwts } from '@/analytics/hooks/useAnalyticsTinybirdJwts';
+import { AnalyticsComponentProps as useGraphDataProps } from '@/analytics/types/AnalyticsComponentProps';
+import { fetchGraphDataOrThrow } from '@/analytics/utils/fetchGraphDataOrThrow';
+import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { isUndefined } from '@sniptt/guards';
+import { useCallback } from 'react';
+
+export const useGraphData = ({ recordId, endpointName }: useGraphDataProps) => {
+ const { enqueueSnackBar } = useSnackBar();
+ const tinybirdJwt = useAnalyticsTinybirdJwts(endpointName);
+
+ const fetchGraphData = useCallback(
+ async (windowLengthGraphOption: '7D' | '1D' | '12H' | '4H') => {
+ try {
+ if (isUndefined(tinybirdJwt)) {
+ throw new Error('No jwt associated with this endpoint found');
+ }
+
+ return await fetchGraphDataOrThrow({
+ recordId,
+ windowLength: windowLengthGraphOption,
+ tinybirdJwt,
+ endpointName,
+ });
+ } catch (error) {
+ if (error instanceof Error) {
+ enqueueSnackBar(
+ `Something went wrong while fetching webhook usage: ${error.message}`,
+ {
+ variant: SnackBarVariant.Error,
+ },
+ );
+ }
+ return [];
+ }
+ },
+ [tinybirdJwt, recordId, endpointName, enqueueSnackBar],
+ );
+
+ return { fetchGraphData };
+};
diff --git a/packages/twenty-front/src/modules/analytics/states/analyticsGraphDataComponentState.ts b/packages/twenty-front/src/modules/analytics/states/analyticsGraphDataComponentState.ts
new file mode 100644
index 000000000000..315d5f276d46
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/states/analyticsGraphDataComponentState.ts
@@ -0,0 +1,10 @@
+import { AnalyticsGraphDataInstanceContext } from '@/analytics/states/contexts/AnalyticsGraphDataInstanceContext';
+import { NivoLineInput } from '@/analytics/types/NivoLineInput';
+import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
+export const analyticsGraphDataComponentState = createComponentStateV2<
+ NivoLineInput[]
+>({
+ key: 'analyticsGraphDataComponentState',
+ defaultValue: [],
+ componentInstanceContext: AnalyticsGraphDataInstanceContext,
+});
diff --git a/packages/twenty-front/src/modules/analytics/states/contexts/AnalyticsGraphDataInstanceContext.ts b/packages/twenty-front/src/modules/analytics/states/contexts/AnalyticsGraphDataInstanceContext.ts
new file mode 100644
index 000000000000..9d2746554794
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/states/contexts/AnalyticsGraphDataInstanceContext.ts
@@ -0,0 +1,4 @@
+import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
+
+export const AnalyticsGraphDataInstanceContext =
+ createComponentInstanceContext();
diff --git a/packages/twenty-front/src/modules/analytics/types/AnalyticsComponentProps.ts b/packages/twenty-front/src/modules/analytics/types/AnalyticsComponentProps.ts
new file mode 100644
index 000000000000..c3b6f4f269c6
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/types/AnalyticsComponentProps.ts
@@ -0,0 +1,6 @@
+import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';
+
+export type AnalyticsComponentProps = {
+ recordId: string;
+ endpointName: keyof AnalyticsTinybirdJwtMap;
+};
diff --git a/packages/twenty-front/src/modules/analytics/types/NivoLineInput.ts b/packages/twenty-front/src/modules/analytics/types/NivoLineInput.ts
new file mode 100644
index 000000000000..7f2883870ae0
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/types/NivoLineInput.ts
@@ -0,0 +1,8 @@
+export type NivoLineInput = {
+ id: string | number;
+ color?: string;
+ data: Array<{
+ x: number | string | Date;
+ y: number | string | Date;
+ }>;
+};
diff --git a/packages/twenty-front/src/modules/analytics/utils/__tests__/computeAnalyticsGraphDataFunction.test.js b/packages/twenty-front/src/modules/analytics/utils/__tests__/computeAnalyticsGraphDataFunction.test.js
new file mode 100644
index 000000000000..8d48b2f82bba
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/__tests__/computeAnalyticsGraphDataFunction.test.js
@@ -0,0 +1,56 @@
+import { computeAnalyticsGraphDataFunction } from '@/analytics/utils/computeAnalyticsGraphDataFunction';
+import { mapServerlessFunctionDurationToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionDurationToNivoLineInput';
+import { mapServerlessFunctionErrorsToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput';
+import { mapWebhookAnalyticsResultToNivoLineInput } from '@/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput';
+
+jest.mock('@/analytics/utils/mapServerlessFunctionDurationToNivoLineInput');
+jest.mock('@/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput');
+jest.mock('@/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput');
+
+describe('computeAnalyticsGraphDataFunction', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return the mapWebhookAnalyticsResultToNivoLineInput function for "getWebhookAnalytics"', () => {
+ const result = computeAnalyticsGraphDataFunction('getWebhookAnalytics');
+ expect(result).toBe(mapWebhookAnalyticsResultToNivoLineInput);
+ });
+
+ it('should return the mapServerlessFunctionDurationToNivoLineInput function for "getServerlessFunctionDuration"', () => {
+ const result = computeAnalyticsGraphDataFunction(
+ 'getServerlessFunctionDuration',
+ );
+ expect(result).toBe(mapServerlessFunctionDurationToNivoLineInput);
+ });
+
+ it('should return a function that calls mapServerlessFunctionErrorsToNivoLineInput with "ErrorCount" for "getServerlessFunctionErrorCount"', () => {
+ const result = computeAnalyticsGraphDataFunction(
+ 'getServerlessFunctionErrorCount',
+ );
+ const data = [{ start: '2023-01-01', error_count: 10 }];
+ result(data);
+ expect(mapServerlessFunctionErrorsToNivoLineInput).toHaveBeenCalledWith(
+ data,
+ 'ErrorCount',
+ );
+ });
+
+ it('should return a function that calls mapServerlessFunctionErrorsToNivoLineInput with "SuccessRate" for "getServerlessFunctionSuccessRate"', () => {
+ const result = computeAnalyticsGraphDataFunction(
+ 'getServerlessFunctionSuccessRate',
+ );
+ const data = [{ start: '2023-01-01', success_rate: 90 }];
+ result(data);
+ expect(mapServerlessFunctionErrorsToNivoLineInput).toHaveBeenCalledWith(
+ data,
+ 'SuccessRate',
+ );
+ });
+
+ it('should throw an error for an unknown endpoint', () => {
+ expect(() => computeAnalyticsGraphDataFunction('unknown')).toThrowError(
+ 'No analytics function found associated with endpoint "unknown"',
+ );
+ });
+});
diff --git a/packages/twenty-front/src/modules/analytics/utils/__tests__/fetchGraphDataOrThrow.test.js b/packages/twenty-front/src/modules/analytics/utils/__tests__/fetchGraphDataOrThrow.test.js
new file mode 100644
index 000000000000..a005480756a6
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/__tests__/fetchGraphDataOrThrow.test.js
@@ -0,0 +1,143 @@
+import { ANALYTICS_GRAPH_OPTION_MAP } from '@/analytics/constants/AnalyticsGraphOptionMap';
+import { computeStartEndDate } from '@/analytics/utils/computeStartEndDate';
+import { fetchGraphDataOrThrow } from '@/analytics/utils/fetchGraphDataOrThrow';
+
+// Im going to make this test more contundent later
+jest.mock('@/analytics/utils/computeStartEndDate', () => ({
+ computeStartEndDate: jest.fn(() => ({
+ start: '2024-01-01',
+ end: '2024-01-07',
+ })),
+}));
+
+describe('fetchGraphDataOrThrow', () => {
+ // Setup fetch mock
+ const mockFetch = jest.fn();
+ global.fetch = mockFetch;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockSuccessResponse = {
+ data: [
+ { timestamp: '2024-01-01', count: 10 },
+ { timestamp: '2024-01-02', count: 20 },
+ ],
+ };
+
+ const defaultProps = {
+ recordId: 'test-123',
+ windowLength: '7D',
+ tinybirdJwt: 'test-jwt',
+ endpointName: 'getWebhookAnalytics',
+ };
+
+ it('should fetch data successfully for webhook type', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve(mockSuccessResponse),
+ });
+
+ const result = await fetchGraphDataOrThrow(defaultProps);
+
+ // Verify URL construction
+ const lastCallArgs = mockFetch.mock.calls[0][0];
+ expect(lastCallArgs).toContain('webhookId=test-123');
+ expect(lastCallArgs).toContain('getWebhookAnalytics.json');
+
+ // Verify headers
+ const headers = mockFetch.mock.calls[0][1].headers;
+ expect(headers.Authorization).toBe('Bearer test-jwt');
+
+ // Verify response
+ expect(result).toEqual(mockSuccessResponse.data);
+ });
+
+ it('should handle different window lengths correctly', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve(mockSuccessResponse),
+ });
+
+ await fetchGraphDataOrThrow({
+ ...defaultProps,
+ windowLength: '1D',
+ });
+
+ // Verify that correct window length options were used
+ const lastCallArgs = mockFetch.mock.calls[0][0];
+ const options = ANALYTICS_GRAPH_OPTION_MAP['1D'];
+ Object.entries(options).forEach(([key, value]) => {
+ expect(lastCallArgs).toContain(`${key}=${value}`);
+ });
+ });
+
+ it('should throw error on failed request', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ json: () => Promise.resolve({ error: 'Failed to fetch' }),
+ });
+
+ await expect(fetchGraphDataOrThrow(defaultProps)).rejects.toThrow(
+ 'Failed to fetch',
+ );
+ });
+
+ it('should throw error on network failure', async () => {
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
+
+ await expect(fetchGraphDataOrThrow(defaultProps)).rejects.toThrow(
+ 'Network error',
+ );
+ });
+
+ it('should use computed start and end dates', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve(mockSuccessResponse),
+ });
+
+ await fetchGraphDataOrThrow(defaultProps);
+
+ // Verify computeStartEndDate was called with correct window length
+ expect(computeStartEndDate).toHaveBeenCalledWith('7D');
+
+ // Verify the computed dates are included in the URL
+ const lastCallArgs = mockFetch.mock.calls[0][0];
+ expect(lastCallArgs).toContain('start=2024-01-01');
+ expect(lastCallArgs).toContain('end=2024-01-07');
+ });
+
+ it('should construct URL with all required parameters', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve(mockSuccessResponse),
+ });
+
+ await fetchGraphDataOrThrow(defaultProps);
+
+ const lastCallArgs = mockFetch.mock.calls[0][0];
+
+ // Check base URL
+ expect(lastCallArgs).toContain(
+ 'https://api.eu-central-1.aws.tinybird.co/v0/pipes/',
+ );
+
+ // Check endpoint
+ expect(lastCallArgs).toContain('getWebhookAnalytics.json');
+
+ // Check window length options
+ const options = ANALYTICS_GRAPH_OPTION_MAP['7D'];
+ Object.entries(options).forEach(([key, value]) => {
+ expect(lastCallArgs).toContain(`${key}=${value}`);
+ });
+
+ // Check computed dates
+ expect(lastCallArgs).toContain('start=2024-01-01');
+ expect(lastCallArgs).toContain('end=2024-01-07');
+
+ // Check record ID
+ expect(lastCallArgs).toContain('webhookId=test-123');
+ });
+});
diff --git a/packages/twenty-front/src/modules/analytics/utils/__tests__/mapServerlessFunctionDurationToNivoLineInput.test.js b/packages/twenty-front/src/modules/analytics/utils/__tests__/mapServerlessFunctionDurationToNivoLineInput.test.js
new file mode 100644
index 000000000000..87212259885f
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/__tests__/mapServerlessFunctionDurationToNivoLineInput.test.js
@@ -0,0 +1,65 @@
+import { mapServerlessFunctionDurationToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionDurationToNivoLineInput';
+
+describe('mapServerlessFunctionDurationToNivoLineInput', () => {
+ it('should convert the serverless function duration result to NivoLineInput format', () => {
+ const serverlessFunctionDurationResult = [
+ {
+ start: '2023-01-01T00:00:00.000Z',
+ minimum: 100,
+ maximum: 200,
+ average: 150,
+ },
+ {
+ start: '2023-01-02T00:00:00.000Z',
+ minimum: 80,
+ maximum: 160,
+ average: 120,
+ },
+ {
+ start: '2023-01-03T00:00:00.000Z',
+ minimum: 90,
+ maximum: 180,
+ average: 135,
+ },
+ ];
+
+ const expected = [
+ {
+ id: 'Maximum',
+ data: [
+ { x: new Date('2023-01-01T00:00:00.000Z'), y: 200 },
+ { x: new Date('2023-01-02T00:00:00.000Z'), y: 160 },
+ { x: new Date('2023-01-03T00:00:00.000Z'), y: 180 },
+ ],
+ },
+ {
+ id: 'Minimum',
+ data: [
+ { x: new Date('2023-01-01T00:00:00.000Z'), y: 100 },
+ { x: new Date('2023-01-02T00:00:00.000Z'), y: 80 },
+ { x: new Date('2023-01-03T00:00:00.000Z'), y: 90 },
+ ],
+ },
+ {
+ id: 'Average',
+ data: [
+ { x: new Date('2023-01-01T00:00:00.000Z'), y: 150 },
+ { x: new Date('2023-01-02T00:00:00.000Z'), y: 120 },
+ { x: new Date('2023-01-03T00:00:00.000Z'), y: 135 },
+ ],
+ },
+ ];
+
+ const result = mapServerlessFunctionDurationToNivoLineInput(
+ serverlessFunctionDurationResult,
+ );
+ expect(result).toEqual(expected);
+ });
+ it('should handle an empty serverless function duration result', () => {
+ const serverlessFunctionDurationResult = [];
+ const result = mapServerlessFunctionDurationToNivoLineInput(
+ serverlessFunctionDurationResult,
+ );
+ expect(result).toEqual([]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/analytics/utils/__tests__/mapServerlessFunctionErrorstToNivoLineInput.test.js b/packages/twenty-front/src/modules/analytics/utils/__tests__/mapServerlessFunctionErrorstToNivoLineInput.test.js
new file mode 100644
index 000000000000..100a583b381e
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/__tests__/mapServerlessFunctionErrorstToNivoLineInput.test.js
@@ -0,0 +1,68 @@
+import { mapServerlessFunctionErrorsToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput';
+
+describe('mapServerlessFunctionErrorsToNivoLineInput', () => {
+ it('should map the serverless function result to Nivo line input format for error count', () => {
+ const serverlessFunctionResult = [
+ { start: '2023-01-01', error_count: 10, success_rate: 0.66 },
+ { start: '2023-01-02', error_count: 5, success_rate: 0.75 },
+ { start: '2023-01-03', error_count: 8, success_rate: 0.69 },
+ ];
+
+ const expected = [
+ {
+ id: 'Error',
+ data: [
+ { x: new Date('2023-01-01'), y: 10 },
+ { x: new Date('2023-01-02'), y: 5 },
+ { x: new Date('2023-01-03'), y: 8 },
+ ],
+ },
+ ];
+
+ const result = mapServerlessFunctionErrorsToNivoLineInput(
+ serverlessFunctionResult,
+ 'ErrorCount',
+ );
+ expect(result).toEqual(expected);
+ });
+
+ it('should map the serverless function result to Nivo line input format for success rate', () => {
+ const serverlessFunctionResult = [
+ { start: '2023-01-01', error_count: 10, success_rate: 0.66 },
+ { start: '2023-01-02', error_count: 5, success_rate: 0.75 },
+ { start: '2023-01-03', error_count: 8, success_rate: 0.69 },
+ ];
+
+ const expected = [
+ {
+ id: 'Success Rate',
+ data: [
+ { x: new Date('2023-01-01'), y: 0.66 },
+ { x: new Date('2023-01-02'), y: 0.75 },
+ { x: new Date('2023-01-03'), y: 0.69 },
+ ],
+ },
+ ];
+
+ const result = mapServerlessFunctionErrorsToNivoLineInput(
+ serverlessFunctionResult,
+ 'SuccessRate',
+ );
+ expect(result).toEqual(expected);
+ });
+
+ it('should handle empty input', () => {
+ const serverlessFunctionResult = [];
+ const expected = [
+ {
+ id: 'Error',
+ data: [],
+ },
+ ];
+ const result = mapServerlessFunctionErrorsToNivoLineInput(
+ serverlessFunctionResult,
+ 'ErrorCount',
+ );
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/twenty-front/src/modules/analytics/utils/__tests__/mapWebhookAnalyticsResultToNivoLineInput.test.js b/packages/twenty-front/src/modules/analytics/utils/__tests__/mapWebhookAnalyticsResultToNivoLineInput.test.js
new file mode 100644
index 000000000000..968ef5d6c114
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/__tests__/mapWebhookAnalyticsResultToNivoLineInput.test.js
@@ -0,0 +1,187 @@
+import { mapWebhookAnalyticsResultToNivoLineInput } from '@/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput';
+
+describe('mapWebhookAnalyticsResultToNivoLineInput', () => {
+ it('should correctly map empty array', () => {
+ const result = mapWebhookAnalyticsResultToNivoLineInput([]);
+ expect(result).toEqual([]);
+ });
+
+ it('should correctly map single data point', () => {
+ const input = [
+ {
+ start: '2024-01-01T00:00:00Z',
+ success_count: 10,
+ failure_count: 5,
+ },
+ ];
+
+ const expected = [
+ {
+ id: 'Failed',
+ data: [
+ {
+ x: new Date('2024-01-01T00:00:00Z'),
+ y: 5,
+ },
+ ],
+ },
+ {
+ id: 'Succeeded',
+ data: [
+ {
+ x: new Date('2024-01-01T00:00:00Z'),
+ y: 10,
+ },
+ ],
+ },
+ ];
+
+ const result = mapWebhookAnalyticsResultToNivoLineInput(input);
+ expect(result).toEqual(expected);
+ });
+
+ it('should correctly map multiple data points', () => {
+ const input = [
+ {
+ start: '2024-01-01T00:00:00Z',
+ success_count: 10,
+ failure_count: 5,
+ },
+ {
+ start: '2024-01-02T00:00:00Z',
+ success_count: 15,
+ failure_count: 3,
+ },
+ ];
+
+ const expected = [
+ {
+ id: 'Failed',
+
+ data: [
+ {
+ x: new Date('2024-01-01T00:00:00Z'),
+ y: 5,
+ },
+ {
+ x: new Date('2024-01-02T00:00:00Z'),
+ y: 3,
+ },
+ ],
+ },
+ {
+ id: 'Succeeded',
+
+ data: [
+ {
+ x: new Date('2024-01-01T00:00:00Z'),
+ y: 10,
+ },
+ {
+ x: new Date('2024-01-02T00:00:00Z'),
+ y: 15,
+ },
+ ],
+ },
+ ];
+
+ const result = mapWebhookAnalyticsResultToNivoLineInput(input);
+ expect(result).toEqual(expected);
+ });
+
+ it('should handle zero counts', () => {
+ const input = [
+ {
+ start: '2024-01-01T00:00:00Z',
+ success_count: 0,
+ failure_count: 0,
+ },
+ ];
+
+ const expected = [
+ {
+ id: 'Failed',
+
+ data: [
+ {
+ x: new Date('2024-01-01T00:00:00Z'),
+ y: 0,
+ },
+ ],
+ },
+ {
+ id: 'Succeeded',
+
+ data: [
+ {
+ x: new Date('2024-01-01T00:00:00Z'),
+ y: 0,
+ },
+ ],
+ },
+ ];
+
+ const result = mapWebhookAnalyticsResultToNivoLineInput(input);
+ expect(result).toEqual(expected);
+ });
+
+ it('should preserve data point order', () => {
+ const input = [
+ {
+ start: '2024-01-02T00:00:00Z',
+ success_count: 15,
+ failure_count: 3,
+ },
+ {
+ start: '2024-01-01T00:00:00Z',
+ success_count: 10,
+ failure_count: 5,
+ },
+ ];
+
+ const result = mapWebhookAnalyticsResultToNivoLineInput(input);
+
+ // Check that dates in data arrays maintain input order
+ expect(result[0].data[0].x).toEqual(new Date('2024-01-02T00:00:00Z'));
+ expect(result[0].data[1].x).toEqual(new Date('2024-01-01T00:00:00Z'));
+ expect(result[1].data[0].x).toEqual(new Date('2024-01-02T00:00:00Z'));
+ expect(result[1].data[1].x).toEqual(new Date('2024-01-01T00:00:00Z'));
+ });
+
+ it('should handle malformed dates by creating invalid Date objects', () => {
+ const input = [
+ {
+ start: 'invalid-date',
+ success_count: 10,
+ failure_count: 5,
+ },
+ ];
+
+ const result = mapWebhookAnalyticsResultToNivoLineInput(input);
+
+ expect(result[0].data[0].x.toString()).toBe('Invalid Date');
+ expect(result[1].data[0].x.toString()).toBe('Invalid Date');
+ });
+
+ it('should maintain consistent structure with mixed data', () => {
+ const input = [
+ {
+ start: '2024-01-01T00:00:00Z',
+ success_count: 10,
+ failure_count: 0,
+ },
+ {
+ start: '2024-01-02T00:00:00Z',
+ success_count: 0,
+ failure_count: 5,
+ },
+ ];
+
+ const result = mapWebhookAnalyticsResultToNivoLineInput(input);
+
+ // Check both lines exist even when one has zero values
+ expect(result.length).toBe(2);
+ expect(result[0].data.length).toBe(2);
+ expect(result[1].data.length).toBe(2);
+ });
+});
diff --git a/packages/twenty-front/src/modules/analytics/utils/computeAnalyticsGraphDataFunction.ts b/packages/twenty-front/src/modules/analytics/utils/computeAnalyticsGraphDataFunction.ts
new file mode 100644
index 000000000000..e614e2ef28c0
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/computeAnalyticsGraphDataFunction.ts
@@ -0,0 +1,25 @@
+import { mapServerlessFunctionDurationToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionDurationToNivoLineInput';
+import { mapServerlessFunctionErrorsToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput';
+import { mapWebhookAnalyticsResultToNivoLineInput } from '@/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput';
+import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';
+
+export const computeAnalyticsGraphDataFunction = (
+ endpointName: keyof AnalyticsTinybirdJwtMap,
+) => {
+ switch (endpointName) {
+ case 'getWebhookAnalytics':
+ return mapWebhookAnalyticsResultToNivoLineInput;
+ case 'getServerlessFunctionDuration':
+ return mapServerlessFunctionDurationToNivoLineInput;
+ case 'getServerlessFunctionErrorCount':
+ return (data: { start: string; error_count: number }[]) =>
+ mapServerlessFunctionErrorsToNivoLineInput(data, 'ErrorCount');
+ case 'getServerlessFunctionSuccessRate':
+ return (data: { start: string; success_rate: number }[]) =>
+ mapServerlessFunctionErrorsToNivoLineInput(data, 'SuccessRate');
+ default:
+ throw new Error(
+ `No analytics function found associated with endpoint "${endpointName}"`,
+ );
+ }
+};
diff --git a/packages/twenty-front/src/modules/analytics/utils/computeStartEndDate.ts b/packages/twenty-front/src/modules/analytics/utils/computeStartEndDate.ts
new file mode 100644
index 000000000000..3e1801650745
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/computeStartEndDate.ts
@@ -0,0 +1,30 @@
+import { subDays, subHours } from 'date-fns';
+
+export const computeStartEndDate = (
+ windowLength: '7D' | '1D' | '12H' | '4H',
+) => {
+ const now = new Date(Date.now());
+ const end = now.toISOString();
+ switch (windowLength) {
+ case '7D':
+ return {
+ start: subDays(now, 7).toISOString(),
+ end,
+ };
+ case '1D':
+ return {
+ start: subDays(now, 1).toISOString(),
+ end,
+ };
+ case '12H':
+ return {
+ start: subHours(now, 12).toISOString(),
+ end,
+ };
+ case '4H':
+ return {
+ start: subHours(now, 4).toISOString(),
+ end,
+ };
+ }
+};
diff --git a/packages/twenty-front/src/modules/analytics/utils/fetchGraphDataOrThrow.ts b/packages/twenty-front/src/modules/analytics/utils/fetchGraphDataOrThrow.ts
new file mode 100644
index 000000000000..a3b881bbab7c
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/fetchGraphDataOrThrow.ts
@@ -0,0 +1,38 @@
+import { ANALYTICS_ENDPOINT_TYPE_MAP } from '@/analytics/constants/AnalyticsEndpointTypeMap';
+import { ANALYTICS_GRAPH_OPTION_MAP } from '@/analytics/constants/AnalyticsGraphOptionMap';
+import { AnalyticsComponentProps } from '@/analytics/types/AnalyticsComponentProps';
+import { computeStartEndDate } from '@/analytics/utils/computeStartEndDate';
+
+type fetchGraphDataOrThrowProps = AnalyticsComponentProps & {
+ windowLength: '7D' | '1D' | '12H' | '4H';
+ tinybirdJwt: string;
+};
+
+export const fetchGraphDataOrThrow = async ({
+ recordId,
+ windowLength,
+ tinybirdJwt,
+ endpointName,
+}: fetchGraphDataOrThrowProps) => {
+ const recordType = ANALYTICS_ENDPOINT_TYPE_MAP[endpointName];
+ const queryString = new URLSearchParams({
+ ...ANALYTICS_GRAPH_OPTION_MAP[windowLength],
+ ...computeStartEndDate(windowLength),
+ ...{ [`${recordType}Id`]: recordId },
+ }).toString();
+
+ const response = await fetch(
+ `https://api.eu-central-1.aws.tinybird.co/v0/pipes/${endpointName}.json?${queryString}`,
+ {
+ headers: {
+ Authorization: 'Bearer ' + tinybirdJwt,
+ },
+ },
+ );
+ const result = await response.json();
+
+ if (!response.ok) {
+ throw new Error(result.error);
+ }
+ return result.data;
+};
diff --git a/packages/twenty-front/src/modules/analytics/utils/mapServerlessFunctionDurationToNivoLineInput.ts b/packages/twenty-front/src/modules/analytics/utils/mapServerlessFunctionDurationToNivoLineInput.ts
new file mode 100644
index 000000000000..4b8b94e6a074
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/mapServerlessFunctionDurationToNivoLineInput.ts
@@ -0,0 +1,49 @@
+import { NivoLineInput } from '@/analytics/types/NivoLineInput';
+//DOING: Adding the servelessFunctionDurationGraph to twenty
+export const mapServerlessFunctionDurationToNivoLineInput = (
+ serverlessFunctionDurationResult: {
+ start: string;
+ minimum: number;
+ maximum: number;
+ average: number;
+ }[],
+): NivoLineInput[] => {
+ return serverlessFunctionDurationResult
+ .flatMap((dataRow) => [
+ {
+ x: new Date(dataRow.start),
+ y: dataRow.maximum,
+ id: 'Maximum',
+ },
+ {
+ x: new Date(dataRow.start),
+ y: dataRow.minimum,
+ id: 'Minimum',
+ },
+ {
+ x: new Date(dataRow.start),
+ y: dataRow.average,
+ id: 'Average',
+ },
+ ])
+ .reduce(
+ (
+ acc: NivoLineInput[],
+ { id, x, y }: { id: string; x: Date; y: number },
+ ) => {
+ const existingGroupIndex = acc.findIndex((group) => group.id === id);
+ const isExistingGroup = existingGroupIndex !== -1;
+
+ if (isExistingGroup) {
+ return acc.map((group, index) =>
+ index === existingGroupIndex
+ ? { ...group, data: [...group.data, { x, y }] }
+ : group,
+ );
+ } else {
+ return [...acc, { id, data: [{ x, y }] }];
+ }
+ },
+ [],
+ );
+};
diff --git a/packages/twenty-front/src/modules/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput.ts b/packages/twenty-front/src/modules/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput.ts
new file mode 100644
index 000000000000..2e18df1c70f6
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput.ts
@@ -0,0 +1,26 @@
+import { NivoLineInput } from '@/analytics/types/NivoLineInput';
+
+export const mapServerlessFunctionErrorsToNivoLineInput = <
+ T extends { start: string },
+>(
+ serverlessFunctionResult: (T & {
+ error_count?: number;
+ success_rate?: number;
+ })[],
+ type: 'ErrorCount' | 'SuccessRate',
+): NivoLineInput[] => {
+ return [
+ {
+ id: type === 'ErrorCount' ? 'Error' : 'Success Rate',
+ data: serverlessFunctionResult.flatMap((dataRow) => [
+ {
+ x: new Date(dataRow.start),
+ y:
+ type === 'ErrorCount'
+ ? (dataRow.error_count ?? 0)
+ : (dataRow.success_rate ?? 0),
+ },
+ ]),
+ },
+ ];
+};
diff --git a/packages/twenty-front/src/modules/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput.ts b/packages/twenty-front/src/modules/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput.ts
new file mode 100644
index 000000000000..845d861340e5
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput.ts
@@ -0,0 +1,43 @@
+import { NivoLineInput } from '@/analytics/types/NivoLineInput';
+
+export const mapWebhookAnalyticsResultToNivoLineInput = (
+ webhookAnalyticsResult: {
+ start: string;
+ failure_count: number;
+ success_count: number;
+ }[],
+): NivoLineInput[] => {
+ return webhookAnalyticsResult
+ .flatMap((dataRow) => [
+ {
+ x: new Date(dataRow.start),
+ y: dataRow.failure_count,
+ id: 'Failed',
+ },
+ {
+ x: new Date(dataRow.start),
+ y: dataRow.success_count,
+ id: 'Succeeded',
+ },
+ ])
+ .reduce(
+ (
+ acc: NivoLineInput[],
+ { id, x, y }: { id: string; x: Date; y: number },
+ ) => {
+ const existingGroupIndex = acc.findIndex((group) => group.id === id);
+ const isExistingGroup = existingGroupIndex !== -1;
+
+ if (isExistingGroup) {
+ return acc.map((group, index) =>
+ index === existingGroupIndex
+ ? { ...group, data: [...group.data, { x, y }] }
+ : group,
+ );
+ } else {
+ return [...acc, { id, data: [{ x, y }] }];
+ }
+ },
+ [],
+ );
+};
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/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/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