mirror of
https://github.com/lingble/twenty.git
synced 2025-10-29 20:02:29 +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
|
||||
jobs:
|
||||
deploy-main:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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*'
|
||||
jobs:
|
||||
deploy-tag:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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:
|
||||
chrome-extension-build:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VITE_SERVER_BASE_URL: http://localhost:3000
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
name: Playwright Tests
|
||||
name: CI E2E Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
packages/** # Adjust this to your relevant directories
|
||||
playwright.config.ts # Include any relevant config files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
packages/**
|
||||
playwright.config.ts
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed != 'true'
|
||||
run: echo "No relevant changes detected. Marking as valid."
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes detected. Marking as valid."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npm install -g yarn && yarn
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Install Playwright Browsers
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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:
|
||||
front-sb-build:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
@@ -58,8 +59,8 @@ jobs:
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx storybook:build twenty-front
|
||||
front-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: shipfox-8vcpu-ubuntu-2204
|
||||
timeout-minutes: 60
|
||||
needs: front-sb-build
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -101,8 +102,8 @@ jobs:
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }}
|
||||
front-sb-test-performance:
|
||||
timeout-minutes: 30
|
||||
runs-on: shipfox-8vcpu-ubuntu-2204
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
@@ -135,6 +136,7 @@ jobs:
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx run twenty-front:storybook:serve-and-test:static:performance
|
||||
front-chromatic-deployment:
|
||||
timeout-minutes: 30
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push'
|
||||
needs: front-sb-build
|
||||
runs-on: ubuntu-latest
|
||||
@@ -177,6 +179,7 @@ jobs:
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx run twenty-front:chromatic:ci
|
||||
front-task:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
|
||||
1
.github/workflows/ci-release-create.yaml
vendored
1
.github/workflows/ci-release-create.yaml
vendored
@@ -15,6 +15,7 @@ on:
|
||||
|
||||
jobs:
|
||||
create_pr:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
1
.github/workflows/ci-release-merge.yaml
vendored
1
.github/workflows/ci-release-merge.yaml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
tag_and_release:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
|
||||
steps:
|
||||
|
||||
36
.github/workflows/ci-server.yaml
vendored
36
.github/workflows/ci-server.yaml
vendored
@@ -12,17 +12,25 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
server-setup:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
@@ -62,11 +70,17 @@ jobs:
|
||||
- name: Server / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
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
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx run twenty-server:worker:ci
|
||||
|
||||
server-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
needs: server-setup
|
||||
env:
|
||||
@@ -102,16 +116,24 @@ jobs:
|
||||
tasks: test
|
||||
|
||||
server-integration-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
needs: server-setup
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
@@ -146,7 +168,7 @@ jobs:
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: "test:integration"
|
||||
tasks: "test:integration:with-db-reset"
|
||||
- name: Server / Upload reset-logs file
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
36
.github/workflows/ci-test-docker-compose.yaml
vendored
36
.github/workflows/ci-test-docker-compose.yaml
vendored
@@ -8,6 +8,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -35,20 +36,42 @@ jobs:
|
||||
|
||||
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.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..."
|
||||
cp .env.example .env
|
||||
echo "Generating secrets..."
|
||||
echo "# === Randomly generated secrets ===" >>.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..."
|
||||
docker compose up -d
|
||||
echo "Docker compose up..."
|
||||
docker compose up -d || {
|
||||
echo "Docker compose failed to start"
|
||||
docker compose logs
|
||||
exit 1
|
||||
}
|
||||
docker compose logs db server -f &
|
||||
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..."
|
||||
count=0
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||
@@ -56,11 +79,14 @@ jobs:
|
||||
count=$((count+1));
|
||||
if [ $(docker inspect --format='{{.State.Status}}' twenty-server-1) = "exited" ]; then
|
||||
echo "Server exited"
|
||||
docker compose logs server
|
||||
exit 1
|
||||
fi
|
||||
if [ $count -gt 300 ]; then
|
||||
echo "Failed to start server"
|
||||
echo "Failed to start server after 5 minutes"
|
||||
docker compose logs server
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting for server... (${count}/300s)"
|
||||
done
|
||||
working-directory: ./packages/twenty-docker/
|
||||
|
||||
32
.github/workflows/ci-tinybird.yaml
vendored
32
.github/workflows/ci-tinybird.yaml
vendored
@@ -3,8 +3,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-tinybird/**'
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'packages/twenty-tinybird/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -12,23 +18,9 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-tinybird/**
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Check twenty-tinybird package
|
||||
uses: tinybirdco/ci/.github/workflows/ci.yml@main
|
||||
with:
|
||||
data_project_dir: packages/twenty-tinybird
|
||||
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
|
||||
tb_host: https://api.eu-central-1.aws.tinybird.co
|
||||
uses: tinybirdco/ci/.github/workflows/ci.yml@main
|
||||
with:
|
||||
data_project_dir: packages/twenty-tinybird
|
||||
secrets:
|
||||
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
|
||||
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
|
||||
concurrency:
|
||||
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:
|
||||
danger-js:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'closed'
|
||||
steps:
|
||||
@@ -31,6 +34,7 @@ jobs:
|
||||
DANGER_GITHUB_API_TOKEN: ${{ github.token }}
|
||||
|
||||
congratulate:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'closed' && github.event.pull_request.merged == true
|
||||
steps:
|
||||
|
||||
22
.github/workflows/ci-website.yaml
vendored
22
.github/workflows/ci-website.yaml
vendored
@@ -13,15 +13,23 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
website-build:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
POSTGRES_PASSWORD: twenty
|
||||
POSTGRES_USER: twenty
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -36,16 +44,20 @@ jobs:
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
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
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
run: npx nx database:migrate twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default
|
||||
DATABASE_PG_URL: postgres://postgres:twenty@localhost:5432/default
|
||||
- name: Website / Build Website
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
run: npx nx build twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default
|
||||
DATABASE_PG_URL: postgres://postgres:twenty@localhost:5432/default
|
||||
|
||||
- name: 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",
|
||||
"path": "../packages/twenty-emails"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-postgres",
|
||||
"path": "../packages/twenty-postgres"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-server",
|
||||
"path": "../packages/twenty-server"
|
||||
|
||||
22
Makefile
22
Makefile
@@ -1,12 +1,20 @@
|
||||
postgres-on-docker:
|
||||
docker run \
|
||||
--name twenty_postgres \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_DB=default \
|
||||
-v twenty_db_data:/var/lib/postgresql/data \
|
||||
docker run -d \
|
||||
--name twenty_pg \
|
||||
-e PGUSER_SUPERUSER=postgres \
|
||||
-e PGPASSWORD_SUPERUSER=twenty \
|
||||
-e ALLOW_NOSSL=true \
|
||||
-v twenty_db_data:/home/postgres/pgdata \
|
||||
-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:
|
||||
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 "APP_SECRET=$(openssl rand -base64 32)" >>.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"
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@linaria/core": "^6.2.0",
|
||||
"@linaria/react": "^6.2.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
"@nestjs/apollo": "^11.0.5",
|
||||
"@nestjs/axios": "^3.0.1",
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
@@ -201,6 +202,7 @@
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.0",
|
||||
"@nestjs/testing": "^9.0.0",
|
||||
@@ -350,7 +352,7 @@
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
"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": {
|
||||
"packages": [
|
||||
|
||||
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
||||
type H2TitleProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
addornment?: React.ReactNode;
|
||||
adornment?: React.ReactNode;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -33,11 +33,11 @@ const StyledDescription = styled.h3`
|
||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const H2Title = ({ title, description, addornment }: H2TitleProps) => (
|
||||
export const H2Title = ({ title, description, adornment }: H2TitleProps) => (
|
||||
<StyledContainer>
|
||||
<StyledTitleContainer>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
{addornment}
|
||||
{adornment}
|
||||
</StyledTitleContainer>
|
||||
{description && <StyledDescription>{description}</StyledDescription>}
|
||||
</StyledContainer>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
@@ -4,9 +4,6 @@ prod-build:
|
||||
prod-run:
|
||||
@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:
|
||||
@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:
|
||||
@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"
|
||||
environment:
|
||||
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}
|
||||
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"
|
||||
|
||||
@@ -52,10 +52,10 @@ services:
|
||||
image: twentycrm/twenty:${TAG}
|
||||
command: ["yarn", "worker:prod"]
|
||||
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}
|
||||
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
|
||||
|
||||
@@ -73,13 +73,16 @@ services:
|
||||
restart: always
|
||||
|
||||
db:
|
||||
image: twentycrm/twenty-postgres:${TAG}
|
||||
image: twentycrm/twenty-postgres-spilo:${TAG}
|
||||
volumes:
|
||||
- db-data:/bitnami/postgresql
|
||||
- db-data:/home/postgres/pgdata
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD}
|
||||
PGUSER_SUPERUSER: ${PGUSER_SUPERUSER:-postgres}
|
||||
PGPASSWORD_SUPERUSER: ${PGPASSWORD_SUPERUSER:-twenty}
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
healthcheck:
|
||||
test: pg_isready -U twenty -d default
|
||||
test: pg_isready -U ${PGUSER_SUPERUSER:-postgres} -h localhost -d postgres
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
@@ -30,10 +30,10 @@ spec:
|
||||
image: twentycrm/twenty-postgres:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: POSTGRES_PASSWORD
|
||||
- name: PGUSER_SUPERUSER
|
||||
value: "postgres"
|
||||
- name: PGPASSWORD_SUPERUSER
|
||||
value: "twenty"
|
||||
- name: BITNAMI_DEBUG
|
||||
value: "true"
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: tcp
|
||||
@@ -48,7 +48,7 @@ spec:
|
||||
stdin: true
|
||||
tty: true
|
||||
volumeMounts:
|
||||
- mountPath: /bitnami/postgresql
|
||||
- mountPath: /home/postgres/pgdata
|
||||
name: twentycrm-db-data
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
|
||||
@@ -40,7 +40,7 @@ spec:
|
||||
- name: FRONT_BASE_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- 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"
|
||||
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
|
||||
- name: ENABLE_DB_MIGRATIONS
|
||||
|
||||
@@ -31,7 +31,7 @@ spec:
|
||||
- name: FRONT_BASE_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- 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
|
||||
value: "false" # it already runs on the server
|
||||
- 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_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_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_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 |
|
||||
|
||||
@@ -29,7 +29,7 @@ variable "twentycrm_server_image" {
|
||||
|
||||
variable "twentycrm_db_image" {
|
||||
type = string
|
||||
default = "twentycrm/twenty-postgres:latest"
|
||||
default = "twentycrm/twenty-postgres-spilo: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
|
||||
|
||||
# Build the mysql_fdw extension
|
||||
FROM debian:bookworm as build-mysql_fdw
|
||||
FROM debian:bookworm AS build-mysql_fdw
|
||||
ARG POSTGRES_VERSION
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt update && \
|
||||
apt install -y \
|
||||
build-essential \
|
||||
@@ -17,14 +17,14 @@ RUN apt update && \
|
||||
|
||||
# Install mysql_fdw
|
||||
RUN git clone https://github.com/EnterpriseDB/mysql_fdw.git
|
||||
WORKDIR mysql_fdw
|
||||
WORKDIR /mysql_fdw
|
||||
RUN make USE_PGXS=1
|
||||
|
||||
|
||||
# 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 && \
|
||||
apt install -y \
|
||||
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 apk add --no-cache postgresql-client
|
||||
|
||||
COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
WORKDIR /app/packages/twenty-server
|
||||
|
||||
ARG REACT_APP_SERVER_BASE_URL
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
||||
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
|
||||
NODE_OPTIONS="--max-old-space-size=1500" tsx ./scripts/setup-db.ts
|
||||
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 { config } from 'dotenv';
|
||||
import path = require('path');
|
||||
config({ path: path.resolve(__dirname, '..', '.env') });
|
||||
|
||||
test.describe('Basic check', () => {
|
||||
test('Checking if table in Companies is visible', async ({ page }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-emails",
|
||||
"version": "0.32.0",
|
||||
"version": "0.33.0-canary",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -2,6 +2,7 @@ REACT_APP_SERVER_BASE_URL=http://localhost:3000
|
||||
GENERATE_SOURCEMAP=false
|
||||
|
||||
# ———————— Optional ————————
|
||||
# REACT_APP_PORT=3001
|
||||
# CHROMATIC_PROJECT_TOKEN=
|
||||
# VITE_DISABLE_TYPESCRIPT_CHECKER=true
|
||||
# VITE_DISABLE_ESLINT_CHECKER=true
|
||||
@@ -6,7 +6,6 @@
|
||||
<link rel="icon" href="/icons/android/android-launchericon-48-48.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="description" content="A modern open-source CRM" />
|
||||
<meta
|
||||
@@ -30,6 +29,22 @@
|
||||
|
||||
<title>Twenty</title>
|
||||
<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>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-front",
|
||||
"version": "0.32.0",
|
||||
"version": "0.33.0-canary",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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 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 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 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,
|
||||
@@ -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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,6 +31,16 @@ export type Analytics = {
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type AnalyticsTinybirdJwtMap = {
|
||||
__typename?: 'AnalyticsTinybirdJwtMap';
|
||||
getPageviewsAnalytics: Scalars['String'];
|
||||
getServerlessFunctionDuration: Scalars['String'];
|
||||
getServerlessFunctionErrorCount: Scalars['String'];
|
||||
getServerlessFunctionSuccessRate: Scalars['String'];
|
||||
getUsersAnalytics: Scalars['String'];
|
||||
getWebhookAnalytics: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ApiConfig = {
|
||||
__typename?: 'ApiConfig';
|
||||
mutationMaximumAffectedRecords: Scalars['Float'];
|
||||
@@ -321,12 +331,6 @@ export type FullName = {
|
||||
lastName: Scalars['String'];
|
||||
};
|
||||
|
||||
export type FunctionParameter = {
|
||||
__typename?: 'FunctionParameter';
|
||||
name: Scalars['String'];
|
||||
type: Scalars['String'];
|
||||
};
|
||||
|
||||
export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;
|
||||
|
||||
export type GenerateJwtOutputWithAuthTokens = {
|
||||
@@ -973,7 +977,7 @@ export type ServerlessFunction = {
|
||||
description?: Maybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
latestVersion?: Maybe<Scalars['String']>;
|
||||
latestVersionInputSchema?: Maybe<Array<FunctionParameter>>;
|
||||
latestVersionInputSchema?: Maybe<Scalars['JSON']>;
|
||||
name: Scalars['String'];
|
||||
publishedVersions: Array<Scalars['String']>;
|
||||
runtime: Scalars['String'];
|
||||
@@ -1207,7 +1211,7 @@ export type UpdateWorkspaceInput = {
|
||||
|
||||
export type User = {
|
||||
__typename?: 'User';
|
||||
analyticsTinybirdJwt?: Maybe<Scalars['String']>;
|
||||
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
||||
canImpersonate: Scalars['Boolean'];
|
||||
createdAt: Scalars['DateTime'];
|
||||
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<{
|
||||
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<{
|
||||
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 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; }>;
|
||||
|
||||
@@ -1833,7 +1837,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, 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<{
|
||||
workflowVersionId: Scalars['String'];
|
||||
@@ -2060,7 +2064,14 @@ export const UserQueryFragmentFragmentDoc = gql`
|
||||
email
|
||||
canImpersonate
|
||||
supportUserHash
|
||||
analyticsTinybirdJwt
|
||||
analyticsTinybirdJwts {
|
||||
getWebhookAnalytics
|
||||
getPageviewsAnalytics
|
||||
getUsersAnalytics
|
||||
getServerlessFunctionDuration
|
||||
getServerlessFunctionSuccessRate
|
||||
getServerlessFunctionErrorCount
|
||||
}
|
||||
onboardingStatus
|
||||
workspaceMember {
|
||||
...WorkspaceMemberQueryFragment
|
||||
|
||||
@@ -35,9 +35,7 @@ export const Default: Story = {
|
||||
|
||||
await canvas.findByText('Search');
|
||||
await canvas.findByText('Settings');
|
||||
await canvas.findByText('Tasks');
|
||||
await canvas.findByText('People');
|
||||
await canvas.findByText('Opportunities');
|
||||
await canvas.findByText('Rockets');
|
||||
await canvas.findByText('Linkedin');
|
||||
await canvas.findByText('All companies (v2)');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,7 +34,9 @@ export const WorkflowRunActionEffect = () => {
|
||||
position: index,
|
||||
Icon: IconSettingsAutomation,
|
||||
onClick: async () => {
|
||||
await runWorkflowVersion(activeWorkflowVersion.id);
|
||||
await runWorkflowVersion({
|
||||
workflowVersionId: activeWorkflowVersion.id,
|
||||
});
|
||||
|
||||
enqueueSnackBar('', {
|
||||
variant: SnackBarVariant.Success,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
||||
@@ -35,7 +37,8 @@ export const DeleteRecordsActionEffect = ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { favorites, deleteFavorite } = useFavorites();
|
||||
const favorites = useFavorites();
|
||||
const deleteFavorite = useDeleteFavorite();
|
||||
|
||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
@@ -45,8 +48,13 @@ export const DeleteRecordsActionEffect = ({
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const contextStoreFilters = useRecoilComponentValueV2(
|
||||
contextStoreFiltersComponentState,
|
||||
);
|
||||
|
||||
const graphqlFilter = computeContextStoreFilters(
|
||||
contextStoreTargetedRecordsRule,
|
||||
contextStoreFilters,
|
||||
objectMetadataItem,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
displayedExportProgress,
|
||||
useExportRecordData,
|
||||
} from '@/action-menu/hooks/useExportRecordData';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { IconDatabaseExport } from 'twenty-ui';
|
||||
|
||||
import {
|
||||
displayedExportProgress,
|
||||
useExportRecords,
|
||||
} from '@/object-record/record-index/export/hooks/useExportRecords';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const ExportRecordsActionEffect = ({
|
||||
@@ -22,7 +22,7 @@ export const ExportRecordsActionEffect = ({
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
);
|
||||
|
||||
const { progress, download } = useExportRecordData({
|
||||
const { progress, download } = useExportRecords({
|
||||
delayMs: 100,
|
||||
objectMetadataItem,
|
||||
recordIndexId: objectMetadataItem.namePlural,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
@@ -21,7 +23,11 @@ export const ManageFavoritesActionEffect = ({
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const { favorites, createFavorite, deleteFavorite } = useFavorites();
|
||||
const favorites = useFavorites();
|
||||
|
||||
const createFavorite = useCreateFavorite();
|
||||
|
||||
const deleteFavorite = useDeleteFavorite();
|
||||
|
||||
const selectedRecordId =
|
||||
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 { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
const globalRecordActionEffects = [ExportRecordsActionEffect];
|
||||
const noSelectionRecordActionEffects = [ExportRecordsActionEffect];
|
||||
|
||||
const singleRecordActionEffects = [
|
||||
ManageFavoritesActionEffect,
|
||||
DeleteRecordsActionEffect,
|
||||
];
|
||||
|
||||
const multipleRecordActionEffects = [DeleteRecordsActionEffect];
|
||||
const multipleRecordActionEffects = [
|
||||
ExportRecordsActionEffect,
|
||||
DeleteRecordsActionEffect,
|
||||
];
|
||||
|
||||
export const RecordActionMenuEntriesSetter = () => {
|
||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
@@ -39,23 +42,18 @@ export const RecordActionMenuEntriesSetter = () => {
|
||||
}
|
||||
|
||||
const actions =
|
||||
contextStoreNumberOfSelectedRecords === 1
|
||||
? singleRecordActionEffects
|
||||
: multipleRecordActionEffects;
|
||||
contextStoreNumberOfSelectedRecords === 0
|
||||
? noSelectionRecordActionEffects
|
||||
: contextStoreNumberOfSelectedRecords === 1
|
||||
? singleRecordActionEffects
|
||||
: multipleRecordActionEffects;
|
||||
|
||||
return (
|
||||
<>
|
||||
{globalRecordActionEffects.map((ActionEffect, index) => (
|
||||
<ActionEffect
|
||||
key={index}
|
||||
position={index}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
))}
|
||||
{actions.map((ActionEffect, index) => (
|
||||
<ActionEffect
|
||||
key={index}
|
||||
position={globalRecordActionEffects.length + index}
|
||||
position={index}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -65,7 +65,10 @@ export const WorkflowRunRecordActionEffect = ({
|
||||
return;
|
||||
}
|
||||
|
||||
await runWorkflowVersion(activeWorkflowVersion.id, selectedRecord);
|
||||
await runWorkflowVersion({
|
||||
workflowVersionId: activeWorkflowVersion.id,
|
||||
payload: selectedRecord,
|
||||
});
|
||||
|
||||
enqueueSnackBar('', {
|
||||
variant: SnackBarVariant.Success,
|
||||
|
||||
@@ -10,10 +10,10 @@ import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDro
|
||||
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import { MenuItem } from 'twenty-ui';
|
||||
|
||||
type StyledContainerProps = {
|
||||
position: PositionType;
|
||||
|
||||
@@ -11,16 +11,16 @@ import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBa
|
||||
|
||||
export const RecordShowActionMenu = ({
|
||||
isFavorite,
|
||||
handleFavoriteButtonClick,
|
||||
record,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
handleFavoriteButtonClick,
|
||||
}: {
|
||||
isFavorite: boolean;
|
||||
handleFavoriteButtonClick: () => void;
|
||||
record: ObjectRecord | undefined;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectNameSingular: string;
|
||||
handleFavoriteButtonClick: () => void;
|
||||
}) => {
|
||||
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
||||
contextStoreCurrentObjectMetadataIdComponentState,
|
||||
@@ -40,10 +40,10 @@ export const RecordShowActionMenu = ({
|
||||
<RecordShowPageBaseHeader
|
||||
{...{
|
||||
isFavorite,
|
||||
handleFavoriteButtonClick,
|
||||
record,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
handleFavoriteButtonClick,
|
||||
}}
|
||||
/>
|
||||
<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 { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
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 { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
@@ -21,7 +21,7 @@ export const RecordShowRightDrawerActionMenu = () => {
|
||||
onActionExecutedCallback: () => {},
|
||||
}}
|
||||
>
|
||||
<RecordShowRightDrawerActionMenuBar />
|
||||
<RightDrawerActionMenuDropdown />
|
||||
<ActionMenuConfirmationModals />
|
||||
<RecordActionMenuEntriesSetter />
|
||||
<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 { 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 { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
|
||||
import { userEvent, waitFor, within } from '@storybook/test';
|
||||
import {
|
||||
ComponentDecorator,
|
||||
IconFileExport,
|
||||
IconHeart,
|
||||
IconTrash,
|
||||
MenuItemAccent,
|
||||
} from 'twenty-ui';
|
||||
|
||||
const deleteMock = jest.fn();
|
||||
const addToFavoritesMock = jest.fn();
|
||||
const exportMock = jest.fn();
|
||||
|
||||
const meta: Meta<typeof RecordShowRightDrawerActionMenuBar> = {
|
||||
title: 'Modules/ActionMenu/RecordShowRightDrawerActionMenuBar',
|
||||
component: RecordShowRightDrawerActionMenuBar,
|
||||
const meta: Meta<typeof RightDrawerActionMenuDropdown> = {
|
||||
title: 'Modules/ActionMenu/RightDrawerActionMenuDropdown',
|
||||
component: RightDrawerActionMenuDropdown,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<RecoilRoot
|
||||
@@ -98,7 +98,7 @@ const meta: Meta<typeof RecordShowRightDrawerActionMenuBar> = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RecordShowRightDrawerActionMenuBar>;
|
||||
type Story = StoryObj<typeof RightDrawerActionMenuDropdown>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
@@ -113,12 +113,21 @@ export const WithButtonClicks: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
let actionButton = await canvas.findByText('Actions');
|
||||
await userEvent.click(actionButton);
|
||||
|
||||
const deleteButton = await canvas.findByText('Delete');
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
actionButton = await canvas.findByText('Actions');
|
||||
await userEvent.click(actionButton);
|
||||
|
||||
const addToFavoritesButton = await canvas.findByText('Add to favorites');
|
||||
await userEvent.click(addToFavoritesButton);
|
||||
|
||||
actionButton = await canvas.findByText('Actions');
|
||||
await userEvent.click(actionButton);
|
||||
|
||||
const exportButton = await canvas.findByText('Export');
|
||||
await userEvent.click(exportButton);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
import { IconComponent } from 'twenty-ui';
|
||||
|
||||
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
|
||||
import { IconComponent, MenuItemAccent } from 'twenty-ui';
|
||||
|
||||
export type ActionMenuEntry = {
|
||||
type: 'standard' | 'workflow-run';
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
RightDrawerHotkeyScope.RightDrawer,
|
||||
[],
|
||||
{
|
||||
preventDefault: false,
|
||||
},
|
||||
);
|
||||
|
||||
const handleBlockEditorFocus = () => {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
import { IconPlus, MenuItemAvatar } from 'twenty-ui';
|
||||
|
||||
export const MessageThreadSubscriberDropdownAddSubscriberMenuItem = ({
|
||||
workspaceMember,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { offset } from '@floating-ui/react';
|
||||
import { IconMinus, IconPlus } from 'twenty-ui';
|
||||
import { IconMinus, IconPlus, MenuItem, MenuItemAvatar } from 'twenty-ui';
|
||||
|
||||
import { MessageThreadSubscriberDropdownAddSubscriber } from '@/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriber';
|
||||
import { MessageThreadSubscribersChip } from '@/activities/emails/components/MessageThreadSubscribersChip';
|
||||
@@ -10,8 +10,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID =
|
||||
|
||||
@@ -4,13 +4,12 @@ import {
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
LightIconButton,
|
||||
MenuItem,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
|
||||
type AttachmentDropdownProps = {
|
||||
onDownload: () => void;
|
||||
@@ -50,27 +49,26 @@ export const AttachmentDropdown = ({
|
||||
clickableComponent={
|
||||
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||
}
|
||||
dropdownMenuWidth={160}
|
||||
dropdownComponents={
|
||||
<DropdownMenu width="160px">
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
text="Download"
|
||||
LeftIcon={IconDownload}
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Rename"
|
||||
LeftIcon={IconPencil}
|
||||
onClick={handleRename}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Delete"
|
||||
accent="danger"
|
||||
LeftIcon={IconTrash}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
text="Download"
|
||||
LeftIcon={IconDownload}
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Rename"
|
||||
LeftIcon={IconPencil}
|
||||
onClick={handleRename}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Delete"
|
||||
accent="danger"
|
||||
LeftIcon={IconTrash}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect';
|
||||
import { ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect';
|
||||
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
@@ -287,6 +288,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
<ActivityTargetInlineCellEditModeMultiRecordsEffect
|
||||
selectedObjectRecordIds={selectedTargetObjectIds}
|
||||
/>
|
||||
<ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect />
|
||||
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
|
||||
</RelationPickerScope>
|
||||
</StyledSelectContainer>
|
||||
|
||||
@@ -84,6 +84,7 @@ const mocks: MockedResponse[] = [
|
||||
companyId
|
||||
createdAt
|
||||
deletedAt
|
||||
favoriteFolderId
|
||||
id
|
||||
noteId
|
||||
opportunityId
|
||||
|
||||
@@ -26,8 +26,6 @@ const StyledTimelineContainer = styled.div`
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: flex-start;
|
||||
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(8)});
|
||||
`;
|
||||
|
||||
export const EventList = ({ events, targetableObject }: EventListProps) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TimelineActivity } from '@/activities/timeline-activities/types/Timelin
|
||||
import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
@@ -62,6 +63,7 @@ const StyledSummary = styled.summary`
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledItemContainer = styled.div<{ isMarginBottom?: boolean }>`
|
||||
@@ -77,6 +79,9 @@ const StyledItemContainer = styled.div<{ isMarginBottom?: boolean }>`
|
||||
`;
|
||||
|
||||
const StyledItemTitleDate = styled.div`
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
display: none;
|
||||
}
|
||||
align-items: flex-start;
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
AnimatedPlaceholderEmptyTextContainer,
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
||||
MOBILE_VIEWPORT,
|
||||
} from 'twenty-ui';
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
@@ -31,6 +32,11 @@ const StyledMainContainer = styled.div`
|
||||
padding-right: ${({ theme }) => theme.spacing(6)};
|
||||
padding-left: ${({ theme }) => theme.spacing(6)};
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
}
|
||||
`;
|
||||
|
||||
export const TimelineActivities = ({
|
||||
|
||||
@@ -16,6 +16,10 @@ const StyledLinkedActivity = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const StyledEventRowItemText = styled.span`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Card } from 'twenty-ui';
|
||||
import { Card, MOBILE_VIEWPORT } from 'twenty-ui';
|
||||
|
||||
type EventCardProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -16,6 +16,10 @@ const StyledCardContainer = styled.div`
|
||||
width: 400px;
|
||||
padding: ${({ theme }) => theme.spacing(2)} 0px
|
||||
${({ theme }) => theme.spacing(1)} 0px;
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
width: 300px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
|
||||
@@ -20,7 +20,10 @@ const StyledEventFieldDiffContainer = styled.div`
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: 24px;
|
||||
width: 380px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledEmptyValue = styled.div`
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip';
|
||||
import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
|
||||
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
|
||||
import { WebhookAnalyticsTooltip } from '@/analytics/components/WebhookAnalyticsTooltip';
|
||||
import { ANALYTICS_GRAPH_DESCRIPTION_MAP } from '@/analytics/constants/AnalyticsGraphDescriptionMap';
|
||||
import { ANALYTICS_GRAPH_TITLE_MAP } from '@/analytics/constants/AnalyticsGraphTitleMap';
|
||||
import { useGraphData } from '@/analytics/hooks/useGraphData';
|
||||
import { analyticsGraphDataComponentState } from '@/analytics/states/analyticsGraphDataComponentState';
|
||||
import { AnalyticsComponentProps as AnalyticsActivityGraphProps } from '@/analytics/types/AnalyticsComponentProps';
|
||||
import { computeAnalyticsGraphDataFunction } from '@/analytics/utils/computeAnalyticsGraphDataFunction';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
import { Section } from '@react-email/components';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useId, useState } from 'react';
|
||||
import { H2Title } from 'twenty-ui';
|
||||
|
||||
export type NivoLineInput = {
|
||||
id: string | number;
|
||||
color?: string;
|
||||
data: Array<{
|
||||
x: number | string | Date;
|
||||
y: number | string | Date;
|
||||
}>;
|
||||
};
|
||||
const StyledGraphContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
@@ -33,34 +29,38 @@ const StyledTitleContainer = styled.div`
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
type SettingsDevelopersWebhookUsageGraphProps = {
|
||||
webhookId: string;
|
||||
};
|
||||
|
||||
export const SettingsDevelopersWebhookUsageGraph = ({
|
||||
webhookId,
|
||||
}: SettingsDevelopersWebhookUsageGraphProps) => {
|
||||
const webhookGraphData = useRecoilValue(webhookGraphDataState);
|
||||
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
|
||||
export const AnalyticsActivityGraph = ({
|
||||
recordId,
|
||||
endpointName,
|
||||
}: AnalyticsActivityGraphProps) => {
|
||||
const [analyticsGraphData, setAnalyticsGraphData] = useRecoilComponentStateV2(
|
||||
analyticsGraphDataComponentState,
|
||||
);
|
||||
const theme = useTheme();
|
||||
|
||||
const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
|
||||
'7D' | '1D' | '12H' | '4H'
|
||||
>('7D');
|
||||
|
||||
const { fetchGraphData } = useGraphData(webhookId);
|
||||
const { fetchGraphData } = useGraphData({
|
||||
recordId,
|
||||
endpointName,
|
||||
});
|
||||
|
||||
const transformDataFunction = computeAnalyticsGraphDataFunction(endpointName);
|
||||
|
||||
const dropdownId = useId();
|
||||
return (
|
||||
<>
|
||||
{webhookGraphData.length ? (
|
||||
{analyticsGraphData.length ? (
|
||||
<Section>
|
||||
<StyledTitleContainer>
|
||||
<H2Title
|
||||
title="Activity"
|
||||
description="See your webhook activity over time"
|
||||
title={`${ANALYTICS_GRAPH_TITLE_MAP[endpointName]}`}
|
||||
description={`${ANALYTICS_GRAPH_DESCRIPTION_MAP[endpointName]}`}
|
||||
/>
|
||||
<Select
|
||||
dropdownId="test-id-webhook-graph"
|
||||
dropdownId={dropdownId}
|
||||
value={windowLengthGraphOption}
|
||||
options={[
|
||||
{ value: '7D', label: 'This week' },
|
||||
@@ -71,7 +71,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
||||
onChange={(windowLengthGraphOption) => {
|
||||
setWindowLengthGraphOption(windowLengthGraphOption);
|
||||
fetchGraphData(windowLengthGraphOption).then((graphInput) => {
|
||||
setWebhookGraphData(graphInput);
|
||||
setAnalyticsGraphData(transformDataFunction(graphInput));
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -79,10 +79,12 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
||||
|
||||
<StyledGraphContainer>
|
||||
<ResponsiveLine
|
||||
data={webhookGraphData}
|
||||
data={analyticsGraphData}
|
||||
curve={'monotoneX'}
|
||||
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={{
|
||||
text: {
|
||||
fill: theme.font.color.light,
|
||||
@@ -149,7 +151,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
||||
type: 'linear',
|
||||
}}
|
||||
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,
|
||||
tickPadding: 5,
|
||||
tickSize: 6,
|
||||
@@ -167,9 +169,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
||||
useMesh={true}
|
||||
enableSlices={false}
|
||||
enableCrosshair={false}
|
||||
tooltip={({ point }) => (
|
||||
<SettingsDevelopersWebhookTooltip point={point} />
|
||||
)}
|
||||
tooltip={({ point }) => <WebhookAnalyticsTooltip point={point} />} // later add a condition to get different tooltips
|
||||
/>
|
||||
</StyledGraphContainer>
|
||||
</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`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
`;
|
||||
type SettingsDevelopersWebhookTooltipProps = {
|
||||
type WebhookAnalyticsTooltipProps = {
|
||||
point: Point;
|
||||
};
|
||||
export const SettingsDevelopersWebhookTooltip = ({
|
||||
export const WebhookAnalyticsTooltip = ({
|
||||
point,
|
||||
}: SettingsDevelopersWebhookTooltipProps): ReactElement => {
|
||||
}: WebhookAnalyticsTooltipProps): ReactElement => {
|
||||
const { timeFormat, timeZone } = useContext(UserContext);
|
||||
const windowInterval = new Date(point.data.x);
|
||||
const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
|
||||
@@ -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