mirror of
https://github.com/lingble/twenty.git
synced 2025-10-31 20:57:55 +00:00
Merge branch 'main' into horizontal-nav
This commit is contained in:
1
.github/workflows/cd-deploy-main.yaml
vendored
1
.github/workflows/cd-deploy-main.yaml
vendored
@@ -5,6 +5,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
deploy-main:
|
deploy-main:
|
||||||
|
timeout-minutes: 3
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Repository Dispatch
|
- name: Repository Dispatch
|
||||||
|
|||||||
1
.github/workflows/cd-deploy-tag.yaml
vendored
1
.github/workflows/cd-deploy-tag.yaml
vendored
@@ -5,6 +5,7 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
jobs:
|
jobs:
|
||||||
deploy-tag:
|
deploy-tag:
|
||||||
|
timeout-minutes: 3
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Repository Dispatch
|
- name: Repository Dispatch
|
||||||
|
|||||||
1
.github/workflows/ci-chrome-extension.yaml
vendored
1
.github/workflows/ci-chrome-extension.yaml
vendored
@@ -12,6 +12,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
chrome-extension-build:
|
chrome-extension-build:
|
||||||
|
timeout-minutes: 15
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
VITE_SERVER_BASE_URL: http://localhost:3000
|
VITE_SERVER_BASE_URL: http://localhost:3000
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
name: Playwright Tests
|
name: CI E2E Tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main, master ]
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main, master ]
|
branches:
|
||||||
|
- '**'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version: lts/*
|
||||||
@@ -19,16 +28,16 @@ jobs:
|
|||||||
uses: tj-actions/changed-files@v11
|
uses: tj-actions/changed-files@v11
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
packages/** # Adjust this to your relevant directories
|
packages/**
|
||||||
playwright.config.ts # Include any relevant config files
|
playwright.config.ts
|
||||||
|
|
||||||
- name: Skip if no relevant changes
|
- name: Skip if no relevant changes
|
||||||
if: steps.changed-files.outputs.any_changed != 'true'
|
if: steps.changed-files.outputs.any_changed == 'false'
|
||||||
run: echo "No relevant changes detected. Marking as valid."
|
run: echo "No relevant changes detected. Marking as valid."
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: npm install -g yarn && yarn
|
uses: ./.github/workflows/actions/yarn-install
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
7
.github/workflows/ci-front.yaml
vendored
7
.github/workflows/ci-front.yaml
vendored
@@ -12,6 +12,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
front-sb-build:
|
front-sb-build:
|
||||||
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||||
@@ -58,8 +59,8 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: npx nx storybook:build twenty-front
|
run: npx nx storybook:build twenty-front
|
||||||
front-sb-test:
|
front-sb-test:
|
||||||
|
timeout-minutes: 30
|
||||||
runs-on: shipfox-8vcpu-ubuntu-2204
|
runs-on: shipfox-8vcpu-ubuntu-2204
|
||||||
timeout-minutes: 60
|
|
||||||
needs: front-sb-build
|
needs: front-sb-build
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -101,8 +102,8 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }}
|
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }}
|
||||||
front-sb-test-performance:
|
front-sb-test-performance:
|
||||||
|
timeout-minutes: 30
|
||||||
runs-on: shipfox-8vcpu-ubuntu-2204
|
runs-on: shipfox-8vcpu-ubuntu-2204
|
||||||
timeout-minutes: 60
|
|
||||||
env:
|
env:
|
||||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||||
@@ -135,6 +136,7 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: npx nx run twenty-front:storybook:serve-and-test:static:performance
|
run: npx nx run twenty-front:storybook:serve-and-test:static:performance
|
||||||
front-chromatic-deployment:
|
front-chromatic-deployment:
|
||||||
|
timeout-minutes: 30
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push'
|
if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push'
|
||||||
needs: front-sb-build
|
needs: front-sb-build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -177,6 +179,7 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: npx nx run twenty-front:chromatic:ci
|
run: npx nx run twenty-front:chromatic:ci
|
||||||
front-task:
|
front-task:
|
||||||
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||||
|
|||||||
1
.github/workflows/ci-release-create.yaml
vendored
1
.github/workflows/ci-release-create.yaml
vendored
@@ -15,6 +15,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_pr:
|
create_pr:
|
||||||
|
timeout-minutes: 10
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
1
.github/workflows/ci-release-merge.yaml
vendored
1
.github/workflows/ci-release-merge.yaml
vendored
@@ -6,6 +6,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tag_and_release:
|
tag_and_release:
|
||||||
|
timeout-minutes: 10
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
|
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
36
.github/workflows/ci-server.yaml
vendored
36
.github/workflows/ci-server.yaml
vendored
@@ -12,17 +12,25 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
server-setup:
|
server-setup:
|
||||||
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: twentycrm/twenty-postgres
|
image: twentycrm/twenty-postgres-spilo
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: postgres
|
PGUSER_SUPERUSER: postgres
|
||||||
POSTGRES_USER: postgres
|
PGPASSWORD_SUPERUSER: twenty
|
||||||
|
ALLOW_NOSSL: "true"
|
||||||
|
SPILO_PROVIDER: "local"
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis
|
||||||
ports:
|
ports:
|
||||||
@@ -62,11 +70,17 @@ jobs:
|
|||||||
- name: Server / Write .env
|
- name: Server / Write .env
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: npx nx reset:env twenty-server
|
run: npx nx reset:env twenty-server
|
||||||
|
- name: Server / Create DB
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||||
|
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||||
- name: Worker / Run
|
- name: Worker / Run
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: npx nx run twenty-server:worker:ci
|
run: npx nx run twenty-server:worker:ci
|
||||||
|
|
||||||
server-test:
|
server-test:
|
||||||
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: server-setup
|
needs: server-setup
|
||||||
env:
|
env:
|
||||||
@@ -102,16 +116,24 @@ jobs:
|
|||||||
tasks: test
|
tasks: test
|
||||||
|
|
||||||
server-integration-test:
|
server-integration-test:
|
||||||
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: server-setup
|
needs: server-setup
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: twentycrm/twenty-postgres
|
image: twentycrm/twenty-postgres-spilo
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: postgres
|
PGUSER_SUPERUSER: postgres
|
||||||
POSTGRES_USER: postgres
|
PGPASSWORD_SUPERUSER: twenty
|
||||||
|
ALLOW_NOSSL: "true"
|
||||||
|
SPILO_PROVIDER: "local"
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis
|
||||||
ports:
|
ports:
|
||||||
@@ -146,7 +168,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/actions/nx-affected
|
uses: ./.github/workflows/actions/nx-affected
|
||||||
with:
|
with:
|
||||||
tag: scope:backend
|
tag: scope:backend
|
||||||
tasks: "test:integration"
|
tasks: "test:integration:with-db-reset"
|
||||||
- name: Server / Upload reset-logs file
|
- name: Server / Upload reset-logs file
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
36
.github/workflows/ci-test-docker-compose.yaml
vendored
36
.github/workflows/ci-test-docker-compose.yaml
vendored
@@ -8,6 +8,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -35,20 +36,42 @@ jobs:
|
|||||||
|
|
||||||
yq eval 'del(.services.db.image)' -i docker-compose.yml
|
yq eval 'del(.services.db.image)' -i docker-compose.yml
|
||||||
yq eval '.services.db.build.context = "../../"' -i docker-compose.yml
|
yq eval '.services.db.build.context = "../../"' -i docker-compose.yml
|
||||||
yq eval '.services.db.build.dockerfile = "./packages/twenty-docker/twenty-postgres/Dockerfile"' -i docker-compose.yml
|
yq eval '.services.db.build.dockerfile = "./packages/twenty-docker/twenty-postgres-spilo/Dockerfile"' -i docker-compose.yml
|
||||||
|
|
||||||
echo "Setting up .env file..."
|
echo "Setting up .env file..."
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
echo "Generating secrets..."
|
echo "Generating secrets..."
|
||||||
echo "# === Randomly generated secrets ===" >>.env
|
echo "# === Randomly generated secrets ===" >>.env
|
||||||
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
||||||
echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env
|
echo "PGPASSWORD_SUPERUSER=$(openssl rand -base64 32)" >>.env
|
||||||
|
|
||||||
echo "Starting server..."
|
echo "Docker compose up..."
|
||||||
docker compose up -d
|
docker compose up -d || {
|
||||||
|
echo "Docker compose failed to start"
|
||||||
|
docker compose logs
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
docker compose logs db server -f &
|
docker compose logs db server -f &
|
||||||
pid=$!
|
pid=$!
|
||||||
|
|
||||||
|
echo "Waiting for database to start..."
|
||||||
|
count=0
|
||||||
|
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ]; do
|
||||||
|
sleep 1;
|
||||||
|
count=$((count+1));
|
||||||
|
if [ $(docker inspect --format='{{.State.Status}}' twenty-db-1) = "exited" ]; then
|
||||||
|
echo "Database exited"
|
||||||
|
docker compose logs db
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ $count -gt 300 ]; then
|
||||||
|
echo "Failed to start database after 5 minutes"
|
||||||
|
docker compose logs db
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Still waiting for database... (${count}/60)"
|
||||||
|
done
|
||||||
|
|
||||||
echo "Waiting for server to start..."
|
echo "Waiting for server to start..."
|
||||||
count=0
|
count=0
|
||||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||||
@@ -56,11 +79,14 @@ jobs:
|
|||||||
count=$((count+1));
|
count=$((count+1));
|
||||||
if [ $(docker inspect --format='{{.State.Status}}' twenty-server-1) = "exited" ]; then
|
if [ $(docker inspect --format='{{.State.Status}}' twenty-server-1) = "exited" ]; then
|
||||||
echo "Server exited"
|
echo "Server exited"
|
||||||
|
docker compose logs server
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ $count -gt 300 ]; then
|
if [ $count -gt 300 ]; then
|
||||||
echo "Failed to start server"
|
echo "Failed to start server after 5 minutes"
|
||||||
|
docker compose logs server
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
echo "Still waiting for server... (${count}/300s)"
|
||||||
done
|
done
|
||||||
working-directory: ./packages/twenty-docker/
|
working-directory: ./packages/twenty-docker/
|
||||||
|
|||||||
22
.github/workflows/ci-tinybird.yaml
vendored
22
.github/workflows/ci-tinybird.yaml
vendored
@@ -3,8 +3,14 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- 'package.json'
|
||||||
|
- 'packages/twenty-tinybird/**'
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'package.json'
|
||||||
|
- 'packages/twenty-tinybird/**'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -12,23 +18,9 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check for changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@v11
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
package.json
|
|
||||||
packages/twenty-tinybird/**
|
|
||||||
|
|
||||||
- name: Skip if no relevant changes
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'false'
|
|
||||||
run: echo "No relevant changes. Skipping CI."
|
|
||||||
|
|
||||||
- name: Check twenty-tinybird package
|
|
||||||
uses: tinybirdco/ci/.github/workflows/ci.yml@main
|
uses: tinybirdco/ci/.github/workflows/ci.yml@main
|
||||||
with:
|
with:
|
||||||
data_project_dir: packages/twenty-tinybird
|
data_project_dir: packages/twenty-tinybird
|
||||||
|
secrets:
|
||||||
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
|
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
|
||||||
tb_host: https://api.eu-central-1.aws.tinybird.co
|
tb_host: https://api.eu-central-1.aws.tinybird.co
|
||||||
|
|||||||
6
.github/workflows/ci-utils.yaml
vendored
6
.github/workflows/ci-utils.yaml
vendored
@@ -15,10 +15,13 @@ permissions:
|
|||||||
statuses: write
|
statuses: write
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
# We don't cancel in-progress because this workflow is triggered on
|
||||||
|
# pull_request_target, which means the ref can be the same accross two PRs.
|
||||||
|
# cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
danger-js:
|
danger-js:
|
||||||
|
timeout-minutes: 3
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.action != 'closed'
|
if: github.event.action != 'closed'
|
||||||
steps:
|
steps:
|
||||||
@@ -31,6 +34,7 @@ jobs:
|
|||||||
DANGER_GITHUB_API_TOKEN: ${{ github.token }}
|
DANGER_GITHUB_API_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
congratulate:
|
congratulate:
|
||||||
|
timeout-minutes: 3
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.action == 'closed' && github.event.pull_request.merged == true
|
if: github.event.action == 'closed' && github.event.pull_request.merged == true
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
22
.github/workflows/ci-website.yaml
vendored
22
.github/workflows/ci-website.yaml
vendored
@@ -13,15 +13,23 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
website-build:
|
website-build:
|
||||||
|
timeout-minutes: 3
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: twentycrm/twenty-postgres
|
image: twentycrm/twenty-postgres-spilo
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: twenty
|
PGUSER_SUPERUSER: postgres
|
||||||
POSTGRES_USER: twenty
|
PGPASSWORD_SUPERUSER: twenty
|
||||||
|
ALLOW_NOSSL: "true"
|
||||||
|
SPILO_PROVIDER: "local"
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -36,16 +44,20 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.changed == 'true'
|
if: steps.changed-files.outputs.changed == 'true'
|
||||||
uses: ./.github/workflows/actions/yarn-install
|
uses: ./.github/workflows/actions/yarn-install
|
||||||
|
|
||||||
|
- name: Server / Create DB
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||||
|
|
||||||
- name: Website / Run migrations
|
- name: Website / Run migrations
|
||||||
if: steps.changed-files.outputs.changed == 'true'
|
if: steps.changed-files.outputs.changed == 'true'
|
||||||
run: npx nx database:migrate twenty-website
|
run: npx nx database:migrate twenty-website
|
||||||
env:
|
env:
|
||||||
DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default
|
DATABASE_PG_URL: postgres://postgres:twenty@localhost:5432/default
|
||||||
- name: Website / Build Website
|
- name: Website / Build Website
|
||||||
if: steps.changed-files.outputs.changed == 'true'
|
if: steps.changed-files.outputs.changed == 'true'
|
||||||
run: npx nx build twenty-website
|
run: npx nx build twenty-website
|
||||||
env:
|
env:
|
||||||
DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default
|
DATABASE_PG_URL: postgres://postgres:twenty@localhost:5432/default
|
||||||
|
|
||||||
- name: Mark as VALID
|
- name: Mark as VALID
|
||||||
if: steps.changed-files.outputs.changed != 'true' # If no changes, mark as valid
|
if: steps.changed-files.outputs.changed != 'true' # If no changes, mark as valid
|
||||||
|
|||||||
4
.vscode/twenty.code-workspace
vendored
4
.vscode/twenty.code-workspace
vendored
@@ -24,10 +24,6 @@
|
|||||||
"name": "packages/twenty-emails",
|
"name": "packages/twenty-emails",
|
||||||
"path": "../packages/twenty-emails"
|
"path": "../packages/twenty-emails"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "packages/twenty-postgres",
|
|
||||||
"path": "../packages/twenty-postgres"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "packages/twenty-server",
|
"name": "packages/twenty-server",
|
||||||
"path": "../packages/twenty-server"
|
"path": "../packages/twenty-server"
|
||||||
|
|||||||
22
Makefile
22
Makefile
@@ -1,12 +1,20 @@
|
|||||||
postgres-on-docker:
|
postgres-on-docker:
|
||||||
docker run \
|
docker run -d \
|
||||||
--name twenty_postgres \
|
--name twenty_pg \
|
||||||
-e POSTGRES_USER=postgres \
|
-e PGUSER_SUPERUSER=postgres \
|
||||||
-e POSTGRES_PASSWORD=postgres \
|
-e PGPASSWORD_SUPERUSER=twenty \
|
||||||
-e POSTGRES_DB=default \
|
-e ALLOW_NOSSL=true \
|
||||||
-v twenty_db_data:/var/lib/postgresql/data \
|
-v twenty_db_data:/home/postgres/pgdata \
|
||||||
-p 5432:5432 \
|
-p 5432:5432 \
|
||||||
twentycrm/twenty-postgres:latest
|
twentycrm/twenty-postgres-spilo:latest
|
||||||
|
@echo "Waiting for PostgreSQL to be ready..."
|
||||||
|
@until PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres \
|
||||||
|
-c 'SELECT pg_is_in_recovery();' 2>/dev/null | grep -q 'f'; do \
|
||||||
|
sleep 1; \
|
||||||
|
done
|
||||||
|
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres \
|
||||||
|
-c "CREATE DATABASE \"default\" WITH OWNER postgres;" \
|
||||||
|
-c "CREATE DATABASE \"test\" WITH OWNER postgres;"
|
||||||
|
|
||||||
redis-on-docker:
|
redis-on-docker:
|
||||||
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
|
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
|
||||||
@@ -93,7 +93,7 @@ fi
|
|||||||
echo "# === Randomly generated secrets ===" >>.env
|
echo "# === Randomly generated secrets ===" >>.env
|
||||||
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
||||||
echo "" >>.env
|
echo "" >>.env
|
||||||
echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env
|
echo "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
|
||||||
|
|
||||||
echo -e "\t• .env configuration completed"
|
echo -e "\t• .env configuration completed"
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@linaria/core": "^6.2.0",
|
"@linaria/core": "^6.2.0",
|
||||||
"@linaria/react": "^6.2.1",
|
"@linaria/react": "^6.2.1",
|
||||||
"@mdx-js/react": "^3.0.0",
|
"@mdx-js/react": "^3.0.0",
|
||||||
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
"@nestjs/apollo": "^11.0.5",
|
"@nestjs/apollo": "^11.0.5",
|
||||||
"@nestjs/axios": "^3.0.1",
|
"@nestjs/axios": "^3.0.1",
|
||||||
"@nestjs/cli": "^9.0.0",
|
"@nestjs/cli": "^9.0.0",
|
||||||
@@ -201,6 +202,7 @@
|
|||||||
"@graphql-codegen/typescript": "^3.0.4",
|
"@graphql-codegen/typescript": "^3.0.4",
|
||||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||||
|
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||||
"@nestjs/cli": "^9.0.0",
|
"@nestjs/cli": "^9.0.0",
|
||||||
"@nestjs/schematics": "^9.0.0",
|
"@nestjs/schematics": "^9.0.0",
|
||||||
"@nestjs/testing": "^9.0.0",
|
"@nestjs/testing": "^9.0.0",
|
||||||
@@ -350,7 +352,7 @@
|
|||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"nx": {},
|
"nx": {},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx nx run-many -t start worker -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": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
type H2TitleProps = {
|
type H2TitleProps = {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
addornment?: React.ReactNode;
|
adornment?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@@ -33,11 +33,11 @@ const StyledDescription = styled.h3`
|
|||||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const H2Title = ({ title, description, addornment }: H2TitleProps) => (
|
export const H2Title = ({ title, description, adornment }: H2TitleProps) => (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledTitleContainer>
|
<StyledTitleContainer>
|
||||||
<StyledTitle>{title}</StyledTitle>
|
<StyledTitle>{title}</StyledTitle>
|
||||||
{addornment}
|
{adornment}
|
||||||
</StyledTitleContainer>
|
</StyledTitleContainer>
|
||||||
{description && <StyledDescription>{description}</StyledDescription>}
|
{description && <StyledDescription>{description}</StyledDescription>}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
TAG=latest
|
TAG=latest
|
||||||
|
|
||||||
# POSTGRES_ADMIN_PASSWORD=replace_me_with_a_strong_password
|
#PGUSER_SUPERUSER=postgres
|
||||||
|
#PGPASSWORD_SUPERUSER=replace_me_with_a_strong_password
|
||||||
|
|
||||||
PG_DATABASE_HOST=db:5432
|
PG_DATABASE_HOST=db:5432
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ prod-build:
|
|||||||
prod-run:
|
prod-run:
|
||||||
@docker run -d -p 3000:3000 --name twenty twenty
|
@docker run -d -p 3000:3000 --name twenty twenty
|
||||||
|
|
||||||
prod-postgres-build:
|
|
||||||
@cd ../.. && docker build -f ./packages/twenty-docker/twenty-postgres/Dockerfile --tag twenty-postgres . && cd -
|
|
||||||
|
|
||||||
prod-postgres-run:
|
prod-postgres-run:
|
||||||
@docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres
|
@docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres
|
||||||
|
|
||||||
@@ -15,11 +12,3 @@ prod-website-build:
|
|||||||
|
|
||||||
prod-website-run:
|
prod-website-run:
|
||||||
@docker run -d -p 3000:3000 --name twenty-website twenty-website
|
@docker run -d -p 3000:3000 --name twenty-website twenty-website
|
||||||
|
|
||||||
release-postgres:
|
|
||||||
@cd ../.. && docker buildx build \
|
|
||||||
--push \
|
|
||||||
--no-cache \
|
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
-f ./packages/twenty-docker/twenty-postgres/Dockerfile -t twentycrm/twenty-postgres:$(version) -t twentycrm/twenty-postgres:latest . \
|
|
||||||
&& cd -
|
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ services:
|
|||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default
|
PG_DATABASE_URL: postgres://${PGUSER_SUPERUSER:-postgres}:${PGPASSWORD_SUPERUSER:-twenty}@${PG_DATABASE_HOST:-db:5432}/default
|
||||||
SERVER_URL: ${SERVER_URL}
|
SERVER_URL: ${SERVER_URL}
|
||||||
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
||||||
REDIS_URL: ${REDIS_URL:-redis://localhost:6379}
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||||
|
|
||||||
ENABLE_DB_MIGRATIONS: "true"
|
ENABLE_DB_MIGRATIONS: "true"
|
||||||
|
|
||||||
@@ -52,10 +52,10 @@ services:
|
|||||||
image: twentycrm/twenty:${TAG}
|
image: twentycrm/twenty:${TAG}
|
||||||
command: ["yarn", "worker:prod"]
|
command: ["yarn", "worker:prod"]
|
||||||
environment:
|
environment:
|
||||||
PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default
|
PG_DATABASE_URL: postgres://${PGUSER_SUPERUSER:-postgres}:${PGPASSWORD_SUPERUSER:-twenty}@${PG_DATABASE_HOST:-db:5432}/default
|
||||||
SERVER_URL: ${SERVER_URL}
|
SERVER_URL: ${SERVER_URL}
|
||||||
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
||||||
REDIS_URL: ${REDIS_URL:-redis://localhost:6379}
|
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||||
|
|
||||||
ENABLE_DB_MIGRATIONS: "false" # it already runs on the server
|
ENABLE_DB_MIGRATIONS: "false" # it already runs on the server
|
||||||
|
|
||||||
@@ -73,13 +73,16 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: twentycrm/twenty-postgres:${TAG}
|
image: twentycrm/twenty-postgres-spilo:${TAG}
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/bitnami/postgresql
|
- db-data:/home/postgres/pgdata
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD}
|
PGUSER_SUPERUSER: ${PGUSER_SUPERUSER:-postgres}
|
||||||
|
PGPASSWORD_SUPERUSER: ${PGPASSWORD_SUPERUSER:-twenty}
|
||||||
|
ALLOW_NOSSL: "true"
|
||||||
|
SPILO_PROVIDER: "local"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: pg_isready -U twenty -d default
|
test: pg_isready -U ${PGUSER_SUPERUSER:-postgres} -h localhost -d postgres
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ spec:
|
|||||||
image: twentycrm/twenty-postgres:latest
|
image: twentycrm/twenty-postgres:latest
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
env:
|
env:
|
||||||
- name: POSTGRES_PASSWORD
|
- name: PGUSER_SUPERUSER
|
||||||
|
value: "postgres"
|
||||||
|
- name: PGPASSWORD_SUPERUSER
|
||||||
value: "twenty"
|
value: "twenty"
|
||||||
- name: BITNAMI_DEBUG
|
|
||||||
value: "true"
|
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5432
|
- containerPort: 5432
|
||||||
name: tcp
|
name: tcp
|
||||||
@@ -48,7 +48,7 @@ spec:
|
|||||||
stdin: true
|
stdin: true
|
||||||
tty: true
|
tty: true
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /bitnami/postgresql
|
- mountPath: /home/postgres/pgdata
|
||||||
name: twentycrm-db-data
|
name: twentycrm-db-data
|
||||||
dnsPolicy: ClusterFirst
|
dnsPolicy: ClusterFirst
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ spec:
|
|||||||
- name: FRONT_BASE_URL
|
- name: FRONT_BASE_URL
|
||||||
value: "https://crm.example.com:443"
|
value: "https://crm.example.com:443"
|
||||||
- name: "PG_DATABASE_URL"
|
- name: "PG_DATABASE_URL"
|
||||||
value: "postgres://twenty:twenty@twenty-db.twentycrm.svc.cluster.local/default"
|
value: "postgres://postgres:twenty@twenty-db.twentycrm.svc.cluster.local/default"
|
||||||
- name: "REDIS_URL"
|
- name: "REDIS_URL"
|
||||||
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
|
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
|
||||||
- name: ENABLE_DB_MIGRATIONS
|
- name: ENABLE_DB_MIGRATIONS
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ spec:
|
|||||||
- name: FRONT_BASE_URL
|
- name: FRONT_BASE_URL
|
||||||
value: "https://crm.example.com:443"
|
value: "https://crm.example.com:443"
|
||||||
- name: PG_DATABASE_URL
|
- name: PG_DATABASE_URL
|
||||||
value: "postgres://twenty:twenty@twenty-db.twentycrm.svc.cluster.local/default"
|
value: "postgres://postgres:twenty@twenty-db.twentycrm.svc.cluster.local/default"
|
||||||
- name: ENABLE_DB_MIGRATIONS
|
- name: ENABLE_DB_MIGRATIONS
|
||||||
value: "false" # it already runs on the server
|
value: "false" # it already runs on the server
|
||||||
- name: STORAGE_TYPE
|
- name: STORAGE_TYPE
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ To make configuration changes to how this doc is generated, see `./.terraform-do
|
|||||||
| <a name="input_twentycrm_app_hostname"></a> [twentycrm\_app\_hostname](#input\_twentycrm\_app\_hostname) | The protocol, DNS fully qualified hostname, and port used to access TwentyCRM in your environment. Ex: https://crm.example.com:443 | `string` | n/a | yes |
|
| <a name="input_twentycrm_app_hostname"></a> [twentycrm\_app\_hostname](#input\_twentycrm\_app\_hostname) | The protocol, DNS fully qualified hostname, and port used to access TwentyCRM in your environment. Ex: https://crm.example.com:443 | `string` | n/a | yes |
|
||||||
| <a name="input_twentycrm_pgdb_admin_password"></a> [twentycrm\_pgdb\_admin\_password](#input\_twentycrm\_pgdb\_admin\_password) | TwentyCRM password for postgres database. | `string` | n/a | yes |
|
| <a name="input_twentycrm_pgdb_admin_password"></a> [twentycrm\_pgdb\_admin\_password](#input\_twentycrm\_pgdb\_admin\_password) | TwentyCRM password for postgres database. | `string` | n/a | yes |
|
||||||
| <a name="input_twentycrm_app_name"></a> [twentycrm\_app\_name](#input\_twentycrm\_app\_name) | A friendly name prefix to use for every component deployed. | `string` | `"twentycrm"` | no |
|
| <a name="input_twentycrm_app_name"></a> [twentycrm\_app\_name](#input\_twentycrm\_app\_name) | A friendly name prefix to use for every component deployed. | `string` | `"twentycrm"` | no |
|
||||||
| <a name="input_twentycrm_db_image"></a> [twentycrm\_db\_image](#input\_twentycrm\_db\_image) | TwentyCRM image for database deployment. This defaults to latest. | `string` | `"twentycrm/twenty-postgres:latest"` | no |
|
| <a name="input_twentycrm_db_image"></a> [twentycrm\_db\_image](#input\_twentycrm\_db\_image) | TwentyCRM image for database deployment. This defaults to latest. | `string` | `"twentycrm/twenty-postgres-spilo:latest"` | no |
|
||||||
| <a name="input_twentycrm_db_pv_capacity"></a> [twentycrm\_db\_pv\_capacity](#input\_twentycrm\_db\_pv\_capacity) | Storage capacity provisioned for database persistent volume. | `string` | `"10Gi"` | no |
|
| <a name="input_twentycrm_db_pv_capacity"></a> [twentycrm\_db\_pv\_capacity](#input\_twentycrm\_db\_pv\_capacity) | Storage capacity provisioned for database persistent volume. | `string` | `"10Gi"` | no |
|
||||||
| <a name="input_twentycrm_db_pv_path"></a> [twentycrm\_db\_pv\_path](#input\_twentycrm\_db\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no |
|
| <a name="input_twentycrm_db_pv_path"></a> [twentycrm\_db\_pv\_path](#input\_twentycrm\_db\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no |
|
||||||
| <a name="input_twentycrm_db_pvc_requests"></a> [twentycrm\_db\_pvc\_requests](#input\_twentycrm\_db\_pvc\_requests) | Storage capacity reservation for database persistent volume claim. | `string` | `"10Gi"` | no |
|
| <a name="input_twentycrm_db_pvc_requests"></a> [twentycrm\_db\_pvc\_requests](#input\_twentycrm\_db\_pvc\_requests) | Storage capacity reservation for database persistent volume claim. | `string` | `"10Gi"` | no |
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ variable "twentycrm_server_image" {
|
|||||||
|
|
||||||
variable "twentycrm_db_image" {
|
variable "twentycrm_db_image" {
|
||||||
type = string
|
type = string
|
||||||
default = "twentycrm/twenty-postgres:latest"
|
default = "twentycrm/twenty-postgres-spilo:latest"
|
||||||
description = "TwentyCRM image for database deployment. This defaults to latest."
|
description = "TwentyCRM image for database deployment. This defaults to latest."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ ARG SPILO_VERSION=3.2-p1
|
|||||||
ARG WRAPPERS_VERSION=0.2.0
|
ARG WRAPPERS_VERSION=0.2.0
|
||||||
|
|
||||||
# Build the mysql_fdw extension
|
# Build the mysql_fdw extension
|
||||||
FROM debian:bookworm as build-mysql_fdw
|
FROM debian:bookworm AS build-mysql_fdw
|
||||||
ARG POSTGRES_VERSION
|
ARG POSTGRES_VERSION
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt install -y \
|
apt install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
@@ -17,14 +17,14 @@ RUN apt update && \
|
|||||||
|
|
||||||
# Install mysql_fdw
|
# Install mysql_fdw
|
||||||
RUN git clone https://github.com/EnterpriseDB/mysql_fdw.git
|
RUN git clone https://github.com/EnterpriseDB/mysql_fdw.git
|
||||||
WORKDIR mysql_fdw
|
WORKDIR /mysql_fdw
|
||||||
RUN make USE_PGXS=1
|
RUN make USE_PGXS=1
|
||||||
|
|
||||||
|
|
||||||
# Build libssl for wrappers
|
# Build libssl for wrappers
|
||||||
FROM ubuntu:22.04 as build-libssl
|
FROM ubuntu:22.04 AS build-libssl
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt install -y \
|
apt install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
ARG IMAGE_TAG='15.5.0-debian-11-r15'
|
|
||||||
|
|
||||||
FROM bitnami/postgresql:${IMAGE_TAG}
|
|
||||||
|
|
||||||
ARG PG_MAIN_VERSION=15
|
|
||||||
ARG WRAPPERS_VERSION=0.2.0
|
|
||||||
ARG TARGETARCH
|
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
|
||||||
case "${ARCH}" in \
|
|
||||||
aarch64|arm64) \
|
|
||||||
TARGETARCH='arm64'; \
|
|
||||||
;; \
|
|
||||||
amd64|x86_64) \
|
|
||||||
TARGETARCH='amd64'; \
|
|
||||||
;; \
|
|
||||||
*) \
|
|
||||||
echo "Unsupported arch: ${ARCH}"; \
|
|
||||||
exit 1; \
|
|
||||||
;; \
|
|
||||||
esac;
|
|
||||||
|
|
||||||
RUN apt update && apt install build-essential git curl default-libmysqlclient-dev -y
|
|
||||||
|
|
||||||
# Install precompiled supabase wrappers extensions
|
|
||||||
RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_VERSION}/wrappers-v${WRAPPERS_VERSION}-pg${PG_MAIN_VERSION}-${TARGETARCH}-linux-gnu.deb" -o wrappers.deb
|
|
||||||
RUN dpkg --install wrappers.deb
|
|
||||||
RUN cp /usr/share/postgresql/${PG_MAIN_VERSION}/extension/wrappers* /opt/bitnami/postgresql/share/extension/
|
|
||||||
RUN cp /usr/lib/postgresql/${PG_MAIN_VERSION}/lib/wrappers* /opt/bitnami/postgresql/lib/
|
|
||||||
|
|
||||||
RUN export PATH=/usr/local/pgsql/bin/:$PATH
|
|
||||||
RUN export PATH=/usr/local/mysql/bin/:$PATH
|
|
||||||
RUN git clone https://github.com/EnterpriseDB/mysql_fdw.git
|
|
||||||
WORKDIR mysql_fdw
|
|
||||||
RUN make USE_PGXS=1
|
|
||||||
RUN make USE_PGXS=1 install
|
|
||||||
|
|
||||||
COPY ./packages/twenty-docker/twenty-postgres/init.sql /docker-entrypoint-initdb.d/
|
|
||||||
|
|
||||||
USER 1001
|
|
||||||
ENTRYPOINT ["/opt/bitnami/scripts/postgresql/entrypoint.sh"]
|
|
||||||
CMD ["/opt/bitnami/scripts/postgresql/run.sh"]
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
CREATE DATABASE "default";
|
|
||||||
CREATE DATABASE "test";
|
|
||||||
CREATE USER twenty PASSWORD 'twenty';
|
|
||||||
ALTER ROLE twenty superuser;
|
|
||||||
@@ -52,9 +52,10 @@ RUN apk add --no-cache curl jq
|
|||||||
|
|
||||||
RUN npm install -g tsx
|
RUN npm install -g tsx
|
||||||
|
|
||||||
|
RUN apk add --no-cache postgresql-client
|
||||||
|
|
||||||
COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh
|
COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh
|
||||||
RUN chmod +x /app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
WORKDIR /app/packages/twenty-server
|
WORKDIR /app/packages/twenty-server
|
||||||
|
|
||||||
ARG REACT_APP_SERVER_BASE_URL
|
ARG REACT_APP_SERVER_BASE_URL
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
||||||
echo "Running database setup and migrations..."
|
echo "Running database setup and migrations..."
|
||||||
|
|
||||||
|
# Creating the database if it doesn't exist
|
||||||
|
PGUSER=$(echo $PG_DATABASE_URL | awk -F '//' '{print $2}' | awk -F ':' '{print $1}')
|
||||||
|
PGPASS=$(echo $PG_DATABASE_URL | awk -F ':' '{print $3}' | awk -F '@' '{print $1}')
|
||||||
|
PGHOST=$(echo $PG_DATABASE_URL | awk -F '@' '{print $2}' | awk -F ':' '{print $1}')
|
||||||
|
PGPORT=$(echo $PG_DATABASE_URL | awk -F ':' '{print $4}' | awk -F '/' '{print $1}')
|
||||||
|
PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'default'" | grep -q 1 || \
|
||||||
|
PGPASSWORD=${PGPASS} psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d postgres -c "CREATE DATABASE \"default\""
|
||||||
|
|
||||||
# Run setup and migration scripts
|
# Run setup and migration scripts
|
||||||
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
|
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
|
||||||
yarn database:migrate:prod
|
yarn database:migrate:prod
|
||||||
|
|||||||
22
packages/twenty-e2e-testing/drivers/env_variables.ts
Normal file
22
packages/twenty-e2e-testing/drivers/env_variables.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export const envVariables = (variables: string) => {
|
||||||
|
let payload = `
|
||||||
|
PG_DATABASE_URL=postgres://postgres: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;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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?)
|
||||||
|
}
|
||||||
23
packages/twenty-e2e-testing/lib/pom/helper/iconSelect.ts
Normal file
23
packages/twenty-e2e-testing/lib/pom/helper/iconSelect.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
267
packages/twenty-e2e-testing/lib/pom/helper/insertFieldData.ts
Normal file
267
packages/twenty-e2e-testing/lib/pom/helper/insertFieldData.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import { Locator, Page } from '@playwright/test';
|
||||||
|
import { formatDate } from './formatDate.function';
|
||||||
|
|
||||||
|
export class InsertFieldData {
|
||||||
|
private readonly address1Input: Locator;
|
||||||
|
private readonly address2Input: Locator;
|
||||||
|
private readonly cityInput: Locator;
|
||||||
|
private readonly stateInput: Locator;
|
||||||
|
private readonly postCodeInput: Locator;
|
||||||
|
private readonly countrySelect: Locator;
|
||||||
|
private readonly arrayValueInput: Locator;
|
||||||
|
private readonly arrayAddValueButton: Locator;
|
||||||
|
// boolean react after click so no need to write special locator
|
||||||
|
private readonly currencySelect: Locator;
|
||||||
|
private readonly currencyAmountInput: Locator;
|
||||||
|
private readonly monthSelect: Locator;
|
||||||
|
private readonly yearSelect: Locator;
|
||||||
|
private readonly previousMonthButton: Locator;
|
||||||
|
private readonly nextMonthButton: Locator;
|
||||||
|
private readonly clearDateButton: Locator;
|
||||||
|
private readonly dateInput: Locator;
|
||||||
|
private readonly firstNameInput: Locator;
|
||||||
|
private readonly lastNameInput: Locator;
|
||||||
|
private readonly addURLButton: Locator;
|
||||||
|
private readonly setAsPrimaryButton: Locator;
|
||||||
|
private readonly addPhoneButton: Locator;
|
||||||
|
private readonly addMailButton: Locator;
|
||||||
|
|
||||||
|
constructor(public readonly page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
this.address1Input = page.locator(
|
||||||
|
'//label[contains(., "ADDRESS 1")]/../div[last()]/input',
|
||||||
|
);
|
||||||
|
this.address2Input = page.locator(
|
||||||
|
'//label[contains(., "ADDRESS 2")]/../div[last()]/input',
|
||||||
|
);
|
||||||
|
this.cityInput = page.locator(
|
||||||
|
'//label[contains(., "CITY")]/../div[last()]/input',
|
||||||
|
);
|
||||||
|
this.stateInput = page.locator(
|
||||||
|
'//label[contains(., "STATE")]/../div[last()]/input',
|
||||||
|
);
|
||||||
|
this.postCodeInput = page.locator(
|
||||||
|
'//label[contains(., "POST CODE")]/../div[last()]/input',
|
||||||
|
);
|
||||||
|
this.countrySelect = page.locator(
|
||||||
|
'//span[contains(., "COUNTRY")]/../div[last()]/input',
|
||||||
|
);
|
||||||
|
this.arrayValueInput = page.locator("//input[@placeholder='Enter value']");
|
||||||
|
this.arrayAddValueButton = page.locator(
|
||||||
|
"//div[@data-testid='tooltip' and contains(.,'Add item')]",
|
||||||
|
);
|
||||||
|
this.currencySelect = page.locator(
|
||||||
|
'//body/div[last()]/div/div/div[first()]/div/div',
|
||||||
|
);
|
||||||
|
this.currencyAmountInput = page.locator("//input[@placeholder='Currency']");
|
||||||
|
this.monthSelect; // TODO: add once some other attributes are added
|
||||||
|
this.yearSelect;
|
||||||
|
this.previousMonthButton;
|
||||||
|
this.nextMonthButton;
|
||||||
|
this.clearDateButton = page.locator(
|
||||||
|
"//div[@data-testid='tooltip' and contains(., 'Clear')]",
|
||||||
|
);
|
||||||
|
this.dateInput = page.locator("//input[@placeholder='Type date and time']");
|
||||||
|
this.firstNameInput = page.locator("//input[@placeholder='First name']"); // may fail if placeholder is `F‌‌irst name` instead of `First name`
|
||||||
|
this.lastNameInput = page.locator("//input[@placeholder='Last name']"); // may fail if placeholder is `L‌‌ast name` instead of `Last name`
|
||||||
|
this.addURLButton = page.locator(
|
||||||
|
"//div[@data-testid='tooltip' and contains(., 'Add URL')]",
|
||||||
|
);
|
||||||
|
this.setAsPrimaryButton = page.locator(
|
||||||
|
"//div[@data-testid='tooltip' and contains(., 'Set as primary')]",
|
||||||
|
);
|
||||||
|
this.addPhoneButton = page.locator(
|
||||||
|
"//div[@data-testid='tooltip' and contains(., 'Add Phone')]",
|
||||||
|
);
|
||||||
|
this.addMailButton = page.locator(
|
||||||
|
"//div[@data-testid='tooltip' and contains(., 'Add Email')]",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// address
|
||||||
|
async typeAddress1(value: string) {
|
||||||
|
await this.address1Input.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async typeAddress2(value: string) {
|
||||||
|
await this.address2Input.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async typeCity(value: string) {
|
||||||
|
await this.cityInput.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async typeState(value: string) {
|
||||||
|
await this.stateInput.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async typePostCode(value: string) {
|
||||||
|
await this.postCodeInput.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectCountry(value: string) {
|
||||||
|
await this.countrySelect.click();
|
||||||
|
await this.page
|
||||||
|
.locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`)
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// array
|
||||||
|
async typeArrayValue(value: string) {
|
||||||
|
await this.arrayValueInput.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickAddItemButton() {
|
||||||
|
await this.arrayAddValueButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// currency
|
||||||
|
async selectCurrency(value: string) {
|
||||||
|
await this.currencySelect.click();
|
||||||
|
await this.page
|
||||||
|
.locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`)
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async typeCurrencyAmount(value: string) {
|
||||||
|
await this.currencyAmountInput.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// date(-time)
|
||||||
|
async typeDate(value: string) {
|
||||||
|
await this.dateInput.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectMonth(value: string) {
|
||||||
|
await this.monthSelect.click();
|
||||||
|
await this.page
|
||||||
|
.locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`)
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectYear(value: string) {
|
||||||
|
await this.yearSelect.click();
|
||||||
|
await this.page
|
||||||
|
.locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`)
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickPreviousMonthButton() {
|
||||||
|
await this.previousMonthButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickNextMonthButton() {
|
||||||
|
await this.nextMonthButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectDay(value: string) {
|
||||||
|
await this.page
|
||||||
|
.locator(`//div[@aria-label='${formatDate(value)}']`)
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearDate() {
|
||||||
|
await this.clearDateButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// email
|
||||||
|
async typeEmail(value: string) {
|
||||||
|
await this.page.locator(`//input[@placeholder='Email']`).fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickAddMailButton() {
|
||||||
|
await this.addMailButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// full name
|
||||||
|
async typeFirstName(name: string) {
|
||||||
|
await this.firstNameInput.fill(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async typeLastName(name: string) {
|
||||||
|
await this.lastNameInput.fill(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
// placeholder is dependent on the name of field
|
||||||
|
async typeJSON(placeholder: string, value: string) {
|
||||||
|
await this.page
|
||||||
|
.locator(`//input[@placeholder='${placeholder}']`)
|
||||||
|
.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// link
|
||||||
|
async typeLink(value: string) {
|
||||||
|
await this.page.locator("//input[@placeholder='URL']").fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickAddURL() {
|
||||||
|
await this.addURLButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// (multi-)select
|
||||||
|
async selectValue(value: string) {
|
||||||
|
await this.page
|
||||||
|
.locator(`//div[@data-testid='tooltip' and contains(., '${value}')]`)
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// number
|
||||||
|
// placeholder is dependent on the name of field
|
||||||
|
async typeNumber(placeholder: string, value: string) {
|
||||||
|
await this.page
|
||||||
|
.locator(`//input[@placeholder='${placeholder}']`)
|
||||||
|
.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// phones
|
||||||
|
async selectCountryPhoneCode(countryCode: string) {
|
||||||
|
await this.page
|
||||||
|
.locator(
|
||||||
|
`//div[@data-testid='tooltip' and contains(., '${countryCode}')]`,
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async typePhoneNumber(value: string) {
|
||||||
|
await this.page.locator(`//input[@placeholder='Phone']`).fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickAddPhoneButton() {
|
||||||
|
await this.addPhoneButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// rating
|
||||||
|
// if adding rating for the first time, hover must be used
|
||||||
|
async selectRating(rating: number) {
|
||||||
|
await this.page.locator(`//div[@role='slider']/div[${rating}]`).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// text
|
||||||
|
// placeholder is dependent on the name of field
|
||||||
|
async typeText(placeholder: string, value: string) {
|
||||||
|
await this.page
|
||||||
|
.locator(`//input[@placeholder='${placeholder}']`)
|
||||||
|
.fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickSetAsPrimaryButton() {
|
||||||
|
await this.setAsPrimaryButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchValue(value: string) {
|
||||||
|
await this.page.locator(`//div[@placeholder='Search']`).fill(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickEditButton() {
|
||||||
|
await this.page
|
||||||
|
.locator("//div[@data-testid='tooltip' and contains(., 'Edit')]")
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickDeleteButton() {
|
||||||
|
await this.page
|
||||||
|
.locator("//div[@data-testid='tooltip' and contains(., 'Delete')]")
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/twenty-e2e-testing/lib/pom/helper/stripePage.ts
Normal file
5
packages/twenty-e2e-testing/lib/pom/helper/stripePage.ts
Normal file
@@ -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?)
|
||||||
|
}
|
||||||
25
packages/twenty-e2e-testing/lib/pom/helper/uploadImage.ts
Normal file
25
packages/twenty-e2e-testing/lib/pom/helper/uploadImage.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
115
packages/twenty-e2e-testing/lib/pom/leftMenu.ts
Normal file
115
packages/twenty-e2e-testing/lib/pom/leftMenu.ts
Normal file
@@ -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;
|
||||||
187
packages/twenty-e2e-testing/lib/pom/loginPage.ts
Normal file
187
packages/twenty-e2e-testing/lib/pom/loginPage.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
196
packages/twenty-e2e-testing/lib/pom/mainPage.ts
Normal file
196
packages/twenty-e2e-testing/lib/pom/mainPage.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
150
packages/twenty-e2e-testing/lib/pom/recordDetails.ts
Normal file
150
packages/twenty-e2e-testing/lib/pom/recordDetails.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
189
packages/twenty-e2e-testing/lib/pom/settings/dataModelSection.ts
Normal file
189
packages/twenty-e2e-testing/lib/pom/settings/dataModelSection.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
159
packages/twenty-e2e-testing/lib/pom/settings/functionsSection.ts
Normal file
159
packages/twenty-e2e-testing/lib/pom/settings/functionsSection.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
250
packages/twenty-e2e-testing/lib/pom/settings/newFieldSection.ts
Normal file
250
packages/twenty-e2e-testing/lib/pom/settings/newFieldSection.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
104
packages/twenty-e2e-testing/lib/pom/settingsPage.ts
Normal file
104
packages/twenty-e2e-testing/lib/pom/settingsPage.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
94
packages/twenty-e2e-testing/lib/utils/keyboardShortcuts.ts
Normal file
94
packages/twenty-e2e-testing/lib/utils/keyboardShortcuts.ts
Normal file
@@ -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<void>) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
15
packages/twenty-e2e-testing/lib/utils/uploadFile.ts
Normal file
15
packages/twenty-e2e-testing/lib/utils/uploadFile.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Page } from '@playwright/test';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export const fileUploader = async (
|
||||||
|
page: Page,
|
||||||
|
trigger: () => Promise<void>,
|
||||||
|
filename: string,
|
||||||
|
) => {
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
|
await trigger();
|
||||||
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles(
|
||||||
|
path.join(__dirname, '..', 'test_files', filename),
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
import { test, expect } from '../lib/fixtures/screenshot';
|
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.describe('Basic check', () => {
|
||||||
test('Checking if table in Companies is visible', async ({ page }) => {
|
test('Checking if table in Companies is visible', async ({ page }) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twenty-emails",
|
"name": "twenty-emails",
|
||||||
"version": "0.32.0",
|
"version": "0.33.0-canary",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ REACT_APP_SERVER_BASE_URL=http://localhost:3000
|
|||||||
GENERATE_SOURCEMAP=false
|
GENERATE_SOURCEMAP=false
|
||||||
|
|
||||||
# ———————— Optional ————————
|
# ———————— Optional ————————
|
||||||
|
# REACT_APP_PORT=3001
|
||||||
# CHROMATIC_PROJECT_TOKEN=
|
# CHROMATIC_PROJECT_TOKEN=
|
||||||
# VITE_DISABLE_TYPESCRIPT_CHECKER=true
|
# VITE_DISABLE_TYPESCRIPT_CHECKER=true
|
||||||
# VITE_DISABLE_ESLINT_CHECKER=true
|
# VITE_DISABLE_ESLINT_CHECKER=true
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
|
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
|
||||||
<link rel="apple-touch-icon" href="/icons/ios/192.png" />
|
<link rel="apple-touch-icon" href="/icons/ios/192.png" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="A modern open-source CRM" />
|
<meta name="description" content="A modern open-source CRM" />
|
||||||
<meta
|
<meta
|
||||||
@@ -30,6 +29,22 @@
|
|||||||
|
|
||||||
<title>Twenty</title>
|
<title>Twenty</title>
|
||||||
<script src="/env-config.js"></script>
|
<script src="/env-config.js"></script>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script type="module">
|
||||||
|
const disableInputAutoZoom = () => {
|
||||||
|
const viewportMetadata = document.querySelector('meta[name=viewport]');
|
||||||
|
|
||||||
|
if (viewportMetadata !== null) {
|
||||||
|
viewportMetadata.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isIOS = /iPad|iPhone/.test(navigator.userAgent);
|
||||||
|
if (isIOS) {
|
||||||
|
disableInputAutoZoom();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twenty-front",
|
"name": "twenty-front",
|
||||||
"version": "0.32.0",
|
"version": "0.33.0-canary",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const documents = {
|
|||||||
"\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 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 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 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 query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n latestVersionInputSchema {\n name\n type\n }\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n 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 CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||||
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\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 mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||||
@@ -142,7 +142,7 @@ export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilt
|
|||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* 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 latestVersionInputSchema {\n name\n type\n }\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n latestVersionInputSchema {\n name\n type\n }\n publishedVersions\n createdAt\n updatedAt\n }\n"];
|
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.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -31,6 +31,16 @@ export type Analytics = {
|
|||||||
success: Scalars['Boolean'];
|
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 = {
|
export type ApiConfig = {
|
||||||
__typename?: 'ApiConfig';
|
__typename?: 'ApiConfig';
|
||||||
mutationMaximumAffectedRecords: Scalars['Float'];
|
mutationMaximumAffectedRecords: Scalars['Float'];
|
||||||
@@ -321,12 +331,6 @@ export type FullName = {
|
|||||||
lastName: Scalars['String'];
|
lastName: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FunctionParameter = {
|
|
||||||
__typename?: 'FunctionParameter';
|
|
||||||
name: Scalars['String'];
|
|
||||||
type: Scalars['String'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;
|
export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;
|
||||||
|
|
||||||
export type GenerateJwtOutputWithAuthTokens = {
|
export type GenerateJwtOutputWithAuthTokens = {
|
||||||
@@ -973,7 +977,7 @@ export type ServerlessFunction = {
|
|||||||
description?: Maybe<Scalars['String']>;
|
description?: Maybe<Scalars['String']>;
|
||||||
id: Scalars['UUID'];
|
id: Scalars['UUID'];
|
||||||
latestVersion?: Maybe<Scalars['String']>;
|
latestVersion?: Maybe<Scalars['String']>;
|
||||||
latestVersionInputSchema?: Maybe<Array<FunctionParameter>>;
|
latestVersionInputSchema?: Maybe<Scalars['JSON']>;
|
||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
publishedVersions: Array<Scalars['String']>;
|
publishedVersions: Array<Scalars['String']>;
|
||||||
runtime: Scalars['String'];
|
runtime: Scalars['String'];
|
||||||
@@ -1207,7 +1211,7 @@ export type UpdateWorkspaceInput = {
|
|||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
__typename?: 'User';
|
__typename?: 'User';
|
||||||
analyticsTinybirdJwt?: Maybe<Scalars['String']>;
|
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
||||||
canImpersonate: Scalars['Boolean'];
|
canImpersonate: Scalars['Boolean'];
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
defaultAvatarUrl?: Maybe<Scalars['String']>;
|
defaultAvatarUrl?: Maybe<Scalars['String']>;
|
||||||
@@ -1696,7 +1700,7 @@ export type ImpersonateMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type 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<{
|
export type RenewTokenMutationVariables = Exact<{
|
||||||
appToken: Scalars['String'];
|
appToken: Scalars['String'];
|
||||||
@@ -1729,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, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type 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<{
|
export type CheckUserExistsQueryVariables = Exact<{
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@@ -1816,7 +1820,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key:
|
|||||||
|
|
||||||
export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdpType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
|
export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdpType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
|
||||||
|
|
||||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
export type 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; }>;
|
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@@ -1833,7 +1837,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
|||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
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, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
export type 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<{
|
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||||
workflowVersionId: Scalars['String'];
|
workflowVersionId: Scalars['String'];
|
||||||
@@ -2060,7 +2064,14 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
email
|
email
|
||||||
canImpersonate
|
canImpersonate
|
||||||
supportUserHash
|
supportUserHash
|
||||||
analyticsTinybirdJwt
|
analyticsTinybirdJwts {
|
||||||
|
getWebhookAnalytics
|
||||||
|
getPageviewsAnalytics
|
||||||
|
getUsersAnalytics
|
||||||
|
getServerlessFunctionDuration
|
||||||
|
getServerlessFunctionSuccessRate
|
||||||
|
getServerlessFunctionErrorCount
|
||||||
|
}
|
||||||
onboardingStatus
|
onboardingStatus
|
||||||
workspaceMember {
|
workspaceMember {
|
||||||
...WorkspaceMemberQueryFragment
|
...WorkspaceMemberQueryFragment
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ export const Default: Story = {
|
|||||||
|
|
||||||
await canvas.findByText('Search');
|
await canvas.findByText('Search');
|
||||||
await canvas.findByText('Settings');
|
await canvas.findByText('Settings');
|
||||||
await canvas.findByText('Tasks');
|
await canvas.findByText('Linkedin');
|
||||||
await canvas.findByText('People');
|
await canvas.findByText('All companies (v2)');
|
||||||
await canvas.findByText('Opportunities');
|
|
||||||
await canvas.findByText('Rockets');
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ export const WorkflowRunActionEffect = () => {
|
|||||||
position: index,
|
position: index,
|
||||||
Icon: IconSettingsAutomation,
|
Icon: IconSettingsAutomation,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await runWorkflowVersion(activeWorkflowVersion.id);
|
await runWorkflowVersion({
|
||||||
|
workflowVersionId: activeWorkflowVersion.id,
|
||||||
|
});
|
||||||
|
|
||||||
enqueueSnackBar('', {
|
enqueueSnackBar('', {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||||
|
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||||
|
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
||||||
@@ -35,7 +37,8 @@ export const DeleteRecordsActionEffect = ({
|
|||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { favorites, deleteFavorite } = useFavorites();
|
const favorites = useFavorites();
|
||||||
|
const deleteFavorite = useDeleteFavorite();
|
||||||
|
|
||||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||||
contextStoreNumberOfSelectedRecordsComponentState,
|
contextStoreNumberOfSelectedRecordsComponentState,
|
||||||
@@ -45,8 +48,13 @@ export const DeleteRecordsActionEffect = ({
|
|||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const contextStoreFilters = useRecoilComponentValueV2(
|
||||||
|
contextStoreFiltersComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const graphqlFilter = computeContextStoreFilters(
|
const graphqlFilter = computeContextStoreFilters(
|
||||||
contextStoreTargetedRecordsRule,
|
contextStoreTargetedRecordsRule,
|
||||||
|
contextStoreFilters,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||||
import {
|
|
||||||
displayedExportProgress,
|
|
||||||
useExportRecordData,
|
|
||||||
} from '@/action-menu/hooks/useExportRecordData';
|
|
||||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { IconDatabaseExport } from 'twenty-ui';
|
import { IconDatabaseExport } from 'twenty-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
displayedExportProgress,
|
||||||
|
useExportRecords,
|
||||||
|
} from '@/object-record/record-index/export/hooks/useExportRecords';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export const ExportRecordsActionEffect = ({
|
export const ExportRecordsActionEffect = ({
|
||||||
@@ -22,7 +22,7 @@ export const ExportRecordsActionEffect = ({
|
|||||||
contextStoreNumberOfSelectedRecordsComponentState,
|
contextStoreNumberOfSelectedRecordsComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { progress, download } = useExportRecordData({
|
const { progress, download } = useExportRecords({
|
||||||
delayMs: 100,
|
delayMs: 100,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
recordIndexId: objectMetadataItem.namePlural,
|
recordIndexId: objectMetadataItem.namePlural,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
|
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||||
|
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
@@ -21,7 +23,11 @@ export const ManageFavoritesActionEffect = ({
|
|||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { favorites, createFavorite, deleteFavorite } = useFavorites();
|
const favorites = useFavorites();
|
||||||
|
|
||||||
|
const createFavorite = useCreateFavorite();
|
||||||
|
|
||||||
|
const deleteFavorite = useDeleteFavorite();
|
||||||
|
|
||||||
const selectedRecordId =
|
const selectedRecordId =
|
||||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||||
|
|||||||
@@ -8,14 +8,17 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMeta
|
|||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
|
||||||
const globalRecordActionEffects = [ExportRecordsActionEffect];
|
const noSelectionRecordActionEffects = [ExportRecordsActionEffect];
|
||||||
|
|
||||||
const singleRecordActionEffects = [
|
const singleRecordActionEffects = [
|
||||||
ManageFavoritesActionEffect,
|
ManageFavoritesActionEffect,
|
||||||
DeleteRecordsActionEffect,
|
DeleteRecordsActionEffect,
|
||||||
];
|
];
|
||||||
|
|
||||||
const multipleRecordActionEffects = [DeleteRecordsActionEffect];
|
const multipleRecordActionEffects = [
|
||||||
|
ExportRecordsActionEffect,
|
||||||
|
DeleteRecordsActionEffect,
|
||||||
|
];
|
||||||
|
|
||||||
export const RecordActionMenuEntriesSetter = () => {
|
export const RecordActionMenuEntriesSetter = () => {
|
||||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||||
@@ -39,23 +42,18 @@ export const RecordActionMenuEntriesSetter = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions =
|
const actions =
|
||||||
contextStoreNumberOfSelectedRecords === 1
|
contextStoreNumberOfSelectedRecords === 0
|
||||||
|
? noSelectionRecordActionEffects
|
||||||
|
: contextStoreNumberOfSelectedRecords === 1
|
||||||
? singleRecordActionEffects
|
? singleRecordActionEffects
|
||||||
: multipleRecordActionEffects;
|
: multipleRecordActionEffects;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{globalRecordActionEffects.map((ActionEffect, index) => (
|
|
||||||
<ActionEffect
|
|
||||||
key={index}
|
|
||||||
position={index}
|
|
||||||
objectMetadataItem={objectMetadataItem}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{actions.map((ActionEffect, index) => (
|
{actions.map((ActionEffect, index) => (
|
||||||
<ActionEffect
|
<ActionEffect
|
||||||
key={index}
|
key={index}
|
||||||
position={globalRecordActionEffects.length + index}
|
position={index}
|
||||||
objectMetadataItem={objectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ export const WorkflowRunRecordActionEffect = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await runWorkflowVersion(activeWorkflowVersion.id, selectedRecord);
|
await runWorkflowVersion({
|
||||||
|
workflowVersionId: activeWorkflowVersion.id,
|
||||||
|
payload: selectedRecord,
|
||||||
|
});
|
||||||
|
|
||||||
enqueueSnackBar('', {
|
enqueueSnackBar('', {
|
||||||
variant: SnackBarVariant.Success,
|
variant: SnackBarVariant.Success,
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDro
|
|||||||
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
|
||||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||||
|
import { MenuItem } from 'twenty-ui';
|
||||||
|
|
||||||
type StyledContainerProps = {
|
type StyledContainerProps = {
|
||||||
position: PositionType;
|
position: PositionType;
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBa
|
|||||||
|
|
||||||
export const RecordShowActionMenu = ({
|
export const RecordShowActionMenu = ({
|
||||||
isFavorite,
|
isFavorite,
|
||||||
handleFavoriteButtonClick,
|
|
||||||
record,
|
record,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
handleFavoriteButtonClick,
|
||||||
}: {
|
}: {
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
handleFavoriteButtonClick: () => void;
|
|
||||||
record: ObjectRecord | undefined;
|
record: ObjectRecord | undefined;
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
|
handleFavoriteButtonClick: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
||||||
contextStoreCurrentObjectMetadataIdComponentState,
|
contextStoreCurrentObjectMetadataIdComponentState,
|
||||||
@@ -40,10 +40,10 @@ export const RecordShowActionMenu = ({
|
|||||||
<RecordShowPageBaseHeader
|
<RecordShowPageBaseHeader
|
||||||
{...{
|
{...{
|
||||||
isFavorite,
|
isFavorite,
|
||||||
handleFavoriteButtonClick,
|
|
||||||
record,
|
record,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
handleFavoriteButtonClick,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionMenuConfirmationModals />
|
<ActionMenuConfirmationModals />
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
|
||||||
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
|
|
||||||
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
|
|
||||||
|
|
||||||
type RecordShowActionMenuBarEntryProps = {
|
|
||||||
entry: ActionMenuEntry;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledButton = styled.div<{ accent: MenuItemAccent }>`
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
color: ${(props) =>
|
|
||||||
props.accent === 'danger'
|
|
||||||
? props.theme.color.red
|
|
||||||
: props.theme.font.color.secondary};
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
|
||||||
transition: background 0.1s ease;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: ${({ theme, accent }) =>
|
|
||||||
accent === 'danger'
|
|
||||||
? theme.background.danger
|
|
||||||
: theme.background.transparent.light};
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledButtonLabel = styled.div`
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
||||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
// For now, this component is the same as RecordIndexActionMenuBarEntry but they
|
|
||||||
// will probably diverge in the future
|
|
||||||
export const RecordShowActionMenuBarEntry = ({
|
|
||||||
entry,
|
|
||||||
}: RecordShowActionMenuBarEntryProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<StyledButton
|
|
||||||
accent={entry.accent ?? 'default'}
|
|
||||||
onClick={() => entry.onClick?.()}
|
|
||||||
>
|
|
||||||
{entry.Icon && <entry.Icon size={theme.icon.size.md} />}
|
|
||||||
<StyledButtonLabel>{entry.label}</StyledButtonLabel>
|
|
||||||
</StyledButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter';
|
import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter';
|
||||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||||
import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar';
|
import { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown';
|
||||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||||
|
|
||||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||||
@@ -21,7 +21,7 @@ export const RecordShowRightDrawerActionMenu = () => {
|
|||||||
onActionExecutedCallback: () => {},
|
onActionExecutedCallback: () => {},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RecordShowRightDrawerActionMenuBar />
|
<RightDrawerActionMenuDropdown />
|
||||||
<ActionMenuConfirmationModals />
|
<ActionMenuConfirmationModals />
|
||||||
<RecordActionMenuEntriesSetter />
|
<RecordActionMenuEntriesSetter />
|
||||||
<GlobalActionMenuEntriesSetter />
|
<GlobalActionMenuEntriesSetter />
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { RecordShowActionMenuBarEntry } from '@/action-menu/components/RecordShowActionMenuBarEntry';
|
|
||||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
|
||||||
|
|
||||||
export const RecordShowRightDrawerActionMenuBar = () => {
|
|
||||||
const actionMenuEntries = useRecoilComponentValueV2(
|
|
||||||
actionMenuEntriesComponentSelector,
|
|
||||||
);
|
|
||||||
|
|
||||||
const standardActionMenuEntries = actionMenuEntries.filter(
|
|
||||||
(actionMenuEntry) => actionMenuEntry.type === 'standard',
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{standardActionMenuEntries.map((actionMenuEntry) => (
|
|
||||||
<RecordShowActionMenuBarEntry entry={actionMenuEntry} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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 (
|
||||||
|
<Dropdown
|
||||||
|
dropdownId={getRightDrawerActionMenuDropdownIdFromActionMenuId(
|
||||||
|
actionMenuId,
|
||||||
|
)}
|
||||||
|
dropdownHotkeyScope={{
|
||||||
|
scope:
|
||||||
|
RightDrawerActionMenuDropdownHotkeyScope.RightDrawerActionMenuDropdown,
|
||||||
|
}}
|
||||||
|
data-select-disable
|
||||||
|
clickableComponent={<Button title="Actions" shortcut="⌘O" />}
|
||||||
|
dropdownPlacement="top-end"
|
||||||
|
dropdownOffset={{
|
||||||
|
y: parseInt(theme.spacing(2)),
|
||||||
|
}}
|
||||||
|
dropdownComponents={
|
||||||
|
<DropdownMenuItemsContainer>
|
||||||
|
{actionMenuEntries.map((item, index) => (
|
||||||
|
<MenuItem
|
||||||
|
key={index}
|
||||||
|
LeftIcon={item.Icon}
|
||||||
|
onClick={() => {
|
||||||
|
closeDropdown(
|
||||||
|
getRightDrawerActionMenuDropdownIdFromActionMenuId(
|
||||||
|
actionMenuId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
item.onClick?.();
|
||||||
|
}}
|
||||||
|
text={item.label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,28 +2,28 @@ import { expect, jest } from '@storybook/jest';
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar';
|
import { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown';
|
||||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||||
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
|
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
|
||||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
|
|
||||||
import { userEvent, waitFor, within } from '@storybook/test';
|
import { userEvent, waitFor, within } from '@storybook/test';
|
||||||
import {
|
import {
|
||||||
ComponentDecorator,
|
ComponentDecorator,
|
||||||
IconFileExport,
|
IconFileExport,
|
||||||
IconHeart,
|
IconHeart,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
|
MenuItemAccent,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
const deleteMock = jest.fn();
|
const deleteMock = jest.fn();
|
||||||
const addToFavoritesMock = jest.fn();
|
const addToFavoritesMock = jest.fn();
|
||||||
const exportMock = jest.fn();
|
const exportMock = jest.fn();
|
||||||
|
|
||||||
const meta: Meta<typeof RecordShowRightDrawerActionMenuBar> = {
|
const meta: Meta<typeof RightDrawerActionMenuDropdown> = {
|
||||||
title: 'Modules/ActionMenu/RecordShowRightDrawerActionMenuBar',
|
title: 'Modules/ActionMenu/RightDrawerActionMenuDropdown',
|
||||||
component: RecordShowRightDrawerActionMenuBar,
|
component: RightDrawerActionMenuDropdown,
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story) => (
|
(Story) => (
|
||||||
<RecoilRoot
|
<RecoilRoot
|
||||||
@@ -98,7 +98,7 @@ const meta: Meta<typeof RecordShowRightDrawerActionMenuBar> = {
|
|||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj<typeof RecordShowRightDrawerActionMenuBar>;
|
type Story = StoryObj<typeof RightDrawerActionMenuDropdown>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
@@ -113,12 +113,21 @@ export const WithButtonClicks: Story = {
|
|||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
let actionButton = await canvas.findByText('Actions');
|
||||||
|
await userEvent.click(actionButton);
|
||||||
|
|
||||||
const deleteButton = await canvas.findByText('Delete');
|
const deleteButton = await canvas.findByText('Delete');
|
||||||
await userEvent.click(deleteButton);
|
await userEvent.click(deleteButton);
|
||||||
|
|
||||||
|
actionButton = await canvas.findByText('Actions');
|
||||||
|
await userEvent.click(actionButton);
|
||||||
|
|
||||||
const addToFavoritesButton = await canvas.findByText('Add to favorites');
|
const addToFavoritesButton = await canvas.findByText('Add to favorites');
|
||||||
await userEvent.click(addToFavoritesButton);
|
await userEvent.click(addToFavoritesButton);
|
||||||
|
|
||||||
|
actionButton = await canvas.findByText('Actions');
|
||||||
|
await userEvent.click(actionButton);
|
||||||
|
|
||||||
const exportButton = await canvas.findByText('Export');
|
const exportButton = await canvas.findByText('Export');
|
||||||
await userEvent.click(exportButton);
|
await userEvent.click(exportButton);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { MouseEvent, ReactNode } from 'react';
|
import { MouseEvent, ReactNode } from 'react';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent, MenuItemAccent } from 'twenty-ui';
|
||||||
|
|
||||||
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
|
|
||||||
|
|
||||||
export type ActionMenuEntry = {
|
export type ActionMenuEntry = {
|
||||||
type: 'standard' | 'workflow-run';
|
type: 'standard' | 'workflow-run';
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum RightDrawerActionMenuDropdownHotkeyScope {
|
||||||
|
RightDrawerActionMenuDropdown = 'right-drawer-action-menu-dropdown',
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const getRightDrawerActionMenuDropdownIdFromActionMenuId = (
|
||||||
|
actionMenuId: string,
|
||||||
|
) => {
|
||||||
|
return `right-drawer-action-menu-dropdown-${actionMenuId}`;
|
||||||
|
};
|
||||||
@@ -350,6 +350,10 @@ export const RichTextEditor = ({
|
|||||||
editor.focus();
|
editor.focus();
|
||||||
},
|
},
|
||||||
RightDrawerHotkeyScope.RightDrawer,
|
RightDrawerHotkeyScope.RightDrawer,
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
preventDefault: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBlockEditorFocus = () => {
|
const handleBlockEditorFocus = () => {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar';
|
|
||||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||||
import { IconPlus } from 'twenty-ui';
|
import { IconPlus, MenuItemAvatar } from 'twenty-ui';
|
||||||
|
|
||||||
export const MessageThreadSubscriberDropdownAddSubscriberMenuItem = ({
|
export const MessageThreadSubscriberDropdownAddSubscriberMenuItem = ({
|
||||||
workspaceMember,
|
workspaceMember,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { offset } from '@floating-ui/react';
|
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 { MessageThreadSubscriberDropdownAddSubscriber } from '@/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriber';
|
||||||
import { MessageThreadSubscribersChip } from '@/activities/emails/components/MessageThreadSubscribersChip';
|
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 { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
|
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';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export const MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID =
|
export const MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID =
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import {
|
|||||||
IconPencil,
|
IconPencil,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
LightIconButton,
|
LightIconButton,
|
||||||
|
MenuItem,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
|
||||||
|
|
||||||
type AttachmentDropdownProps = {
|
type AttachmentDropdownProps = {
|
||||||
onDownload: () => void;
|
onDownload: () => void;
|
||||||
@@ -50,8 +49,8 @@ export const AttachmentDropdown = ({
|
|||||||
clickableComponent={
|
clickableComponent={
|
||||||
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||||
}
|
}
|
||||||
|
dropdownMenuWidth={160}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownMenu width="160px">
|
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
text="Download"
|
text="Download"
|
||||||
@@ -70,7 +69,6 @@ export const AttachmentDropdown = ({
|
|||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</DropdownMenu>
|
|
||||||
}
|
}
|
||||||
dropdownHotkeyScope={{
|
dropdownHotkeyScope={{
|
||||||
scope: dropdownId,
|
scope: dropdownId,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect';
|
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 { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
||||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||||
@@ -287,6 +288,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
<ActivityTargetInlineCellEditModeMultiRecordsEffect
|
<ActivityTargetInlineCellEditModeMultiRecordsEffect
|
||||||
selectedObjectRecordIds={selectedTargetObjectIds}
|
selectedObjectRecordIds={selectedTargetObjectIds}
|
||||||
/>
|
/>
|
||||||
|
<ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect />
|
||||||
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
|
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
|
||||||
</RelationPickerScope>
|
</RelationPickerScope>
|
||||||
</StyledSelectContainer>
|
</StyledSelectContainer>
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ const mocks: MockedResponse[] = [
|
|||||||
companyId
|
companyId
|
||||||
createdAt
|
createdAt
|
||||||
deletedAt
|
deletedAt
|
||||||
|
favoriteFolderId
|
||||||
id
|
id
|
||||||
noteId
|
noteId
|
||||||
opportunityId
|
opportunityId
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ const StyledTimelineContainer = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
width: calc(100% - ${({ theme }) => theme.spacing(8)});
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EventList = ({ events, targetableObject }: EventListProps) => {
|
export const EventList = ({ events, targetableObject }: EventListProps) => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { TimelineActivity } from '@/activities/timeline-activities/types/Timelin
|
|||||||
import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName';
|
import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
||||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ const StyledSummary = styled.summary`
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledItemContainer = styled.div<{ isMarginBottom?: boolean }>`
|
const StyledItemContainer = styled.div<{ isMarginBottom?: boolean }>`
|
||||||
@@ -77,6 +79,9 @@ const StyledItemContainer = styled.div<{ isMarginBottom?: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledItemTitleDate = styled.div`
|
const StyledItemTitleDate = styled.div`
|
||||||
|
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
AnimatedPlaceholderEmptyTextContainer,
|
AnimatedPlaceholderEmptyTextContainer,
|
||||||
AnimatedPlaceholderEmptyTitle,
|
AnimatedPlaceholderEmptyTitle,
|
||||||
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
||||||
|
MOBILE_VIEWPORT,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
const StyledMainContainer = styled.div`
|
const StyledMainContainer = styled.div`
|
||||||
@@ -31,6 +32,11 @@ const StyledMainContainer = styled.div`
|
|||||||
padding-right: ${({ theme }) => theme.spacing(6)};
|
padding-right: ${({ theme }) => theme.spacing(6)};
|
||||||
padding-left: ${({ theme }) => theme.spacing(6)};
|
padding-left: ${({ theme }) => theme.spacing(6)};
|
||||||
gap: ${({ theme }) => theme.spacing(4)};
|
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 = ({
|
export const TimelineActivities = ({
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ const StyledLinkedActivity = styled.span`
|
|||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledEventRowItemText = styled.span`
|
export const StyledEventRowItemText = styled.span`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { Card } from 'twenty-ui';
|
import { Card, MOBILE_VIEWPORT } from 'twenty-ui';
|
||||||
|
|
||||||
type EventCardProps = {
|
type EventCardProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -16,6 +16,10 @@ const StyledCardContainer = styled.div`
|
|||||||
width: 400px;
|
width: 400px;
|
||||||
padding: ${({ theme }) => theme.spacing(2)} 0px
|
padding: ${({ theme }) => theme.spacing(2)} 0px
|
||||||
${({ theme }) => theme.spacing(1)} 0px;
|
${({ theme }) => theme.spacing(1)} 0px;
|
||||||
|
|
||||||
|
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCard = styled(Card)`
|
const StyledCard = styled(Card)`
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ const StyledEventFieldDiffContainer = styled.div`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
height: 24px;
|
height: 24px;
|
||||||
width: 380px;
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledEmptyValue = styled.div`
|
const StyledEmptyValue = styled.div`
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip';
|
import { WebhookAnalyticsTooltip } from '@/analytics/components/WebhookAnalyticsTooltip';
|
||||||
import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
|
import { ANALYTICS_GRAPH_DESCRIPTION_MAP } from '@/analytics/constants/AnalyticsGraphDescriptionMap';
|
||||||
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
|
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 { Select } from '@/ui/input/components/Select';
|
||||||
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { ResponsiveLine } from '@nivo/line';
|
import { ResponsiveLine } from '@nivo/line';
|
||||||
import { Section } from '@react-email/components';
|
import { Section } from '@react-email/components';
|
||||||
import { useState } from 'react';
|
import { useId, useState } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import { H2Title } from 'twenty-ui';
|
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`
|
const StyledGraphContainer = styled.div`
|
||||||
background-color: ${({ theme }) => theme.background.secondary};
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
@@ -33,34 +29,38 @@ const StyledTitleContainer = styled.div`
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type SettingsDevelopersWebhookUsageGraphProps = {
|
export const AnalyticsActivityGraph = ({
|
||||||
webhookId: string;
|
recordId,
|
||||||
};
|
endpointName,
|
||||||
|
}: AnalyticsActivityGraphProps) => {
|
||||||
export const SettingsDevelopersWebhookUsageGraph = ({
|
const [analyticsGraphData, setAnalyticsGraphData] = useRecoilComponentStateV2(
|
||||||
webhookId,
|
analyticsGraphDataComponentState,
|
||||||
}: SettingsDevelopersWebhookUsageGraphProps) => {
|
);
|
||||||
const webhookGraphData = useRecoilValue(webhookGraphDataState);
|
|
||||||
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
|
const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
|
||||||
'7D' | '1D' | '12H' | '4H'
|
'7D' | '1D' | '12H' | '4H'
|
||||||
>('7D');
|
>('7D');
|
||||||
|
|
||||||
const { fetchGraphData } = useGraphData(webhookId);
|
const { fetchGraphData } = useGraphData({
|
||||||
|
recordId,
|
||||||
|
endpointName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const transformDataFunction = computeAnalyticsGraphDataFunction(endpointName);
|
||||||
|
|
||||||
|
const dropdownId = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{webhookGraphData.length ? (
|
{analyticsGraphData.length ? (
|
||||||
<Section>
|
<Section>
|
||||||
<StyledTitleContainer>
|
<StyledTitleContainer>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Activity"
|
title={`${ANALYTICS_GRAPH_TITLE_MAP[endpointName]}`}
|
||||||
description="See your webhook activity over time"
|
description={`${ANALYTICS_GRAPH_DESCRIPTION_MAP[endpointName]}`}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
dropdownId="test-id-webhook-graph"
|
dropdownId={dropdownId}
|
||||||
value={windowLengthGraphOption}
|
value={windowLengthGraphOption}
|
||||||
options={[
|
options={[
|
||||||
{ value: '7D', label: 'This week' },
|
{ value: '7D', label: 'This week' },
|
||||||
@@ -71,7 +71,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
|||||||
onChange={(windowLengthGraphOption) => {
|
onChange={(windowLengthGraphOption) => {
|
||||||
setWindowLengthGraphOption(windowLengthGraphOption);
|
setWindowLengthGraphOption(windowLengthGraphOption);
|
||||||
fetchGraphData(windowLengthGraphOption).then((graphInput) => {
|
fetchGraphData(windowLengthGraphOption).then((graphInput) => {
|
||||||
setWebhookGraphData(graphInput);
|
setAnalyticsGraphData(transformDataFunction(graphInput));
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -79,10 +79,12 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
|||||||
|
|
||||||
<StyledGraphContainer>
|
<StyledGraphContainer>
|
||||||
<ResponsiveLine
|
<ResponsiveLine
|
||||||
data={webhookGraphData}
|
data={analyticsGraphData}
|
||||||
curve={'monotoneX'}
|
curve={'monotoneX'}
|
||||||
enableArea={true}
|
enableArea={true}
|
||||||
colors={(d) => d.color}
|
colors={{ scheme: 'set1' }}
|
||||||
|
//it "addapts" to the color scheme of the graph without hardcoding them
|
||||||
|
//is there a color scheme for graph Data in twenty? Do we always want the gradient?
|
||||||
theme={{
|
theme={{
|
||||||
text: {
|
text: {
|
||||||
fill: theme.font.color.light,
|
fill: theme.font.color.light,
|
||||||
@@ -149,7 +151,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
|||||||
type: 'linear',
|
type: 'linear',
|
||||||
}}
|
}}
|
||||||
axisBottom={{
|
axisBottom={{
|
||||||
format: '%b %d, %I:%M %p',
|
format: '%b %d, %I:%M %p', //TODO: add the user prefered time format for the graph
|
||||||
tickValues: 2,
|
tickValues: 2,
|
||||||
tickPadding: 5,
|
tickPadding: 5,
|
||||||
tickSize: 6,
|
tickSize: 6,
|
||||||
@@ -167,9 +169,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
|||||||
useMesh={true}
|
useMesh={true}
|
||||||
enableSlices={false}
|
enableSlices={false}
|
||||||
enableCrosshair={false}
|
enableCrosshair={false}
|
||||||
tooltip={({ point }) => (
|
tooltip={({ point }) => <WebhookAnalyticsTooltip point={point} />} // later add a condition to get different tooltips
|
||||||
<SettingsDevelopersWebhookTooltip point={point} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</StyledGraphContainer>
|
</StyledGraphContainer>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -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 <></>;
|
||||||
|
};
|
||||||
@@ -58,12 +58,12 @@ const StyledDataDefinition = styled.div`
|
|||||||
const StyledSpan = styled.span`
|
const StyledSpan = styled.span`
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
`;
|
`;
|
||||||
type SettingsDevelopersWebhookTooltipProps = {
|
type WebhookAnalyticsTooltipProps = {
|
||||||
point: Point;
|
point: Point;
|
||||||
};
|
};
|
||||||
export const SettingsDevelopersWebhookTooltip = ({
|
export const WebhookAnalyticsTooltip = ({
|
||||||
point,
|
point,
|
||||||
}: SettingsDevelopersWebhookTooltipProps): ReactElement => {
|
}: WebhookAnalyticsTooltipProps): ReactElement => {
|
||||||
const { timeFormat, timeZone } = useContext(UserContext);
|
const { timeFormat, timeZone } = useContext(UserContext);
|
||||||
const windowInterval = new Date(point.data.x);
|
const windowInterval = new Date(point.data.x);
|
||||||
const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
|
const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user