Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e44b085609 |
@@ -1,99 +0,0 @@
|
||||
# Twenty Development Rules
|
||||
|
||||
## General
|
||||
- Twenty is an open source CRM built with Typescript (React, NestJS)
|
||||
|
||||
### Main Packages / folders
|
||||
- packages/twenty-front: Main Frontend (React)
|
||||
- packages/twenty-server: Main Backend (NestJS)
|
||||
- packages/twenty-website: Marketing website + docs (NextJS)
|
||||
- packages/twenty-ui: UI library (React)
|
||||
- packages/twenty-shared: Shared utils, constants, types
|
||||
|
||||
### Development Stack
|
||||
- Package Manager: yarn
|
||||
- Monorepo: nx
|
||||
- Database: PostgreSQL + TypeORM (core, metadata schemas)
|
||||
- State Management: Recoil
|
||||
- Data Management: Apollo / GraphQL
|
||||
- Cache: redis
|
||||
- Auth: JWT
|
||||
- Queue: BullMQ
|
||||
- Storage: S3/local filesystem
|
||||
- Testing Backend: Jest, Supertest
|
||||
- Testing Frontend: Jest, Storybook, MSW
|
||||
- Testing E2E: Playwright
|
||||
|
||||
## Styling
|
||||
- Use @emotion/styled, never CSS classes/Tailwind
|
||||
- Prefix styled components with 'Styled'
|
||||
- Keep styled components at top of file
|
||||
- Use Theme object for colors/spacing/typography
|
||||
- Use Theme values instead of px/rem
|
||||
- Use mq helper for media queries
|
||||
|
||||
## TypeScript
|
||||
- No 'any' type - use proper typing
|
||||
- Use type over interface
|
||||
- String literals over enums (except GraphQL)
|
||||
- Props suffix for component props types
|
||||
- Use type inference when possible
|
||||
- Enable strict mode and noImplicitAny
|
||||
|
||||
## React
|
||||
- Only functional components
|
||||
- Named exports only
|
||||
- No useEffect, prefer event handlers
|
||||
- Small focused components
|
||||
- Proper prop naming (onClick, isActive)
|
||||
- Destructure props with types
|
||||
|
||||
## State Management
|
||||
- Recoil for global state
|
||||
- Apollo Client for GraphQL/server state
|
||||
- Atoms in states/ directory
|
||||
- Multiple small atoms over prop drilling
|
||||
- No useRef for state
|
||||
- Extract data fetching to siblings
|
||||
- useRecoilValue/useRecoilState appropriately
|
||||
|
||||
## File Structure
|
||||
- One component per file
|
||||
- Features in modules/
|
||||
- Hooks in hooks/
|
||||
- States in states/
|
||||
- Types in types/
|
||||
- PascalCase components, camelCase others
|
||||
|
||||
## Translation
|
||||
- Use @lingui/react/macro
|
||||
- Use <Trans> within components
|
||||
- Use t`string` elsewhere (from useLingui hook)
|
||||
- Don't translate metadata (field names, object names, etc)
|
||||
- Don't translate mocks
|
||||
|
||||
## Code Style
|
||||
- Early returns
|
||||
- No nested ternaries
|
||||
- No else-if
|
||||
- Optional chaining over &&
|
||||
- Small focused functions
|
||||
- Clear variable names
|
||||
- No console.logs in commits
|
||||
- Few comments, prefer code readability
|
||||
|
||||
## GraphQL
|
||||
- Use gql tag
|
||||
- Separate operation files
|
||||
- Proper codegen typing
|
||||
- Consistent naming (getX, updateX)
|
||||
- Use fragments
|
||||
- Use generated types
|
||||
|
||||
## Testing
|
||||
- Test every feature
|
||||
- React Testing Library
|
||||
- Test behavior not implementation
|
||||
- Mock external deps
|
||||
- Use data-testid
|
||||
- Follow naming conventions
|
||||
+4
-18
@@ -1,14 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['plugin:prettier/recommended', 'plugin:lingui/recommended'],
|
||||
plugins: [
|
||||
'@nx',
|
||||
'prefer-arrow',
|
||||
'import',
|
||||
'unused-imports',
|
||||
'unicorn',
|
||||
'lingui',
|
||||
],
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
plugins: ['@nx', 'prefer-arrow', 'import', 'unused-imports', 'unicorn'],
|
||||
rules: {
|
||||
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||
'no-console': ['warn', { allow: ['group', 'groupCollapsed', 'groupEnd'] }],
|
||||
@@ -36,10 +29,6 @@ module.exports = {
|
||||
sourceTag: 'scope:frontend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:zapier',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -81,6 +70,7 @@ module.exports = {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
@@ -106,11 +96,7 @@ module.exports = {
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'*.spec.@(ts|tsx|js|jsx)',
|
||||
'*.integration-spec.@(ts|tsx|js|jsx)',
|
||||
'*.test.@(ts|tsx|js|jsx)',
|
||||
],
|
||||
files: ['*.spec.@(ts|tsx|js|jsx)', '*.test.@(ts|tsx|js|jsx)'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: oss.gg hack submission 🕹️
|
||||
description: "Submit your contribution for the for the oss.gg hackathon"
|
||||
title: "[🕹️]"
|
||||
labels: 🕹️ oss.gg, player submission, hacktoberfest
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: contribution-name
|
||||
attributes:
|
||||
label: What side quest or challenge are you solving?
|
||||
description: Add the name of the side quest or challenge.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: points
|
||||
attributes:
|
||||
label: Points
|
||||
description: How many points are assigned to this contribution?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: What's the task your performed?
|
||||
validations:
|
||||
- type: textarea
|
||||
id: proof
|
||||
attributes:
|
||||
label: Provide proof that you've completed the task
|
||||
description: Screenshots, loom recordings, links to the content you shared or interacted with.
|
||||
validations:
|
||||
required: true
|
||||
@@ -2,11 +2,14 @@ name: Nx Affected CI
|
||||
inputs:
|
||||
parallel:
|
||||
required: false
|
||||
default: '3'
|
||||
types: [number]
|
||||
default: 3
|
||||
tag:
|
||||
required: false
|
||||
types: [string]
|
||||
tasks:
|
||||
required: true
|
||||
types: [string]
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Restore cache
|
||||
inputs:
|
||||
key:
|
||||
required: true
|
||||
description: Prefix to the cache key
|
||||
additional-paths:
|
||||
required: false
|
||||
outputs:
|
||||
cache-primary-key:
|
||||
description: actions/cache/restore cache-primary-key outputs proxy
|
||||
value: ${{ steps.restore-cache.outputs.cache-primary-key }}
|
||||
cache-hit:
|
||||
description: String bool indicating whether cache has been directly or indirectly hit
|
||||
value: ${{ steps.restore-cache.outputs.cache-hit == 'true' || steps.restore-cache.outputs.cache-matched-key != '' }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Cache primary key builder
|
||||
id: cache-primary-key-builder
|
||||
shell: bash
|
||||
run: |
|
||||
echo "CACHE_PRIMARY_KEY_PREFIX=${{ inputs.key }}-${{ github.ref_name }}" >> "${GITHUB_OUTPUT}"
|
||||
- name: Restore cache
|
||||
uses: actions/cache/restore@v4
|
||||
id: restore-cache
|
||||
with:
|
||||
key: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-${{ github.sha }}
|
||||
restore-keys: ${{ steps.cache-primary-key-builder.outputs.CACHE_PRIMARY_KEY_PREFIX }}-
|
||||
path: |
|
||||
.cache
|
||||
.nx/cache
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
${{ inputs.additional-paths }}
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Save cache
|
||||
inputs:
|
||||
key:
|
||||
required: true
|
||||
description: Primary key to the cache, should be retrieved from `cache-restore` composite action outputs.
|
||||
additional-paths:
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Save cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: ${{ inputs.key }}
|
||||
path: |
|
||||
.cache
|
||||
.nx/cache
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
${{ inputs.additional-paths }}
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Restore Tasks Cache CI
|
||||
inputs:
|
||||
tag:
|
||||
required: false
|
||||
types: [string]
|
||||
tasks:
|
||||
required: false
|
||||
types: [string]
|
||||
default: all
|
||||
suffix:
|
||||
required: false
|
||||
types: [string]
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Compute tasks key
|
||||
id: tasks-key
|
||||
shell: bash
|
||||
run: echo "key=${{ inputs.tasks }}" | tr , - >> $GITHUB_OUTPUT
|
||||
- name: Restore tasks cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
.nx/cache
|
||||
node_modules/.cache
|
||||
packages/*/node_modules/.cache
|
||||
key: tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
tasks-cache-${{ github.ref_name }}-${{ inputs.tag }}-${{ steps.tasks-key.outputs.key }}${{ inputs.suffix }}-
|
||||
@@ -1,43 +1,23 @@
|
||||
name: Yarn Install
|
||||
inputs:
|
||||
node-version:
|
||||
required: false
|
||||
default: '18'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache primary key builder
|
||||
id: globals
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ACTION_SHELL=bash" >> "${GITHUB_OUTPUT}"
|
||||
echo "CACHE_KEY_PREFIX=node_modules-cache-node-${{ inputs.node-version }}-${{ hashFiles('yarn.lock') }}" >> "${GITHUB_OUTPUT}"
|
||||
echo 'PATH_TO_CACHE<<EOF' >> $GITHUB_OUTPUT
|
||||
echo "node_modules" >> $GITHUB_OUTPUT
|
||||
echo "packages/*/node_modules" >> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
- name: Setup Node.js and get yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
- name: Restore node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache/restore@v4
|
||||
node-version: "18"
|
||||
cache: yarn
|
||||
- name: Cache node modules
|
||||
id: node-modules-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-${{github.sha}}
|
||||
restore-keys: ${{ steps.globals.outputs.CACHE_KEY_PREFIX }}-
|
||||
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
|
||||
path: |
|
||||
node_modules
|
||||
packages/*/node_modules
|
||||
key: root-node_modules-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: root-node_modules-
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
|
||||
shell: ${{ steps.globals.outputs.ACTION_SHELL }}
|
||||
run: |
|
||||
yarn config set enableHardenedMode true
|
||||
yarn --immutable --check-cache
|
||||
- name: Save cache
|
||||
if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' && steps.cache-node-modules.outputs.cache-matched-key == '' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: ${{ steps.cache-node-modules.outputs.cache-primary-key }}
|
||||
path: ${{ steps.globals.outputs.PATH_TO_CACHE }}
|
||||
|
||||
shell: bash
|
||||
run: yarn --immutable --check-cache
|
||||
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Changed files reusable workflow
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
files:
|
||||
required: true
|
||||
type: string
|
||||
outputs:
|
||||
any_changed:
|
||||
value: ${{ jobs.changed-files.outputs.any_changed }}
|
||||
|
||||
jobs:
|
||||
changed-files:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
any_changed: ${{ steps.changed-files.outputs.any_changed }}
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: ${{ inputs.files }}
|
||||
@@ -3,23 +3,15 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
|
||||
pull_request:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-chrome-extension/**
|
||||
chrome-extension-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -33,16 +25,23 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-chrome-extension/**
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Chrome Extension / Run build
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx build twenty-chrome-extension
|
||||
ci-chrome-extension-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, chrome-extension-build]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Mark as Valid if No Changes
|
||||
if: steps.changed-files.outputs.changed != 'true'
|
||||
run: |
|
||||
echo "No relevant changes detected. Marking as valid."
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
name: CI Demo check
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 7,19 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/twenty-e2e-testing
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
id: test
|
||||
run: yarn playwright test --grep "@demo-only"
|
||||
|
||||
- name: Upload report after tests
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 90
|
||||
|
||||
- name: Send Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: 'Demo check ${{ steps.test.outcome }} - check ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
|
||||
- name: Send email if demo is not working
|
||||
if: steps.test.outcome == 'failure'
|
||||
uses: dawidd6/action-send-mail@v3.12.0
|
||||
with:
|
||||
connection_url: ${{ secrets.MAIL_CONNECTION }}
|
||||
server_address: smtp.gmail.com
|
||||
server_port: 465
|
||||
secure: true
|
||||
username: ${{ secrets.MAIL_USERNAME }}
|
||||
subject: 'Demo is not working'
|
||||
from: 'Github CI Demo check'
|
||||
to: ${{ secrets.RECIPIENTS }}
|
||||
body: '<a href="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}">Link</a>'
|
||||
priority: high
|
||||
@@ -1,133 +0,0 @@
|
||||
name: CI E2E Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/**
|
||||
playwright.config.ts
|
||||
.github/workflows/ci-e2e.yaml
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true' && ( github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-e2e')))
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
# https://github.com/actions/runner-images/issues/70#issuecomment-589562148
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
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:
|
||||
- 6379:6379
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Check system resources
|
||||
run: |
|
||||
echo "Available memory:"
|
||||
free -h
|
||||
echo "Available disk space:"
|
||||
df -h
|
||||
echo "CPU info:"
|
||||
lscpu
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx nx setup twenty-e2e-testing
|
||||
|
||||
- name: Setup environment files
|
||||
run: |
|
||||
cp packages/twenty-front/.env.example packages/twenty-front/.env
|
||||
npx nx reset:env twenty-server
|
||||
|
||||
- name: Build frontend
|
||||
run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
|
||||
|
||||
- name: Build server
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Create and setup database
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
npx nx run twenty-server:database:reset
|
||||
|
||||
- name: Start server
|
||||
run: |
|
||||
npx nx start twenty-server &
|
||||
echo "Waiting for server to be ready..."
|
||||
timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'
|
||||
|
||||
- name: Start frontend
|
||||
run: |
|
||||
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
|
||||
echo "Waiting for frontend to be ready..."
|
||||
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
|
||||
|
||||
- name: Start worker
|
||||
run: |
|
||||
npx nx run twenty-server:worker &
|
||||
echo "Worker started"
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npx nx test twenty-e2e-testing
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: packages/twenty-e2e-testing/run_results/
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: packages/twenty-e2e-testing/playwright-report/
|
||||
retention-days: 30
|
||||
ci-e2e-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -0,0 +1,52 @@
|
||||
name: CI E2E Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
packages/**
|
||||
playwright.config.ts
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes detected. Marking as valid."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Install Playwright Browsers
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: yarn test:e2e companies
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
+120
-97
@@ -3,30 +3,17 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
STORYBOOK_BUILD_CACHE_KEY: storybook-build-depot-ubuntu-24.04-8-runner
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-front/**
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
front-sb-build:
|
||||
needs: [changed-files-check]
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
@@ -39,34 +26,46 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-front/**
|
||||
packages/twenty-ui/**
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Diagnostic disk space issue
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: df -h
|
||||
- name: Restore storybook build cache
|
||||
id: restore-storybook-build-cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
- name: Front / Restore Storybook Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
tag: scope:frontend
|
||||
tasks: storybook:build
|
||||
- name: Front / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Front / Build storybook
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx storybook:build twenty-front
|
||||
- name: Save storybook build cache
|
||||
uses: ./.github/workflows/actions/save-cache
|
||||
with:
|
||||
key: ${{ steps.restore-storybook-build-cache.outputs.cache-primary-key }}
|
||||
front-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: shipfox-8vcpu-ubuntu-2204
|
||||
needs: front-sb-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
storybook_scope: [modules, pages, performance]
|
||||
storybook_scope: [pages, modules]
|
||||
env:
|
||||
SHARD_COUNTER: 4
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
steps:
|
||||
@@ -74,57 +73,73 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-front/**
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Install Playwright
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: cd packages/twenty-front && npx playwright install
|
||||
- name: Restore storybook build cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
- name: Front / Restore Storybook Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
tag: scope:frontend
|
||||
tasks: storybook:build
|
||||
- name: Front / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Run storybook tests
|
||||
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }} --checkCoverage=false
|
||||
- name: Rename coverage file
|
||||
run: mv packages/twenty-front/coverage/storybook/coverage-storybook.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 1
|
||||
name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
|
||||
path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
merge-reports-and-check-coverage:
|
||||
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: depot-ubuntu-24.04-8
|
||||
needs: front-sb-test
|
||||
runs-on: shipfox-8vcpu-ubuntu-2204
|
||||
env:
|
||||
PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
|
||||
strategy:
|
||||
matrix:
|
||||
storybook_scope: [modules, pages, performance]
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fetch local actions
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
|
||||
merge-multiple: true
|
||||
path: coverage-artifacts
|
||||
- name: Merge coverage reports
|
||||
run: |
|
||||
mkdir -p ${{ env.PATH_TO_COVERAGE }}
|
||||
npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
|
||||
- name: Checking coverage
|
||||
run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
|
||||
files: |
|
||||
packages/twenty-front/**
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Install Playwright
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: cd packages/twenty-front && npx playwright install
|
||||
- name: Front / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Run storybook tests
|
||||
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: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://127.0.0.1:3000
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
@@ -133,27 +148,41 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Restore storybook build cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY }}
|
||||
files: |
|
||||
packages/twenty-front/**
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Front / Restore Storybook Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: storybook:build
|
||||
- name: Front / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
cd packages/twenty-front
|
||||
touch .env
|
||||
echo "REACT_APP_SERVER_BASE_URL: $REACT_APP_SERVER_BASE_URL" >> .env
|
||||
- name: Publish to Chromatic
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx run twenty-front:chromatic:ci
|
||||
front-task:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
TASK_CACHE_KEY: front-task-${{ matrix.task }}
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
@@ -166,41 +195,35 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Restore ${{ matrix.task }} cache
|
||||
id: restore-task-cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
key: ${{ env.TASK_CACHE_KEY }}
|
||||
files: |
|
||||
packages/twenty-front/**
|
||||
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed == 'false'
|
||||
run: echo "No relevant changes. Skipping CI."
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Front / Restore ${{ matrix.task }} task cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: ${{ matrix.task }}
|
||||
- name: Reset .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: reset:env
|
||||
- name: Run ${{ matrix.task }} task
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: ${{ matrix.task }}
|
||||
- name: Save ${{ matrix.task }} cache
|
||||
uses: ./.github/workflows/actions/save-cache
|
||||
with:
|
||||
key: ${{ steps.restore-task-cache.outputs.cache-primary-key }}
|
||||
ci-front-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs:
|
||||
[
|
||||
changed-files-check,
|
||||
front-task,
|
||||
front-chromatic-deployment,
|
||||
merge-reports-and-check-coverage,
|
||||
front-sb-test,
|
||||
front-sb-build,
|
||||
]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -5,28 +5,15 @@ on:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
SERVER_SETUP_CACHE_KEY: server-setup
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-server/**
|
||||
packages/twenty-emails/**
|
||||
packages/twenty-shared/**
|
||||
server-setup:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
services:
|
||||
@@ -34,9 +21,9 @@ jobs:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -53,79 +40,48 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Restore server setup
|
||||
id: restore-server-setup-cache
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-server/**
|
||||
packages/twenty-emails/**
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Server / Restore Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:backend
|
||||
- name: Server / Run lint & typecheck
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: lint,typecheck
|
||||
- name: Server / Build
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx build twenty-server
|
||||
- name: Server / Write .env
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx reset:env twenty-server
|
||||
- name: Server / Create DB
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
npx nx run twenty-server:database:init:prod
|
||||
npx nx run twenty-server:database:migrate:prod
|
||||
- name: Worker / Run
|
||||
run: |
|
||||
timeout 30s npx nx run twenty-server:worker || exit_code=$?
|
||||
if [ $exit_code -eq 124 ]; then
|
||||
# If timeout was reached (exit code 124), consider it a success
|
||||
exit 0
|
||||
elif [ $exit_code -ne 0 ]; then
|
||||
# If worker failed for other reasons, fail the build
|
||||
exit $exit_code
|
||||
fi
|
||||
- name: Server / Check for Pending Migrations
|
||||
run: |
|
||||
METADATA_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate metadata-migration-check -d src/database/typeorm/metadata/metadata.datasource.ts || true)
|
||||
|
||||
CORE_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate core-migration-check -d src/database/typeorm/core/core.datasource.ts || true)
|
||||
|
||||
METADATA_MIGRATION_FILE=$(ls packages/twenty-server/*metadata-migration-check.ts 2>/dev/null || echo "")
|
||||
CORE_MIGRATION_FILE=$(ls packages/twenty-server/*core-migration-check.ts 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$METADATA_MIGRATION_FILE" ] || [ -n "$CORE_MIGRATION_FILE" ]; then
|
||||
echo "::error::Unexpected migration files were generated. Please create a proper migration manually."
|
||||
echo "$METADATA_MIGRATION_OUTPUT"
|
||||
echo "$CORE_MIGRATION_OUTPUT"
|
||||
|
||||
rm -f packages/twenty-server/*metadata-migration-check.ts packages/twenty-server/*core-migration-check.ts
|
||||
|
||||
exit 1
|
||||
fi
|
||||
- name: GraphQL / Check for Pending Generation
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
GRAPHQL_GENERATE_OUTPUT=$(npx nx run twenty-front:graphql:generate || true)
|
||||
GRAPHQL_METADATA_OUTPUT=$(npx nx run twenty-front:graphql:generate --configuration=metadata || true)
|
||||
if [[ $GRAPHQL_GENERATE_OUTPUT == *"No changes detected"* && $GRAPHQL_METADATA_OUTPUT == *"No changes detected"* ]]; then
|
||||
echo "GraphQL generation check passed."
|
||||
else
|
||||
echo "::error::Unexpected GraphQL changes detected. Please run the required commands and commit the changes."
|
||||
echo "$GRAPHQL_GENERATE_OUTPUT"
|
||||
echo "$GRAPHQL_METADATA_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
- name: Save server setup
|
||||
uses: ./.github/workflows/actions/save-cache
|
||||
with:
|
||||
key: ${{ steps.restore-server-setup-cache.outputs.cache-primary-key }}
|
||||
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: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
needs: server-setup
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
@@ -134,13 +90,26 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Restore server setup
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
- name: Server / Run Tests
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-server/**
|
||||
packages/twenty-emails/**
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Server / Restore Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:backend
|
||||
- name: Server / Run Tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
@@ -148,16 +117,16 @@ jobs:
|
||||
|
||||
server-integration-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
runs-on: ubuntu-latest
|
||||
needs: server-setup
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -176,46 +145,33 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Update .env.test for billing
|
||||
run: |
|
||||
echo "IS_BILLING_ENABLED=true" >> .env.test
|
||||
echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
|
||||
echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
|
||||
echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
|
||||
- name: Restore server setup
|
||||
uses: ./.github/workflows/actions/restore-cache
|
||||
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
key: ${{ env.SERVER_SETUP_CACHE_KEY }}
|
||||
- name: Server / Build
|
||||
run: npx nx build twenty-server
|
||||
- name: Build dependencies
|
||||
run: |
|
||||
npx nx build twenty-shared
|
||||
npx nx build twenty-emails
|
||||
- name: Server / Create Test DB
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/test npx nx run twenty-server:database:init:prod
|
||||
PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/test npx nx run twenty-server:database:migrate:prod
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-server/**
|
||||
packages/twenty-emails/**
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Server / Restore Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
with:
|
||||
tag: scope:backend
|
||||
- name: Server / Run Integration Tests
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:backend
|
||||
tasks: 'test:integration --configuration=with-db-reset'
|
||||
tasks: "test:integration:with-db-reset"
|
||||
- name: Server / Upload reset-logs file
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reset-logs
|
||||
path: reset-logs.log
|
||||
ci-server-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: [changed-files-check, server-setup, server-test, server-integration-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
name: CI Shared
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-shared/**
|
||||
shared-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/workflows/actions/nx-affected
|
||||
with:
|
||||
tag: scope:frontend
|
||||
tasks: ${{ matrix.task }}
|
||||
ci-shared-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, shared-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -1,97 +1,92 @@
|
||||
name: 'Test Docker Compose'
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-docker/**
|
||||
docker-compose.yml
|
||||
test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run compose
|
||||
run: |
|
||||
echo "Patching docker-compose.yml..."
|
||||
# change image to localbuild using yq
|
||||
yq eval 'del(.services.server.image)' -i docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
|
||||
yq eval '.services.server.restart = "no"' -i docker-compose.yml
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-docker/**
|
||||
docker-compose.yml
|
||||
- name: Skip if no relevant changes
|
||||
if: steps.changed-files.outputs.any_changed != 'true'
|
||||
run: echo "No relevant changes detected. Marking as valid."
|
||||
|
||||
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-spilo/Dockerfile"' -i docker-compose.yml
|
||||
- name: Run compose
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
echo "Patching docker-compose.yml..."
|
||||
# change image to localbuild using yq
|
||||
yq eval 'del(.services.server.image)' -i docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i docker-compose.yml
|
||||
yq eval '.services.server.restart = "no"' -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 "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
|
||||
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-spilo/Dockerfile"' -i docker-compose.yml
|
||||
|
||||
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 "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 "PGPASSWORD_SUPERUSER=$(openssl rand -base64 32)" >>.env
|
||||
|
||||
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 "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 server to start..."
|
||||
count=0
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||
sleep 1;
|
||||
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 after 5 minutes"
|
||||
docker compose logs server
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting for server... (${count}/300s)"
|
||||
done
|
||||
working-directory: ./packages/twenty-docker/
|
||||
ci-test-docker-compose-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
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
|
||||
sleep 1;
|
||||
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 after 5 minutes"
|
||||
docker compose logs server
|
||||
exit 1
|
||||
fi
|
||||
echo "Still waiting for server... (${count}/300s)"
|
||||
done
|
||||
working-directory: ./packages/twenty-docker/
|
||||
|
||||
@@ -5,21 +5,14 @@ on:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
packages/twenty-website/**
|
||||
website-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
@@ -27,9 +20,9 @@ jobs:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
@@ -41,27 +34,31 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v11
|
||||
with:
|
||||
files: 'package.json, packages/twenty-website/**'
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Server / Create DB
|
||||
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
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
|
||||
run: npx nx database:migrate twenty-website
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
run: npx nx database:migrate twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://postgres:postgres@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://postgres:postgres@localhost:5432/default
|
||||
ci-website-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 1
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, website-build]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
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
|
||||
run: echo "No relevant changes detected. CI is valid."
|
||||
@@ -1,128 +0,0 @@
|
||||
# Pull down translations from Crowdin every two hours or when triggered manually.
|
||||
# When force_pull input is true, translations will be pulled regardless of compilation status.
|
||||
|
||||
name: 'Pull translations'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */2 * * *' # Every two hours.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_pull:
|
||||
description: 'Force pull translations regardless of compilation status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_call:
|
||||
inputs:
|
||||
force_pull:
|
||||
description: 'Force pull translations regardless of compilation status'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pull_translations:
|
||||
name: Pull translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
- name: Setup i18n branch
|
||||
run: |
|
||||
git fetch origin i18n || true
|
||||
git checkout -B i18n origin/i18n || git checkout -b i18n
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Build twenty-shared
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
# Strict mode fails if there are missing translations.
|
||||
- name: Compile translations
|
||||
id: compile_translations
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:compile --strict
|
||||
npx nx run twenty-emails:lingui:compile --strict
|
||||
npx nx run twenty-front:lingui:compile --strict
|
||||
continue-on-error: true
|
||||
|
||||
- name: Stash any changes before pulling translations
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add .
|
||||
git stash
|
||||
|
||||
- name: Pull translations from Crowdin
|
||||
if: inputs.force_pull || steps.compile_translations.outcome == 'failure'
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n
|
||||
commit_message: 'i18n - translations'
|
||||
pull_request_title: 'i18n - translations'
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
auto_approve_imported: false
|
||||
import_eq_suggestions: false
|
||||
download_sources: false
|
||||
push_sources: false
|
||||
skip_untranslated_strings: false
|
||||
skip_untranslated_files: false
|
||||
push_translations: true
|
||||
create_pull_request: true
|
||||
skip_ref_checkout: true
|
||||
dryrun_action: false
|
||||
github_base_url: 'github.com'
|
||||
github_user_name: 'Crowdin Bot'
|
||||
github_user_email: 'support+bot@crowdin.com'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CROWDIN_PROJECT_ID: '1'
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Compile translations
|
||||
if: inputs.force_pull || steps.compile_translations.outcome == 'failure'
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:compile
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
npx nx run twenty-front:lingui:compile
|
||||
|
||||
- name: Check and commit compiled files
|
||||
id: check_changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add .
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: compile translations [skip ci]"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
run: git push origin HEAD:i18n
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
run: gh pr create -B main -H i18n --title 'i18n - translations' --body 'Created by Github action' || true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,86 +0,0 @@
|
||||
name: 'Extract translations when there is a push to main, and upload them to Crowdin'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
extract_translations:
|
||||
name: Extract and upload translations
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
ref: main
|
||||
|
||||
- name: Setup i18n branch
|
||||
run: |
|
||||
git fetch origin i18n || true
|
||||
git checkout -B i18n origin/i18n || git checkout -b i18n
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Extract translations
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:extract
|
||||
npx nx run twenty-emails:lingui:extract
|
||||
npx nx run twenty-front:lingui:extract
|
||||
|
||||
- name: Compile translations
|
||||
run: |
|
||||
npx nx run twenty-server:lingui:compile
|
||||
npx nx run twenty-emails:lingui:compile
|
||||
npx nx run twenty-front:lingui:compile
|
||||
|
||||
- name: Check and commit any files created
|
||||
id: check_changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add .
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: extract translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changes_detected=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Push changes and create remote branch if needed
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
run: git push origin HEAD:i18n
|
||||
|
||||
- name: Upload missing translations
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: false
|
||||
localization_branch_name: i18n
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
env:
|
||||
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api
|
||||
CROWDIN_PROJECT_ID: 1
|
||||
|
||||
# Visit https://crowdin.com/settings#api-key to create this token
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Create a pull request
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
run: gh pr create -B main -H i18n --title 'i18n - translations' --body 'Created by Github action' || true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
-13
@@ -18,7 +18,6 @@
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.vercel
|
||||
.swc
|
||||
|
||||
**/**/logs/**
|
||||
|
||||
@@ -31,15 +30,3 @@ storybook-static
|
||||
test-results/
|
||||
dump.rdb
|
||||
.tinyb
|
||||
|
||||
.notes
|
||||
/data/
|
||||
/.devenv/
|
||||
/.direnv/
|
||||
/.pre-commit-config.yaml
|
||||
/.envrc
|
||||
/devenv.nix
|
||||
/flake.lock
|
||||
/flake.nix
|
||||
|
||||
.crowdin.yml
|
||||
Vendored
+1
-2
@@ -14,7 +14,6 @@
|
||||
"styled-components.vscode-styled-components",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"xyc.vscode-mdx-preview",
|
||||
"yoavbls.pretty-ts-errors",
|
||||
"ms-playwright.playwright"
|
||||
"yoavbls.pretty-ts-errors"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+7
-37
@@ -6,11 +6,12 @@
|
||||
"name": "twenty-server - start debug",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeExecutable": "yarn",
|
||||
"runtimeVersion": "18",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:start"
|
||||
"twenty-server:start",
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
@@ -21,11 +22,12 @@
|
||||
"name": "twenty-server - worker debug",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeExecutable": "yarn",
|
||||
"runtimeVersion": "18",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:worker"
|
||||
"twenty-server:worker",
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
@@ -43,44 +45,12 @@
|
||||
"run",
|
||||
"twenty-server:command",
|
||||
"my-command",
|
||||
"--my-parameter value"
|
||||
"--my-parameter value",
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"console": "internalConsole",
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server/"
|
||||
},
|
||||
{
|
||||
"name": "Playwright Test current file",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["nx", "test", "twenty-e2e-testing", "${file}"],
|
||||
"console": "integratedTerminal",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"envFile": "${workspaceFolder}/packages/twenty-e2e-testing/.env"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "twenty-server - debug integration test file (to launch with test file open)",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": [
|
||||
"nx",
|
||||
"run",
|
||||
"twenty-server:jest",
|
||||
"--",
|
||||
"--config",
|
||||
"./jest-integration.config.ts",
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/twenty-server",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"NODE_ENV": "test"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-5
@@ -46,9 +46,4 @@
|
||||
"**/.yarn": true,
|
||||
},
|
||||
"eslint.debug": true,
|
||||
"files.associations": {
|
||||
".cursorrules": "markdown"
|
||||
},
|
||||
"jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-8
@@ -24,10 +24,6 @@
|
||||
"name": "packages/twenty-emails",
|
||||
"path": "../packages/twenty-emails"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-shared",
|
||||
"path": "../packages/twenty-shared"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-server",
|
||||
"path": "../packages/twenty-server"
|
||||
@@ -44,10 +40,6 @@
|
||||
"name": "tools/eslint-rules",
|
||||
"path": "../tools/eslint-rules"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-e2e-testing",
|
||||
"path": "../packages/twenty-e2e-testing"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
enableHardenedMode: true
|
||||
|
||||
enableInlineHunks: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
@@ -2,17 +2,17 @@ postgres-on-docker:
|
||||
docker run -d \
|
||||
--name twenty_pg \
|
||||
-e PGUSER_SUPERUSER=postgres \
|
||||
-e PGPASSWORD_SUPERUSER=postgres \
|
||||
-e PGPASSWORD_SUPERUSER=twenty \
|
||||
-e ALLOW_NOSSL=true \
|
||||
-v twenty_db_data:/home/postgres/pgdata \
|
||||
-p 5432:5432 \
|
||||
twentycrm/twenty-postgres-spilo:latest
|
||||
@echo "Waiting for PostgreSQL to be ready..."
|
||||
@until docker exec twenty_pg psql -U postgres -d postgres \
|
||||
@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
|
||||
docker exec twenty_pg psql -U postgres -d postgres \
|
||||
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;"
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
</p>
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM </h3>
|
||||
<p align="center">Tailored to your unique business needs</p>
|
||||
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://twenty.com/developers">📚 Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a><p>
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://twenty.com/developers">📚 Documentation</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a><p>
|
||||
<br />
|
||||
|
||||
|
||||
@@ -21,33 +22,45 @@
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<br>
|
||||
|
||||
We’ve spent thousands of hours grappling with traditional CRMs like Pipedrive and Salesforce to align them with our business needs, only to end up frustrated — customizations are complex and the closed ecosystems of these platforms can feel restrictive.
|
||||
|
||||
We felt the need for a CRM platform that empowers rather than constrains. We believe the next great CRM will come from the open-source community. We’ve packed Twenty with powerful features to give you full control and help you run your business efficiently.
|
||||
|
||||
<br>
|
||||
|
||||
# Installation
|
||||
# Demo
|
||||
Go to <a href="https://demo.twenty.com/">demo.twenty.com</a> and login with the following credentials:
|
||||
|
||||
See:
|
||||
```
|
||||
email: tim@apple.dev
|
||||
password: Applecar2025
|
||||
```
|
||||
|
||||
See also:
|
||||
🚀 [Self-hosting](https://twenty.com/developers/section/self-hosting)
|
||||
🖥️ [Local Setup](https://twenty.com/developers/local-setup)
|
||||
|
||||
# Does the world need another CRM?
|
||||
# Why Choose Twenty?
|
||||
We understand that the CRM landscape is vast. So why should you choose us?
|
||||
|
||||
We built Twenty for three reasons:
|
||||
⛓️ **Full control, Full Freedom:** Contribute, self-host, fork. Break free from vendor lock-in and join us in shaping the open future of CRM.
|
||||
|
||||
**CRMs are too expensive, and users are trapped.** Companies use locked-in customer data to hike prices. It shouldn't be that way.
|
||||
📊 **Data, Your Way:** The days when the role of CRM platforms was to shift manual data entries to a database are over. Now, the data is already there. CRM 2.0 should be built around your data, allowing you to access and visualize any existing sources, not forcing you to retrofit your data into predefined objects on a remote cloud.
|
||||
|
||||
**A fresh start is required to build a better experience.** We can learn from past mistakes and craft a cohesive experience inspired by new UX patterns from tools like Notion, Airtable or Linear.
|
||||
🎨 **Effortlessly Intuitive:** We set out to create something that we ourselves would always enjoy using. The main application draws inspiration from Notion, a tool known for its user-friendly interface and customization capabilities.
|
||||
<br>
|
||||
|
||||
**We believe in Open-source and community.** Hundreds of developers are already building Twenty together. Once we have plugin capabilities, a whole ecosystem will grow around it.
|
||||
|
||||
<br>
|
||||
|
||||
# What You Can Do With Twenty
|
||||
We're currently developing Twenty's beta version.
|
||||
We're currently in the development phase of Twenty's alpha version.
|
||||
|
||||
Please feel free to flag any specific needs you have by creating an issue.
|
||||
Please feel free to flag any specific need you have need by creating an issue.
|
||||
|
||||
Below are a few features we have implemented to date:
|
||||
Below are some features we have implemented to date:
|
||||
|
||||
+ [Add, filter, sort, edit, and track customers](#add-filter-sort-edit-and-track-customers)
|
||||
+ [Create one or several opportunities for each company](#create-one-or-several-opportunities-for-each-company)
|
||||
@@ -136,27 +149,24 @@ Below are a few features we have implemented to date:
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
# Stack
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Nx](https://nx.dev/)
|
||||
- [NestJS](https://nestjs.com/), with [BullMQ](https://bullmq.io/), [PostgreSQL](https://www.postgresql.org/), [Redis](https://redis.io/)
|
||||
- [React](https://reactjs.org/), with [Recoil](https://recoiljs.org/) and [Emotion](https://emotion.sh/)
|
||||
- [Greptile](https://greptile.com) for code reviews.
|
||||
- [Lingui](https://lingui.dev/) and [Crowdin](https://twenty.crowdin.com/twenty) for translations.
|
||||
# What's In Store
|
||||
|
||||
Here’s what you can look forward to:
|
||||
|
||||
⏳ **Frequent updates:** We’re shipping fast! Expect regular updates and new features that enhance your experience.
|
||||
|
||||
🔗 **Extensibility:** We’re putting the power in your hands. Soon, you’ll have the tools to extend and customize Twenty with plugins and more.
|
||||
|
||||
<br>
|
||||
|
||||
# Join the Community
|
||||
|
||||
- Star the repo
|
||||
- Subscribe to releases (watch -> custom -> releases)
|
||||
- Join [discussions](https://github.com/twentyhq/twenty/discussions) and track [issues](https://github.com/twentyhq/twenty/issues)
|
||||
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
|
||||
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
#
|
||||
# Basic Crowdin CLI configuration
|
||||
# See https://crowdin.github.io/crowdin-cli/configuration for more information
|
||||
# See https://support.crowdin.com/developer/configuration-file/ for all available options
|
||||
#
|
||||
|
||||
#
|
||||
# Defines whether to preserve the original directory structure in the Crowdin project
|
||||
# Recommended to set to true
|
||||
#
|
||||
"preserve_hierarchy": true
|
||||
|
||||
#
|
||||
# Files configuration.
|
||||
# See https://support.crowdin.com/developer/configuration-file/ for all available options
|
||||
#
|
||||
files: [
|
||||
{
|
||||
#
|
||||
# Source files filter
|
||||
# e.g. "/resources/en/*.json"
|
||||
#
|
||||
"source": "**/en.po",
|
||||
|
||||
#
|
||||
# Translation files filter
|
||||
# e.g. "/resources/%two_letters_code%/%original_file_name%"
|
||||
#
|
||||
"translation": "%original_path%/%locale%.po",
|
||||
}
|
||||
]
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔧 Checking dependencies..."
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo -e "\t❌ Docker is not installed or not in PATH. Please install Docker first.\n\t\tSee https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
# Check if docker compose plugin is installed
|
||||
if ! docker compose version &>/dev/null; then
|
||||
echo -e "\t❌ Docker Compose is not installed or not in PATH (n.b. docker-compose is deprecated)\n\t\tUpdate docker or install docker-compose-plugin\n\t\tOn Linux: sudo apt-get install docker-compose-plugin\n\t\tSee https://docs.docker.com/compose/install/"
|
||||
exit 1
|
||||
fi
|
||||
# Check if docker is started
|
||||
if ! docker info &>/dev/null; then
|
||||
echo -e "\t❌ Docker is not running.\n\t\tPlease start Docker Desktop, Docker or check documentation at https://docs.docker.com/config/daemon/start/"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v curl &>/dev/null; then
|
||||
echo -e "\t❌ Curl is not installed or not in PATH.\n\t\tOn macOS: brew install curl\n\t\tOn Linux: sudo apt install curl"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if docker compose version is >= 2
|
||||
if [ "$(docker compose version --short | cut -d' ' -f3 | cut -d'.' -f1)" -lt 2 ]; then
|
||||
echo -e "\t❌ Docker Compose is outdated. Please update Docker Compose to version 2 or higher.\n\t\tSee https://docs.docker.com/compose/install/linux/"
|
||||
exit 1
|
||||
fi
|
||||
# Check if docker-compose is installed, if so issue a warning if version is < 2
|
||||
if command -v docker-compose &>/dev/null; then
|
||||
if [ "$(docker-compose version --short | cut -d' ' -f3 | cut -d'.' -f1)" -lt 2 ]; then
|
||||
echo -e "\n\t⚠️ 'docker-compose' is installed but outdated. Make sure to use 'docker compose' or to upgrade 'docker-compose' to version 2.\n\t\tSee https://docs.docker.com/compose/install/standalone/\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Catch errors
|
||||
set -e
|
||||
function on_exit {
|
||||
# $? is the exit status of the last command executed
|
||||
local exit_status=$?
|
||||
if [ $exit_status -ne 0 ]; then
|
||||
echo "❌ Something went wrong, exiting: $exit_status"
|
||||
fi
|
||||
}
|
||||
trap on_exit EXIT
|
||||
|
||||
# Use environment variables VERSION and BRANCH, with defaults if not set
|
||||
version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)}
|
||||
branch=${BRANCH:-$version}
|
||||
|
||||
echo "🚀 Using version $version and branch $branch"
|
||||
|
||||
dir_name="twenty"
|
||||
function ask_directory {
|
||||
read -p "📁 Enter the directory name to setup the project (default: $dir_name): " answer
|
||||
if [ -n "$answer" ]; then
|
||||
dir_name=$answer
|
||||
fi
|
||||
}
|
||||
|
||||
ask_directory
|
||||
|
||||
while [ -d "$dir_name" ]; do
|
||||
read -p "🚫 Directory '$dir_name' already exists. Do you want to overwrite it? (y/N) " answer
|
||||
if [ "$answer" = "y" ]; then
|
||||
break
|
||||
else
|
||||
ask_directory
|
||||
fi
|
||||
done
|
||||
|
||||
# Create a directory named twenty
|
||||
echo "📁 Creating directory '$dir_name'"
|
||||
mkdir -p "$dir_name" && cd "$dir_name" || { echo "❌ Failed to create/access directory '$dir_name'"; exit 1; }
|
||||
|
||||
# Copy twenty/packages/twenty-docker/docker-compose.yml in it
|
||||
echo -e "\t• Copying docker-compose.yml"
|
||||
curl -sLo docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/docker-compose.yml
|
||||
|
||||
# Copy twenty/packages/twenty-docker/.env.example to .env
|
||||
echo -e "\t• Setting up .env file"
|
||||
curl -sLo .env https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/.env.example
|
||||
|
||||
# Replace TAG=latest by TAG=<latest_release or version input>
|
||||
if [[ $(uname) == "Darwin" ]]; then
|
||||
# Running on macOS
|
||||
sed -i '' "s/TAG=latest/TAG=$version/g" .env
|
||||
else
|
||||
# Assuming Linux
|
||||
sed -i'' "s/TAG=latest/TAG=$version/g" .env
|
||||
fi
|
||||
|
||||
# Generate random strings for secrets
|
||||
echo "# === Randomly generated secrets ===" >>.env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "" >>.env
|
||||
echo "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
|
||||
echo "" >>.env
|
||||
echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env
|
||||
|
||||
echo -e "\t• .env configuration completed"
|
||||
|
||||
port=3000
|
||||
# Check if command nc is available
|
||||
if command -v nc &> /dev/null; then
|
||||
# Check if port 3000 is already in use, propose to change it
|
||||
while nc -zv localhost $port &>/dev/null; do
|
||||
read -p "🚫 Port $port is already in use. Do you want to use another port? (Y/n) " answer
|
||||
if [ "$answer" = "n" ]; then
|
||||
continue
|
||||
fi
|
||||
read -p "Enter a new port number: " new_port
|
||||
if [[ $(uname) == "Darwin" ]]; then
|
||||
sed -i '' "s/$port:$port/$new_port:$port/g" docker-compose.yml
|
||||
sed -E -i '' "s|^SERVER_URL=http://localhost:[0-9]+|SERVER_URL=http://localhost:$new_port|g" .env
|
||||
else
|
||||
sed -i'' "s/$port:$port/$new_port:$port/g" docker-compose.yml
|
||||
sed -E -i'' "s|^SERVER_URL=http://localhost:[0-9]+|SERVER_URL=http://localhost:$new_port|g" .env
|
||||
fi
|
||||
port=$new_port
|
||||
done
|
||||
fi
|
||||
|
||||
# Ask user if they want to start the project
|
||||
read -p "🚀 Do you want to start the project now? (Y/n) " answer
|
||||
if [ "$answer" = "n" ]; then
|
||||
echo "✅ Project setup completed. Run 'docker compose up -d' to start."
|
||||
exit 0
|
||||
else
|
||||
echo "🐳 Starting Docker containers..."
|
||||
docker compose up -d
|
||||
# Check if port is listening
|
||||
echo "Waiting for server to be healthy, it might take a few minutes while we initialize the database..."
|
||||
# Tail logs of the server until it's ready
|
||||
docker compose logs -f server &
|
||||
pid=$!
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||
sleep 1
|
||||
done
|
||||
kill $pid
|
||||
echo ""
|
||||
echo "✅ Server is up and running"
|
||||
fi
|
||||
|
||||
function ask_open_browser {
|
||||
read -p "🌐 Do you want to open the project in your browser? (Y/n) " answer
|
||||
if [ "$answer" = "n" ]; then
|
||||
echo "✅ Setup completed. Access your project at http://localhost:$port"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Ask user if they want to open the project
|
||||
# Running on macOS
|
||||
if [[ $(uname) == "Darwin" ]]; then
|
||||
ask_open_browser
|
||||
|
||||
open "http://localhost:$port"
|
||||
# Assuming Linux
|
||||
else
|
||||
# xdg-open is not installed, we could be running in a non gui environment
|
||||
if command -v xdg-open >/dev/null 2>&1; then
|
||||
ask_open_browser
|
||||
|
||||
xdg-open "http://localhost:$port"
|
||||
else
|
||||
echo "✅ Setup completed. Your project is available at http://localhost:$port"
|
||||
fi
|
||||
fi
|
||||
@@ -14,7 +14,6 @@
|
||||
"!{projectRoot}/**/tsconfig.spec.json",
|
||||
"!{projectRoot}/**/*.test.(ts|tsx)",
|
||||
"!{projectRoot}/**/*.spec.(ts|tsx)",
|
||||
"!{projectRoot}/**/*.integration-spec.ts",
|
||||
"!{projectRoot}/**/__tests__/*"
|
||||
],
|
||||
"production": [
|
||||
@@ -48,8 +47,7 @@
|
||||
"configurations": {
|
||||
"ci": { "cacheStrategy": "content" },
|
||||
"fix": { "fix": true }
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"fmt": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -65,8 +63,7 @@
|
||||
"configurations": {
|
||||
"ci": { "cacheStrategy": "content" },
|
||||
"fix": { "write": true }
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"typecheck": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -77,8 +74,7 @@
|
||||
},
|
||||
"configurations": {
|
||||
"watch": { "watch": true }
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
@@ -116,11 +112,10 @@
|
||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build --test",
|
||||
"command": "VITE_DISABLE_ESLINT_CHECKER=true storybook build",
|
||||
"output-dir": "storybook-static",
|
||||
"config-dir": ".storybook"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"storybook:serve:dev": {
|
||||
"executor": "nx:run-commands",
|
||||
@@ -152,14 +147,12 @@
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"commands": [
|
||||
"test-storybook --url http://localhost:{args.port} --maxWorkers=3 --coverage --coverageDirectory={args.coverageDir} --shard={args.shard}",
|
||||
"nx storybook:coverage {projectName} --coverageDir={args.coverageDir} --checkCoverage={args.checkCoverage}"
|
||||
"test-storybook --url http://localhost:{args.port} --maxWorkers=3 --coverage --coverageDirectory={args.coverageDir}",
|
||||
"nx storybook:coverage {projectName} --coverageDir={args.coverageDir}"
|
||||
],
|
||||
"shard": "1/1",
|
||||
"parallel": false,
|
||||
"coverageDir": "coverage/storybook",
|
||||
"port": 6006,
|
||||
"checkCoverage": true
|
||||
"port": 6006
|
||||
}
|
||||
},
|
||||
"storybook:test:no-coverage": {
|
||||
@@ -186,10 +179,9 @@
|
||||
"!{projectRoot}/coverage/storybook/coverage-storybook.json"
|
||||
],
|
||||
"options": {
|
||||
"command": "npx nyc report --reporter={args.reporter} --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage={args.checkCoverage} --cwd={projectRoot}",
|
||||
"command": "npx nyc report --reporter={args.reporter} --reporter=text-summary -t {args.coverageDir} --report-dir {args.coverageDir} --check-coverage --cwd={projectRoot}",
|
||||
"coverageDir": "coverage/storybook",
|
||||
"reporter": "lcov",
|
||||
"checkCoverage": true
|
||||
"reporter": "lcov"
|
||||
},
|
||||
"configurations": {
|
||||
"text": { "reporter": "text" }
|
||||
@@ -199,10 +191,8 @@
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"commands": [
|
||||
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --shard={args.shard} --checkCoverage={args.checkCoverage} --port={args.port} --configuration={args.scope}'"
|
||||
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port} --configuration={args.performance}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'"
|
||||
],
|
||||
"shard": "1/1",
|
||||
"checkCoverage": true,
|
||||
"port": 6006
|
||||
}
|
||||
},
|
||||
|
||||
+13
-25
@@ -5,11 +5,9 @@
|
||||
"@apollo/server": "^4.7.3",
|
||||
"@aws-sdk/client-lambda": "^3.614.0",
|
||||
"@aws-sdk/client-s3": "^3.363.0",
|
||||
"@aws-sdk/client-sts": "^3.744.0",
|
||||
"@aws-sdk/credential-providers": "^3.363.0",
|
||||
"@blocknote/mantine": "^0.22.0",
|
||||
"@blocknote/react": "^0.22.0",
|
||||
"@blocknote/server-util": "0.17.1",
|
||||
"@blocknote/mantine": "^0.15.3",
|
||||
"@blocknote/react": "^0.15.3",
|
||||
"@codesandbox/sandpack-react": "^2.13.5",
|
||||
"@dagrejs/dagre": "^1.1.2",
|
||||
"@emotion/react": "^11.11.1",
|
||||
@@ -24,8 +22,6 @@
|
||||
"@jsdevtools/rehype-toc": "^3.0.2",
|
||||
"@linaria/core": "^6.2.0",
|
||||
"@linaria/react": "^6.2.1",
|
||||
"@lingui/core": "^5.1.2",
|
||||
"@lingui/react": "^5.1.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
"@nestjs/apollo": "^11.0.5",
|
||||
@@ -45,8 +41,8 @@
|
||||
"@octokit/graphql": "^7.0.2",
|
||||
"@ptc-org/nestjs-query-core": "^4.2.0",
|
||||
"@ptc-org/nestjs-query-typeorm": "4.2.1-alpha.2",
|
||||
"@react-email/components": "0.0.32",
|
||||
"@react-email/render": "0.0.17",
|
||||
"@react-email/components": "0.0.12",
|
||||
"@react-email/render": "0.0.10",
|
||||
"@sentry/node": "^8",
|
||||
"@sentry/profiling-node": "^8",
|
||||
"@sentry/react": "^8",
|
||||
@@ -54,6 +50,7 @@
|
||||
"@stoplight/elements": "^8.0.5",
|
||||
"@swc/jest": "^0.2.29",
|
||||
"@tabler/icons-react": "^2.44.0",
|
||||
"@tiptap/extension-hard-break": "^2.9.1",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/facepaint": "^1.2.5",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
@@ -63,7 +60,6 @@
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-microsoft": "^1.0.3",
|
||||
"@wyw-in-js/vite": "^0.5.3",
|
||||
"@xyflow/react": "^12.4.2",
|
||||
"add": "^2.0.6",
|
||||
"addressparser": "^1.0.1",
|
||||
"afterframe": "^1.0.2",
|
||||
@@ -74,11 +70,10 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"body-parser": "^1.20.2",
|
||||
"bullmq": "^5.40.0",
|
||||
"bullmq": "^4.14.0",
|
||||
"bytes": "^3.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cron-validate": "^1.4.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"danger-plugin-todos": "^1.3.1",
|
||||
@@ -93,7 +88,7 @@
|
||||
"esbuild-plugin-svgr": "^2.1.0",
|
||||
"facepaint": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"framer-motion": "^11.18.0",
|
||||
"framer-motion": "^10.12.17",
|
||||
"googleapis": "105",
|
||||
"graphiql": "^3.1.1",
|
||||
"graphql": "16.8.0",
|
||||
@@ -119,7 +114,6 @@
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"lodash.compact": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.identity": "^3.0.0",
|
||||
"lodash.isempty": "^4.4.0",
|
||||
@@ -167,13 +161,14 @@
|
||||
"react-hotkeys-hook": "^4.4.4",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-imask": "^7.6.0",
|
||||
"react-intersection-observer": "^9.15.1",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-phone-number-input": "^3.3.4",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-router-dom": "^6.4.4",
|
||||
"react-textarea-autosize": "^8.4.1",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"reactflow": "^11.11.3",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-behead": "^3.1.0",
|
||||
@@ -184,7 +179,7 @@
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.32.1",
|
||||
"slash": "^5.1.0",
|
||||
"stripe": "^17.3.1",
|
||||
"stripe": "^14.17.0",
|
||||
"ts-key-enum": "^2.0.12",
|
||||
"tslib": "^2.3.0",
|
||||
"tsup": "^8.2.4",
|
||||
@@ -207,9 +202,6 @@
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@lingui/cli": "^5.1.2",
|
||||
"@lingui/swc-plugin": "^5.1.0",
|
||||
"@lingui/vite-plugin": "^5.1.2",
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.0",
|
||||
@@ -243,7 +235,7 @@
|
||||
"@stylistic/eslint-plugin": "^1.5.0",
|
||||
"@swc-node/register": "1.8.0",
|
||||
"@swc/cli": "^0.3.12",
|
||||
"@swc/core": "1.7.42",
|
||||
"@swc/core": "~1.3.100",
|
||||
"@swc/helpers": "~0.5.2",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "14.0.0",
|
||||
@@ -255,15 +247,14 @@
|
||||
"@types/chrome": "^0.0.267",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/graphql-fields": "^1.3.6",
|
||||
"@types/graphql-upload": "^8.0.12",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.compact": "^3.0.9",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.escaperegexp": "^4.1.9",
|
||||
"@types/lodash.groupby": "^4.6.9",
|
||||
"@types/lodash.identity": "^3.0.9",
|
||||
"@types/lodash.isempty": "^4.4.7",
|
||||
@@ -305,7 +296,6 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-lingui": "^0.9.0",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"eslint-plugin-project-structure": "^3.9.1",
|
||||
@@ -357,8 +347,7 @@
|
||||
"resolutions": {
|
||||
"graphql": "16.8.0",
|
||||
"type-fest": "4.10.1",
|
||||
"typescript": "5.3.3",
|
||||
"prosemirror-model": "1.23.0"
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
@@ -376,7 +365,6 @@
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
"tools/eslint-rules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,5 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npx vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-shared": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "{projectRoot}/dist"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"start": {
|
||||
"executor": "nx:run-commands",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// Open options page programmatically in a new tab.
|
||||
// chrome.runtime.onInstalled.addListener((details) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
interface CustomDiv extends HTMLDivElement {
|
||||
onClickHandler: (newHandler: () => void) => void;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { createDefaultButton } from '~/contentScript/createButton';
|
||||
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
|
||||
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
|
||||
import extractDomain from '~/contentScript/utils/extractDomain';
|
||||
import { createCompany, fetchCompany } from '~/db/company.db';
|
||||
import { CompanyInput } from '~/db/types/company.types';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const checkIfCompanyExists = async () => {
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { createDefaultButton } from '~/contentScript/createButton';
|
||||
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
|
||||
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
|
||||
import { createPerson, fetchPerson } from '~/db/person.db';
|
||||
import { PersonInput } from '~/db/types/person.types';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const checkIfPersonExists = async () => {
|
||||
const { tab: activeTab } = await chrome.runtime.sendMessage({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { insertButtonForCompany } from '~/contentScript/extractCompanyProfile';
|
||||
import { insertButtonForPerson } from '~/contentScript/extractPersonProfile';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// Inject buttons into the DOM when SPA is reloaded on the resource url.
|
||||
// e.g. reload the page when on https://www.linkedin.com/in/mabdullahabaid/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const btn = document.getElementById('twenty-settings-btn');
|
||||
if (!isDefined(btn)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const changeSidePanelUrl = async (url: string) => {
|
||||
if (isDefined(url)) {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Extract "https://www.linkedin.com/company/twenty/" from any of the following urls, which the user can visit while on the company page.
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// "https://www.linkedin.com/company/twenty/" "https://www.linkedin.com/company/twenty/about/" "https://www.linkedin.com/company/twenty/people/".
|
||||
const extractCompanyLinkedinLink = (activeTabUrl: string) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import {
|
||||
ExchangeAuthCodeInput,
|
||||
ExchangeAuthCodeResponse,
|
||||
Tokens,
|
||||
} from '~/db/types/auth.types';
|
||||
import { EXCHANGE_AUTHORIZATION_CODE } from '~/graphql/auth/mutations';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { callMutation } from '~/utils/requestDb';
|
||||
|
||||
export const exchangeAuthorizationCode = async (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import {
|
||||
CompanyInput,
|
||||
CreateCompanyResponse,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
import { Company, CompanyFilterInput } from '~/generated/graphql';
|
||||
import { CREATE_COMPANY } from '~/graphql/company/mutations';
|
||||
import { FIND_COMPANY } from '~/graphql/company/queries';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { callMutation, callQuery } from '../utils/requestDb';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import {
|
||||
CreatePersonResponse,
|
||||
FindPersonResponse,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
import { Person, PersonFilterInput } from '~/generated/graphql';
|
||||
import { CREATE_PERSON } from '~/graphql/person/mutations';
|
||||
import { FIND_PERSON } from '~/graphql/person/queries';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { callMutation, callQuery } from '../utils/requestDb';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { Tokens } from '~/db/types/auth.types';
|
||||
import { RENEW_TOKEN } from '~/graphql/auth/mutations';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const renewToken = async (
|
||||
appToken: string,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import Settings from '~/options/Settings';
|
||||
import Sidepanel from '~/options/Sidepanel';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const App = () => {
|
||||
const [currentScreen, setCurrentScreen] = useState('');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MainButton } from '@/ui/input/button/MainButton';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { clearStore } from '~/utils/apolloClient';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MainButton } from '@/ui/input/button/MainButton';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledIframe = styled.iframe`
|
||||
display: block;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export type ToggleSize = 'small' | 'medium';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client';
|
||||
import { setContext } from '@apollo/client/link/context';
|
||||
import { onError } from '@apollo/client/link/error';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const clearStore = () => {
|
||||
chrome.storage.local.remove([
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { OperationVariables } from '@apollo/client';
|
||||
import { DocumentNode } from 'graphql';
|
||||
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import getApolloClient from '~/utils/apolloClient';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const callQuery = async <T>(
|
||||
query: DocumentNode,
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/options/modules/*"],
|
||||
"~/*": ["./src/*"]
|
||||
"@/*": ["packages/twenty-chrome-extension/src/options/modules/*"],
|
||||
"~/*": ["packages/twenty-chrome-extension/src/*"]
|
||||
},
|
||||
|
||||
/* Bundler mode */
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
TAG=latest
|
||||
|
||||
#PG_DATABASE_USER=postgres
|
||||
#PG_DATABASE_PASSWORD=replace_me_with_a_strong_password_without_special_characters
|
||||
#PG_DATABASE_HOST=db
|
||||
#PG_DATABASE_PORT=5432
|
||||
#REDIS_URL=redis://redis:6379
|
||||
#PGUSER_SUPERUSER=postgres
|
||||
#PGPASSWORD_SUPERUSER=replace_me_with_a_strong_password
|
||||
|
||||
PG_DATABASE_HOST=db:5432
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
SERVER_URL=http://localhost:3000
|
||||
SIGN_IN_PREFILLED=false
|
||||
|
||||
# Use openssl rand -base64 32 for each secret
|
||||
# APP_SECRET=replace_me_with_a_random_string
|
||||
|
||||
SIGN_IN_PREFILLED=true
|
||||
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# STORAGE_S3_REGION=eu-west3
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
version: "3.9"
|
||||
name: twenty
|
||||
|
||||
services:
|
||||
@@ -13,18 +14,22 @@ services:
|
||||
&& chown -R 1000:1000 /tmp/docker-data"
|
||||
|
||||
server:
|
||||
image: twentycrm/twenty:${TAG:-latest}
|
||||
image: twentycrm/twenty:${TAG}
|
||||
volumes:
|
||||
- server-local-data:/app/packages/twenty-server/${STORAGE_LOCAL_PATH:-.local-storage}
|
||||
- docker-data:/app/docker-data
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
NODE_PORT: 3000
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
PORT: 3000
|
||||
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://redis:6379}
|
||||
|
||||
ENABLE_DB_MIGRATIONS: "true"
|
||||
|
||||
SIGN_IN_PREFILLED: ${SIGN_IN_PREFILLED}
|
||||
STORAGE_TYPE: ${STORAGE_TYPE}
|
||||
STORAGE_S3_REGION: ${STORAGE_S3_REGION}
|
||||
STORAGE_S3_NAME: ${STORAGE_S3_NAME}
|
||||
@@ -44,13 +49,15 @@ services:
|
||||
restart: always
|
||||
|
||||
worker:
|
||||
image: twentycrm/twenty:${TAG:-latest}
|
||||
image: twentycrm/twenty:${TAG}
|
||||
command: ["yarn", "worker:prod"]
|
||||
environment:
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/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://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: "true" # it already runs on the server
|
||||
|
||||
ENABLE_DB_MIGRATIONS: "false" # it already runs on the server
|
||||
|
||||
STORAGE_TYPE: ${STORAGE_TYPE}
|
||||
STORAGE_S3_REGION: ${STORAGE_S3_REGION}
|
||||
@@ -66,16 +73,16 @@ services:
|
||||
restart: always
|
||||
|
||||
db:
|
||||
image: twentycrm/twenty-postgres-spilo:${TAG:-latest}
|
||||
image: twentycrm/twenty-postgres-spilo:${TAG}
|
||||
volumes:
|
||||
- db-data:/home/postgres/pgdata
|
||||
environment:
|
||||
PGUSER_SUPERUSER: ${PG_DATABASE_USER:-postgres}
|
||||
PGPASSWORD_SUPERUSER: ${PG_DATABASE_PASSWORD:-postgres}
|
||||
PGUSER_SUPERUSER: ${PGUSER_SUPERUSER:-postgres}
|
||||
PGPASSWORD_SUPERUSER: ${PGPASSWORD_SUPERUSER:-twenty}
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
healthcheck:
|
||||
test: pg_isready -U ${PG_DATABASE_USER:-postgres} -h localhost -d postgres
|
||||
test: pg_isready -U ${PGUSER_SUPERUSER:-postgres} -h localhost -d postgres
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
@@ -27,17 +27,13 @@ spec:
|
||||
claimName: twentycrm-db-pvc
|
||||
containers:
|
||||
- name: twentycrm
|
||||
image: twentycrm/twenty-postgres-spilo:latest
|
||||
image: twentycrm/twenty-postgres:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: PGUSER_SUPERUSER
|
||||
value: "postgres"
|
||||
- name: PGPASSWORD_SUPERUSER
|
||||
value: "postgres"
|
||||
- name: SPILO_PROVIDER
|
||||
value: "local"
|
||||
- name: ALLOW_NOSSL
|
||||
value: "true"
|
||||
value: "twenty"
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: tcp
|
||||
|
||||
@@ -33,18 +33,24 @@ spec:
|
||||
image: twentycrm/twenty:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: NODE_PORT
|
||||
- name: PORT
|
||||
value: 3000
|
||||
- name: SERVER_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: FRONT_BASE_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: "PG_DATABASE_URL"
|
||||
value: "postgres://postgres:postgres@twentycrm-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
|
||||
value: "true"
|
||||
- name: SIGN_IN_PREFILLED
|
||||
value: "false"
|
||||
value: "true"
|
||||
- name: STORAGE_TYPE
|
||||
value: "local"
|
||||
- name: "MESSAGE_QUEUE_TYPE"
|
||||
value: "bull-mq"
|
||||
- name: "ACCESS_TOKEN_EXPIRES_IN"
|
||||
value: "7d"
|
||||
- name: "LOGIN_TOKEN_EXPIRES_IN"
|
||||
|
||||
@@ -28,12 +28,18 @@ spec:
|
||||
env:
|
||||
- name: SERVER_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: FRONT_BASE_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: PG_DATABASE_URL
|
||||
value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default"
|
||||
- name: DISABLE_DB_MIGRATIONS
|
||||
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
|
||||
value: "local"
|
||||
- name: "MESSAGE_QUEUE_TYPE"
|
||||
value: "bull-mq"
|
||||
- name: "CACHE_STORAGE_TYPE"
|
||||
value: "redis"
|
||||
- name: "REDIS_URL"
|
||||
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
|
||||
- name: APP_SECRET
|
||||
|
||||
@@ -38,15 +38,24 @@ resource "kubernetes_deployment" "twentycrm_server" {
|
||||
tty = true
|
||||
|
||||
env {
|
||||
name = "NODE_PORT"
|
||||
name = "PORT"
|
||||
value = "3000"
|
||||
}
|
||||
# env {
|
||||
# name = "DEBUG_MODE"
|
||||
# value = false
|
||||
# }
|
||||
|
||||
env {
|
||||
name = "SERVER_URL"
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "FRONT_BASE_URL"
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "PG_DATABASE_URL"
|
||||
value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default"
|
||||
@@ -56,14 +65,23 @@ resource "kubernetes_deployment" "twentycrm_server" {
|
||||
value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379"
|
||||
}
|
||||
env {
|
||||
name = "DISABLE_DB_MIGRATIONS"
|
||||
value = "false"
|
||||
name = "ENABLE_DB_MIGRATIONS"
|
||||
value = "true"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "SIGN_IN_PREFILLED"
|
||||
value = "true"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "STORAGE_TYPE"
|
||||
value = "local"
|
||||
}
|
||||
env {
|
||||
name = "MESSAGE_QUEUE_TYPE"
|
||||
value = "bull-mq"
|
||||
}
|
||||
env {
|
||||
name = "ACCESS_TOKEN_EXPIRES_IN"
|
||||
value = "7d"
|
||||
|
||||
@@ -43,25 +43,39 @@ resource "kubernetes_deployment" "twentycrm_worker" {
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "FRONT_BASE_URL"
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "PG_DATABASE_URL"
|
||||
value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CACHE_STORAGE_TYPE"
|
||||
value = "redis"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "REDIS_URL"
|
||||
value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "DISABLE_DB_MIGRATIONS"
|
||||
value = "true" #it already runs on the server
|
||||
name = "ENABLE_DB_MIGRATIONS"
|
||||
value = "false" #it already runs on the server
|
||||
}
|
||||
|
||||
env {
|
||||
name = "STORAGE_TYPE"
|
||||
value = "local"
|
||||
}
|
||||
env {
|
||||
name = "MESSAGE_QUEUE_TYPE"
|
||||
value = "bull-mq"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "APP_SECRET"
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
pull_version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/tags | grep '"name":' | head -n 1 | cut -d '"' -f 4)}
|
||||
|
||||
if [[ -z "$pull_version" ]]; then
|
||||
echo "Error: Unable to fetch the latest version tag. Please check your network connection or the GitHub API response."
|
||||
exit 1
|
||||
fi
|
||||
pull_version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)}
|
||||
pull_branch=${BRANCH:-$pull_version}
|
||||
|
||||
version_num=${pull_version#v}
|
||||
@@ -11,12 +6,7 @@ target_version="0.32.4"
|
||||
|
||||
# We moved the install script to a different location in v0.32.4
|
||||
if [[ -n "$BRANCH" ]] || [[ "$(printf '%s\n' "$target_version" "$version_num" | sort -V | head -n1)" != "$version_num" ]]; then
|
||||
curl -sL "https://raw.githubusercontent.com/twentyhq/twenty/$pull_branch/packages/twenty-docker/scripts/install.sh" -o twenty_install.sh
|
||||
curl -sL "https://raw.githubusercontent.com/twentyhq/twenty/$pull_branch/packages/twenty-docker/scripts/install.sh" | bash -s -- "$VERSION" "$BRANCH"
|
||||
else
|
||||
curl -sL "https://raw.githubusercontent.com/twentyhq/twenty/$pull_branch/install.sh" -o twenty_install.sh
|
||||
curl -sL "https://raw.githubusercontent.com/twentyhq/twenty/$pull_branch/install.sh" | bash -s -- "$VERSION" "$BRANCH"
|
||||
fi
|
||||
|
||||
chmod +x twenty_install.sh
|
||||
VERSION="$VERSION" BRANCH="$BRANCH" ./twenty_install.sh
|
||||
|
||||
rm twenty_install.sh
|
||||
|
||||
@@ -44,10 +44,10 @@ function on_exit {
|
||||
trap on_exit EXIT
|
||||
|
||||
# Use environment variables VERSION and BRANCH, with defaults if not set
|
||||
version=${VERSION:-$(curl -s "https://hub.docker.com/v2/repositories/twentycrm/twenty/tags" | grep -o '"name":"[^"]*"' | grep -v 'latest' | cut -d'"' -f4 | sort -V | tail -n1)}
|
||||
branch=${BRANCH:-$(curl -s https://api.github.com/repos/twentyhq/twenty/tags | grep '"name":' | head -n 1 | cut -d '"' -f 4)}
|
||||
version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)}
|
||||
branch=${BRANCH:-$version}
|
||||
|
||||
echo "🚀 Using docker version $version and Github branch $branch"
|
||||
echo "🚀 Using version $version and branch $branch"
|
||||
|
||||
dir_name="twenty"
|
||||
function ask_directory {
|
||||
@@ -90,11 +90,10 @@ else
|
||||
fi
|
||||
|
||||
# Generate random strings for secrets
|
||||
echo "# === Randomly generated secret ===" >> .env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >> .env
|
||||
|
||||
echo "" >> .env
|
||||
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> .env
|
||||
echo "# === Randomly generated secrets ===" >>.env
|
||||
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
|
||||
echo "" >>.env
|
||||
echo "PGPASSWORD_SUPERUSER=$(openssl rand -hex 16)" >>.env
|
||||
|
||||
echo -e "\t• .env configuration completed"
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
|
||||
COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
|
||||
COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
|
||||
COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
|
||||
COPY ./packages/twenty-shared/package.json /app/packages/twenty-shared/
|
||||
COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
|
||||
|
||||
# Install all dependencies
|
||||
@@ -23,7 +22,6 @@ FROM common-deps as twenty-server-build
|
||||
|
||||
# Copy sourcecode after installing dependences to accelerate subsequents builds
|
||||
COPY ./packages/twenty-emails /app/packages/twenty-emails
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
|
||||
RUN npx nx run twenty-server:build
|
||||
@@ -33,7 +31,7 @@ RUN mv /app/packages/twenty-server/dist/package.json /app/packages/twenty-server
|
||||
RUN rm -rf /app/packages/twenty-server/dist
|
||||
RUN mv /app/packages/twenty-server/build /app/packages/twenty-server/dist
|
||||
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-shared twenty-server
|
||||
RUN yarn workspaces focus --production twenty-emails twenty-server
|
||||
|
||||
|
||||
# Build the front
|
||||
@@ -43,7 +41,6 @@ ARG REACT_APP_SERVER_BASE_URL
|
||||
|
||||
COPY ./packages/twenty-front /app/packages/twenty-front
|
||||
COPY ./packages/twenty-ui /app/packages/twenty-ui
|
||||
COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
RUN npx nx build twenty-front
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -e
|
||||
|
||||
# Check if the initialization has already been done and that we enabled automatic migration
|
||||
if [ "${DISABLE_DB_MIGRATIONS}" != "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
||||
if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
||||
echo "Running database setup and migrations..."
|
||||
|
||||
# Creating the database if it doesn't exist
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# Note that provide always without trailing forward slash to have expected behaviour
|
||||
FRONTEND_BASE_URL=http://localhost:3001
|
||||
BACKEND_BASE_URL=http://localhost:3000
|
||||
CI_DEFAULT_BASE_URL=https://demo.twenty.com
|
||||
DEFAULT_LOGIN=tim@apple.dev
|
||||
NEW_WORKSPACE_LOGIN=test@apple.dev
|
||||
DEMO_DEFAULT_LOGIN=noah@demo.dev
|
||||
DEFAULT_PASSWORD=Applecar2025
|
||||
WEBSITE_URL=https://twenty.com
|
||||
|
||||
|
||||
@@ -1,43 +1,39 @@
|
||||
# Twenty end-to-end (E2E) Testing
|
||||
# Twenty e2e Testing
|
||||
|
||||
## Prerequisite
|
||||
|
||||
Installing the browsers:
|
||||
|
||||
```
|
||||
npx nx setup twenty-e2e-testing
|
||||
yarn playwright install
|
||||
```
|
||||
|
||||
### Run end-to-end tests
|
||||
|
||||
```
|
||||
npx nx test twenty-e2e-testing
|
||||
yarn run test:e2e
|
||||
```
|
||||
|
||||
### Start the interactive UI mode
|
||||
|
||||
```
|
||||
npx nx test:ui twenty-e2e-testing
|
||||
yarn run test:e2e:ui
|
||||
```
|
||||
|
||||
### Run test only on Desktop Chrome
|
||||
|
||||
```
|
||||
yarn run test:e2e:chrome
|
||||
```
|
||||
|
||||
### Run test in specific file
|
||||
```
|
||||
npx nx test twenty-e2e-testing <filename>
|
||||
```
|
||||
|
||||
Example (location of the test must be specified from the root of `twenty-e2e-testing` package):
|
||||
```
|
||||
npx nx test twenty-e2e-testing tests/login.spec.ts
|
||||
yarn run test:e2e <filename>
|
||||
```
|
||||
|
||||
### Runs the tests in debug mode.
|
||||
```
|
||||
npx nx test:debug twenty-e2e-testing
|
||||
```
|
||||
|
||||
### Show report after tests
|
||||
```
|
||||
npx nx test:report twenty-e2e-testing
|
||||
yarn run test:e2e:debug
|
||||
```
|
||||
|
||||
## Q&A
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Reporter,
|
||||
FullConfig,
|
||||
Suite,
|
||||
TestCase,
|
||||
TestResult,
|
||||
FullResult,
|
||||
} from '@playwright/test/reporter';
|
||||
|
||||
class CustomReporter implements Reporter {
|
||||
constructor(options: { customOption?: string } = {}) {
|
||||
console.log(
|
||||
`my-awesome-reporter setup with customOption set to ${options.customOption}`,
|
||||
);
|
||||
}
|
||||
|
||||
onBegin(config: FullConfig, suite: Suite) {
|
||||
console.log(`Starting the run with ${suite.allTests().length} tests`);
|
||||
}
|
||||
|
||||
onTestBegin(test: TestCase) {
|
||||
console.log(`Starting test ${test.title}`);
|
||||
}
|
||||
|
||||
onTestEnd(test: TestCase, result: TestResult) {
|
||||
console.log(`Finished test ${test.title}: ${result.status}`);
|
||||
}
|
||||
|
||||
onEnd(result: FullResult) {
|
||||
console.log(`Finished the run: ${result.status}`);
|
||||
}
|
||||
}
|
||||
export default CustomReporter;
|
||||
@@ -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,13 @@
|
||||
import { exec } from 'child_process';
|
||||
|
||||
export async function sh(cmd) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
import { test as base, expect, Locator, Page } from '@playwright/test';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createWorkflow } from '../requests/create-workflow';
|
||||
import { deleteWorkflow } from '../requests/delete-workflow';
|
||||
import { destroyWorkflow } from '../requests/destroy-workflow';
|
||||
import { WorkflowActionType, WorkflowTriggerType } from '../types/workflows';
|
||||
|
||||
export class WorkflowVisualizerPage {
|
||||
#page: Page;
|
||||
|
||||
workflowId: string;
|
||||
workflowName: string;
|
||||
|
||||
readonly addStepButton: Locator;
|
||||
readonly workflowStatus: Locator;
|
||||
readonly activateWorkflowButton: Locator;
|
||||
readonly deactivateWorkflowButton: Locator;
|
||||
readonly addTriggerButton: Locator;
|
||||
readonly commandMenu: Locator;
|
||||
readonly workflowNameButton: Locator;
|
||||
readonly triggerNode: Locator;
|
||||
readonly background: Locator;
|
||||
readonly useAsDraftButton: Locator;
|
||||
readonly overrideDraftButton: Locator;
|
||||
readonly discardDraftButton: Locator;
|
||||
|
||||
#actionNames: Record<WorkflowActionType, string> = {
|
||||
'create-record': 'Create Record',
|
||||
'update-record': 'Update Record',
|
||||
'delete-record': 'Delete Record',
|
||||
code: 'Code',
|
||||
'send-email': 'Send Email',
|
||||
};
|
||||
|
||||
#createdActionNames: Record<WorkflowActionType, string> = {
|
||||
'create-record': 'Create Record',
|
||||
'update-record': 'Update Record',
|
||||
'delete-record': 'Delete Record',
|
||||
code: 'Code - Serverless Function',
|
||||
'send-email': 'Send Email',
|
||||
};
|
||||
|
||||
#triggerNames: Record<WorkflowTriggerType, string> = {
|
||||
'record-created': 'Record is Created',
|
||||
'record-updated': 'Record is Updated',
|
||||
'record-deleted': 'Record is Deleted',
|
||||
manual: 'Launch manually',
|
||||
};
|
||||
|
||||
#createdTriggerNames: Record<WorkflowTriggerType, string> = {
|
||||
'record-created': 'Record is Created',
|
||||
'record-updated': 'Record is Updated',
|
||||
'record-deleted': 'Record is Deleted',
|
||||
manual: 'Manual Trigger',
|
||||
};
|
||||
|
||||
constructor({ page, workflowName }: { page: Page; workflowName: string }) {
|
||||
this.#page = page;
|
||||
this.workflowName = workflowName;
|
||||
|
||||
this.addStepButton = page.getByLabel('Add a step');
|
||||
this.workflowStatus = page.getByTestId('workflow-visualizer-status');
|
||||
this.activateWorkflowButton = page.getByLabel('Activate Workflow', {
|
||||
exact: true,
|
||||
});
|
||||
this.deactivateWorkflowButton = page.getByLabel('Deactivate Workflow', {
|
||||
exact: true,
|
||||
});
|
||||
this.addTriggerButton = page.getByText('Add a Trigger');
|
||||
this.commandMenu = page.getByTestId('command-menu');
|
||||
this.workflowNameButton = page.getByRole('button', {
|
||||
name: this.workflowName,
|
||||
});
|
||||
this.triggerNode = this.#page.getByTestId('rf__node-trigger');
|
||||
this.background = page.locator('.react-flow__pane');
|
||||
this.useAsDraftButton = page.getByRole('button', { name: 'Use as draft' });
|
||||
this.overrideDraftButton = page.getByRole('button', {
|
||||
name: 'Override Draft',
|
||||
});
|
||||
this.discardDraftButton = page.getByRole('button', {
|
||||
name: 'Discard Draft',
|
||||
});
|
||||
}
|
||||
|
||||
async createOneWorkflow() {
|
||||
const id = randomUUID();
|
||||
|
||||
const response = await createWorkflow({
|
||||
page: this.#page,
|
||||
workflowId: id,
|
||||
workflowName: this.workflowName,
|
||||
});
|
||||
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const responseBody = await response.json();
|
||||
expect(responseBody.data.createWorkflow.id).toBe(id);
|
||||
|
||||
this.workflowId = id;
|
||||
}
|
||||
|
||||
async waitForWorkflowVisualizerLoad() {
|
||||
await expect(this.workflowNameButton).toBeVisible();
|
||||
}
|
||||
|
||||
async goToWorkflowVisualizerPage() {
|
||||
await Promise.all([
|
||||
this.#page.goto(`/object/workflow/${this.workflowId}`),
|
||||
|
||||
this.waitForWorkflowVisualizerLoad(),
|
||||
]);
|
||||
}
|
||||
|
||||
async createInitialTrigger(trigger: WorkflowTriggerType) {
|
||||
await this.addTriggerButton.click();
|
||||
|
||||
const triggerName = this.#triggerNames[trigger];
|
||||
const createdTriggerName = this.#createdTriggerNames[trigger];
|
||||
|
||||
const triggerOption = this.#page.getByText(triggerName);
|
||||
await triggerOption.click();
|
||||
|
||||
await expect(this.triggerNode).toHaveClass(/selected/);
|
||||
await expect(this.triggerNode).toContainText(createdTriggerName);
|
||||
}
|
||||
|
||||
async createStep(action: WorkflowActionType) {
|
||||
await this.addStepButton.click();
|
||||
|
||||
const actionName = this.#actionNames[action];
|
||||
const createdActionName = this.#createdActionNames[action];
|
||||
|
||||
const actionToCreateOption = this.commandMenu.getByText(actionName);
|
||||
|
||||
const [createWorkflowStepResponse] = await Promise.all([
|
||||
this.#page.waitForResponse((response) => {
|
||||
if (!response.url().endsWith('/graphql')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const requestBody = response.request().postDataJSON();
|
||||
|
||||
return requestBody.operationName === 'CreateWorkflowVersionStep';
|
||||
}),
|
||||
|
||||
actionToCreateOption.click(),
|
||||
]);
|
||||
const createWorkflowStepResponseBody =
|
||||
await createWorkflowStepResponse.json();
|
||||
const createdStepId =
|
||||
createWorkflowStepResponseBody.data.createWorkflowVersionStep.id;
|
||||
|
||||
await expect(
|
||||
this.#page.getByTestId('command-menu').getByRole('textbox').first(),
|
||||
).toHaveValue(createdActionName);
|
||||
|
||||
const createdActionNode = this.#page
|
||||
.locator('.react-flow__node.selected')
|
||||
.getByText(createdActionName);
|
||||
|
||||
await expect(createdActionNode).toBeVisible();
|
||||
|
||||
const selectedNodes = this.#page.locator('.react-flow__node.selected');
|
||||
|
||||
await expect(selectedNodes).toHaveCount(1);
|
||||
|
||||
return {
|
||||
createdStepId,
|
||||
};
|
||||
}
|
||||
|
||||
getStepNode(stepId: string) {
|
||||
return this.#page.getByTestId(`rf__node-${stepId}`);
|
||||
}
|
||||
|
||||
getDeleteNodeButton(nodeLocator: Locator) {
|
||||
return nodeLocator.getByRole('button');
|
||||
}
|
||||
|
||||
getAllStepNodes() {
|
||||
return this.#page
|
||||
.getByTestId(/^rf__node-.+$/)
|
||||
.and(this.#page.getByTestId(/^((?!rf__node-trigger).)*$/))
|
||||
.and(
|
||||
this.#page.getByTestId(/^((?!rf__node-branch-\d+__create-step).)*$/),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteStep(stepId: string) {
|
||||
const stepNode = this.getStepNode(stepId);
|
||||
|
||||
await stepNode.click();
|
||||
|
||||
await Promise.all([
|
||||
expect(stepNode).not.toBeVisible(),
|
||||
this.#page.waitForResponse((response) => {
|
||||
if (!response.url().endsWith('/graphql')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const requestBody = response.request().postDataJSON();
|
||||
|
||||
return (
|
||||
requestBody.operationName === 'DeleteWorkflowVersionStep' &&
|
||||
requestBody.variables.input.stepId === stepId
|
||||
);
|
||||
}),
|
||||
|
||||
this.getDeleteNodeButton(stepNode).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async deleteTrigger() {
|
||||
await this.triggerNode.click();
|
||||
|
||||
await Promise.all([
|
||||
expect(this.triggerNode).toContainText('Add a Trigger'),
|
||||
this.#page.waitForResponse((response) => {
|
||||
if (!response.url().endsWith('/graphql')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const requestBody = response.request().postDataJSON();
|
||||
|
||||
return (
|
||||
requestBody.operationName === 'UpdateOneWorkflowVersion' &&
|
||||
requestBody.variables.input.trigger === null
|
||||
);
|
||||
}),
|
||||
|
||||
this.getDeleteNodeButton(this.triggerNode).click(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export const test = base.extend<{ workflowVisualizer: WorkflowVisualizerPage }>(
|
||||
{
|
||||
workflowVisualizer: async ({ page }, use) => {
|
||||
const workflowVisualizer = new WorkflowVisualizerPage({
|
||||
page,
|
||||
workflowName: 'Test Workflow',
|
||||
});
|
||||
|
||||
await workflowVisualizer.createOneWorkflow();
|
||||
await workflowVisualizer.goToWorkflowVisualizerPage();
|
||||
|
||||
await use(workflowVisualizer);
|
||||
|
||||
await deleteWorkflow({
|
||||
page,
|
||||
workflowId: workflowVisualizer.workflowId,
|
||||
});
|
||||
await destroyWorkflow({
|
||||
page,
|
||||
workflowId: workflowVisualizer.workflowId,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, Locator, Page } from '@playwright/test';
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class LoginPage {
|
||||
private readonly loginWithGoogleButton: Locator;
|
||||
@@ -10,7 +10,7 @@ export class LoginPage {
|
||||
private readonly forgotPasswordButton: Locator;
|
||||
private readonly passwordField: Locator;
|
||||
private readonly revealPasswordButton: Locator;
|
||||
readonly signInButton: Locator;
|
||||
private readonly signInButton: Locator;
|
||||
private readonly signUpButton: Locator;
|
||||
private readonly previewImageButton: Locator;
|
||||
private readonly uploadImageButton: Locator;
|
||||
@@ -35,7 +35,7 @@ export class LoginPage {
|
||||
name: 'Continue with Google',
|
||||
});
|
||||
this.loginWithEmailButton = page.getByRole('button', {
|
||||
name: 'Continue with Email',
|
||||
name: 'Continue With Email',
|
||||
});
|
||||
this.termsOfServiceLink = page.getByRole('link', {
|
||||
name: 'Terms of Service',
|
||||
@@ -98,8 +98,6 @@ export class LoginPage {
|
||||
}
|
||||
|
||||
async typeEmail(email: string) {
|
||||
await expect(this.emailField).toBeVisible();
|
||||
|
||||
await this.emailField.fill(email);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export const backendGraphQLUrl = new URL(
|
||||
'/graphql',
|
||||
process.env.BACKEND_BASE_URL,
|
||||
).toString();
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { getAuthToken } from '../utils/getAuthToken';
|
||||
import { backendGraphQLUrl } from './backend';
|
||||
|
||||
export const createWorkflow = async ({
|
||||
page,
|
||||
workflowId,
|
||||
workflowName,
|
||||
}: {
|
||||
page: Page;
|
||||
workflowId: string;
|
||||
workflowName: string;
|
||||
}) => {
|
||||
const { authToken } = await getAuthToken(page);
|
||||
|
||||
return page.request.post(backendGraphQLUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
data: {
|
||||
operationName: 'CreateOneWorkflow',
|
||||
query:
|
||||
'mutation CreateOneWorkflow($input: WorkflowCreateInput!) { createWorkflow(data: $input) { __typename id } }',
|
||||
variables: {
|
||||
input: {
|
||||
id: workflowId,
|
||||
name: workflowName,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { getAuthToken } from '../utils/getAuthToken';
|
||||
import { backendGraphQLUrl } from './backend';
|
||||
|
||||
export const deleteWorkflow = async ({
|
||||
page,
|
||||
workflowId,
|
||||
}: {
|
||||
page: Page;
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const { authToken } = await getAuthToken(page);
|
||||
|
||||
return page.request.post(backendGraphQLUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
data: {
|
||||
operationName: 'DeleteOneWorkflow',
|
||||
variables: { idToDelete: workflowId },
|
||||
query:
|
||||
'mutation DeleteOneWorkflow($idToDelete: ID!) {\n deleteWorkflow(id: $idToDelete) {\n __typename\n deletedAt\n id\n }\n}',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { getAuthToken } from '../utils/getAuthToken';
|
||||
import { backendGraphQLUrl } from './backend';
|
||||
|
||||
export const destroyWorkflow = async ({
|
||||
page,
|
||||
workflowId,
|
||||
}: {
|
||||
page: Page;
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const { authToken } = await getAuthToken(page);
|
||||
|
||||
return page.request.post(backendGraphQLUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
data: {
|
||||
operationName: 'DestroyOneWorkflow',
|
||||
variables: { idToDestroy: workflowId },
|
||||
query:
|
||||
'mutation DestroyOneWorkflow($idToDestroy: ID!) {\n destroyWorkflow(id: $idToDestroy) {\n id\n __typename\n }\n}',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
export type WorkflowTriggerType =
|
||||
| 'record-created'
|
||||
| 'record-updated'
|
||||
| 'record-deleted'
|
||||
| 'manual';
|
||||
|
||||
export type WorkflowActionType =
|
||||
| 'create-record'
|
||||
| 'update-record'
|
||||
| 'delete-record'
|
||||
| 'code'
|
||||
| 'send-email';
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
export const getAuthToken = async (page: Page) => {
|
||||
const storageState = await page.context().storageState();
|
||||
const authCookie = storageState.cookies.find(
|
||||
(cookie) => cookie.name === 'tokenPair',
|
||||
);
|
||||
if (!authCookie) {
|
||||
throw new Error('No auth cookie found');
|
||||
}
|
||||
const token = JSON.parse(decodeURIComponent(authCookie.value)).accessToken
|
||||
.token;
|
||||
|
||||
return { authToken: token };
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"name": "twenty-e2e-testing",
|
||||
"version": "0.42.4",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.49.0"
|
||||
"@playwright/test": "^1.46.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test:e2e:setup": "yarn playwright install",
|
||||
"test:e2e": "yarn playwright test",
|
||||
"test:e2e:ui": "yarn playwright test --ui",
|
||||
"test:e2e:chrome": "yarn playwright test --project=chromium",
|
||||
"test:e2e:debug": "yarn playwright test --debug",
|
||||
"test:e2e:report": "yarn playwright show-report"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
import { config } from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
const envResult = config({
|
||||
path: path.resolve(__dirname, '.env'),
|
||||
});
|
||||
|
||||
if (envResult.error) {
|
||||
throw new Error('Failed to load .env file');
|
||||
}
|
||||
config();
|
||||
|
||||
/* === Run your local dev server before starting the tests === */
|
||||
|
||||
@@ -16,38 +10,51 @@ if (envResult.error) {
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
testDir: '.',
|
||||
outputDir: 'run_results/', // directory for screenshots and videos
|
||||
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}', // just in case, do not delete it
|
||||
fullyParallel: false, // parallelization of tests will be done later in the future
|
||||
fullyParallel: true, // false only for specific tests, overwritten in specific projects or global setups of projects
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1, // 1 worker = 1 test at the time, tests can't be parallelized
|
||||
workers: process.env.CI ? 1 : undefined, // undefined = amount of projects * amount of tests
|
||||
timeout: 30 * 1000, // timeout can be changed
|
||||
use: {
|
||||
baseURL: process.env.FRONTEND_BASE_URL || 'http://localhost:3001',
|
||||
baseURL: process.env.CI
|
||||
? process.env.CI_DEFAULT_BASE_URL
|
||||
: (process.env.FRONTEND_BASE_URL ?? 'http://localhost:3001'),
|
||||
trace: 'retain-on-failure', // trace takes EVERYTHING from page source, records every single step, should be used only when normal debugging won't work
|
||||
screenshot: 'on', // either 'on' here or in different method in modules, if 'on' all screenshots are overwritten each time the test is run
|
||||
headless: true, // instead of changing it to false, run 'yarn test:e2e:debug' or 'yarn test:e2e:ui'
|
||||
testIdAttribute: 'data-testid', // taken from Twenty source
|
||||
viewport: { width: 1920, height: 1080 }, // most laptops use this resolution
|
||||
launchOptions: {
|
||||
slowMo: 500, // time in milliseconds between each step, better to use it than explicitly define timeout in tests
|
||||
},
|
||||
},
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
projects: [
|
||||
{
|
||||
name: 'setup',
|
||||
testMatch: /.*\.setup\.ts/,
|
||||
name: 'Login setup',
|
||||
testMatch: /login\.setup\.ts/, // finds all tests matching this regex, in this case only 1 test should be found
|
||||
},
|
||||
{
|
||||
name: 'chrome',
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
storageState: path.resolve(__dirname, '.auth', 'user.json'), // takes saved cookies from directory
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
dependencies: ['Login setup'], // forces to run login setup before running tests from this project - CASE SENSITIVE
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
storageState: path.resolve(__dirname, '.auth', 'user.json'),
|
||||
},
|
||||
dependencies: ['Login setup'],
|
||||
},
|
||||
|
||||
//{
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"name": "twenty-e2e-testing",
|
||||
"projectType": "application",
|
||||
"tags": ["scope:testing"],
|
||||
"targets": {
|
||||
"setup": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "packages/twenty-e2e-testing",
|
||||
"commands": [
|
||||
"yarn playwright install",
|
||||
"cp .env.example .env"
|
||||
]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "packages/twenty-e2e-testing",
|
||||
"commands": [
|
||||
"yarn playwright test"
|
||||
]
|
||||
}
|
||||
},
|
||||
"test:ui": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "packages/twenty-e2e-testing",
|
||||
"commands": [
|
||||
"yarn playwright test --ui"
|
||||
]
|
||||
}
|
||||
},
|
||||
"test:debug": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "packages/twenty-e2e-testing",
|
||||
"commands": [
|
||||
"yarn playwright test --debug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"test:report": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "packages/twenty-e2e-testing",
|
||||
"commands": [
|
||||
"yarn playwright show-report"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { test as base } from '../../lib/fixtures/screenshot';
|
||||
import { ConfirmationModal } from '../../lib/pom/helper/confirmationModal';
|
||||
import { LeftMenu } from '../../lib/pom/leftMenu';
|
||||
import { LoginPage } from '../../lib/pom/loginPage';
|
||||
import { MembersSection } from '../../lib/pom/settings/membersSection';
|
||||
import { ProfileSection } from '../../lib/pom/settings/profileSection';
|
||||
import { SettingsPage } from '../../lib/pom/settingsPage';
|
||||
|
||||
type Fixtures = {
|
||||
confirmationModal: ConfirmationModal;
|
||||
loginPage: LoginPage;
|
||||
leftMenu: LeftMenu;
|
||||
settingsPage: SettingsPage;
|
||||
membersSection: MembersSection;
|
||||
profileSection: ProfileSection;
|
||||
};
|
||||
|
||||
export const test = base.extend<Fixtures>({
|
||||
confirmationModal: async ({ page }, use) => {
|
||||
const confirmationModal = new ConfirmationModal(page);
|
||||
await use(confirmationModal);
|
||||
},
|
||||
loginPage: async ({ page }, use) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await use(loginPage);
|
||||
},
|
||||
leftMenu: async ({ page }, use) => {
|
||||
const leftMenu = new LeftMenu(page);
|
||||
await use(leftMenu);
|
||||
},
|
||||
settingsPage: async ({ page }, use) => {
|
||||
const settingsPage = new SettingsPage(page);
|
||||
await use(settingsPage);
|
||||
},
|
||||
membersSection: async ({ page }, use) => {
|
||||
const membersSection = new MembersSection(page);
|
||||
await use(membersSection);
|
||||
},
|
||||
profileSection: async ({ page }, use) => {
|
||||
const profileSection = new ProfileSection(page);
|
||||
await use(profileSection);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
@@ -1,60 +0,0 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { expect, test } from './fixture';
|
||||
|
||||
test('Sign up with invite link via email', async ({
|
||||
page,
|
||||
loginPage,
|
||||
leftMenu,
|
||||
membersSection,
|
||||
settingsPage,
|
||||
profileSection,
|
||||
confirmationModal,
|
||||
}) => {
|
||||
const email = `test${randomUUID().replaceAll('-', '')}@apple.dev`;
|
||||
const firstName = 'John';
|
||||
const lastName = 'Doe';
|
||||
|
||||
const inviteLink: string =
|
||||
await test.step('Go to Settings and copy invite link', async () => {
|
||||
await page.goto(process.env.LINK); // skip login page (and redirect) when running on environments with multi-workspace enabled
|
||||
await leftMenu.goToSettings();
|
||||
await settingsPage.goToMembersSection();
|
||||
await membersSection.copyInviteLink();
|
||||
return await page.evaluate('navigator.clipboard.readText()');
|
||||
});
|
||||
|
||||
await test.step('Go to invite link', async () => {
|
||||
await settingsPage.logout();
|
||||
|
||||
await Promise.all([
|
||||
expect(page.getByText(/Join .+ team/)).toBeVisible(),
|
||||
|
||||
page.goto(inviteLink),
|
||||
]);
|
||||
});
|
||||
|
||||
await test.step('Create new account', async () => {
|
||||
await loginPage.clickLoginWithEmail();
|
||||
await loginPage.typeEmail(email);
|
||||
await loginPage.clickContinueButton();
|
||||
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
|
||||
await loginPage.clickSignUpButton();
|
||||
await loginPage.typeFirstName(firstName);
|
||||
await loginPage.typeLastName(lastName);
|
||||
await loginPage.clickContinueButton();
|
||||
await loginPage.noSyncWithGoogle();
|
||||
});
|
||||
|
||||
await test.step('Delete account from workspace', async () => {
|
||||
await leftMenu.goToSettings();
|
||||
await settingsPage.goToProfileSection();
|
||||
await profileSection.deleteAccount();
|
||||
await confirmationModal.typePlaceholderToInput();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForURL('/welcome'),
|
||||
|
||||
confirmationModal.clickConfirmButton(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from '../lib/fixtures/screenshot';
|
||||
|
||||
test.describe('Basic check', () => {
|
||||
test('Checking if table in Companies is visible', async ({ page }) => {
|
||||
await expect(page.getByTestId('tooltip').nth(0)).toHaveText('Companies');
|
||||
await expect(page.getByTestId('tooltip').nth(0)).toBeVisible();
|
||||
expect(page.url()).toContain('/companies');
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
await expect(page.locator('tbody > tr')).toHaveCount(13); // shouldn't be hardcoded in case of tests on demo
|
||||
});
|
||||
|
||||
test('', async ({ page }) => {
|
||||
await page.getByRole('link', { name: 'Opportunities' }).click();
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.fixme(
|
||||
'Check if demo account is working properly @demo-only',
|
||||
async ({ page }) => {
|
||||
await page.goto('https://app.twenty-next.com/');
|
||||
await page.getByRole('button', { name: 'Continue with Email' }).click();
|
||||
await page.getByRole('button', { name: 'Continue', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
await expect(page.getByText('Welcome to Twenty')).not.toBeVisible();
|
||||
await page.waitForTimeout(5000);
|
||||
await expect(page.getByText('Server’s on a coffee break')).not.toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,42 +1,18 @@
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { test as setup, expect } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import { LoginPage } from '../lib/pom/loginPage';
|
||||
|
||||
// fixture
|
||||
const test = base.extend<{ loginPage: LoginPage }>({
|
||||
loginPage: async ({ page }, use) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await use(loginPage);
|
||||
},
|
||||
});
|
||||
setup('Login test', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Continue With Email' }).click();
|
||||
await page.getByPlaceholder('Email').fill(process.env.DEFAULT_LOGIN);
|
||||
await page.getByRole('button', { name: 'Continue', exact: true }).click();
|
||||
await page.getByPlaceholder('Password').fill(process.env.DEFAULT_PASSWORD);
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
await expect(page.getByText('Welcome to Twenty')).not.toBeVisible();
|
||||
|
||||
test('Login test', async ({ loginPage, page }) => {
|
||||
await test.step('Navigated to login page', async () => {
|
||||
await page.goto('/');
|
||||
});
|
||||
await test.step(
|
||||
'Logging in '.concat(page.url(), ' as ', process.env.DEFAULT_LOGIN),
|
||||
async () => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
if (
|
||||
page.url().includes('app.twenty-next.com') ||
|
||||
!page.url().includes('app.localhost:3001')
|
||||
) {
|
||||
await loginPage.clickLoginWithEmail();
|
||||
}
|
||||
await loginPage.typeEmail(process.env.DEFAULT_LOGIN);
|
||||
await loginPage.clickContinueButton();
|
||||
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await loginPage.clickSignInButton();
|
||||
await expect(page.getByText(/Welcome to .+/)).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
// End of authentication steps.
|
||||
|
||||
await test.step('Saved auth state', async () => {
|
||||
await page.context().storageState({
|
||||
path: path.resolve(__dirname, '..', '.auth', 'user.json'),
|
||||
});
|
||||
process.env.LINK = page.url();
|
||||
await page.context().storageState({
|
||||
path: path.resolve(__dirname, '..', '.auth', 'user.json'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { deleteWorkflow } from '../lib/requests/delete-workflow';
|
||||
import { destroyWorkflow } from '../lib/requests/destroy-workflow';
|
||||
|
||||
test('Create workflow', async ({ page }) => {
|
||||
const NEW_WORKFLOW_NAME = 'Test Workflow';
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
|
||||
await workflowsLink.click();
|
||||
|
||||
const createWorkflowButton = page.getByRole('button', { name: 'New record' });
|
||||
|
||||
const [createWorkflowResponse] = await Promise.all([
|
||||
page.waitForResponse(async (response) => {
|
||||
if (!response.url().endsWith('/graphql')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const requestBody = response.request().postDataJSON();
|
||||
|
||||
return requestBody.operationName === 'CreateOneWorkflow';
|
||||
}),
|
||||
|
||||
await createWorkflowButton.click(),
|
||||
]);
|
||||
|
||||
const nameInput = page.getByRole('textbox');
|
||||
await nameInput.fill(NEW_WORKFLOW_NAME);
|
||||
|
||||
const workflowDiagramContainer = page.locator('.react-flow__renderer');
|
||||
await workflowDiagramContainer.click();
|
||||
|
||||
const body = await createWorkflowResponse.json();
|
||||
const newWorkflowId = body.data.createWorkflow.id;
|
||||
|
||||
try {
|
||||
const workflowName = page.getByRole('button', { name: NEW_WORKFLOW_NAME });
|
||||
|
||||
await expect(workflowName).toBeVisible();
|
||||
|
||||
await expect(page).toHaveURL(`/object/workflow/${newWorkflowId}`);
|
||||
} finally {
|
||||
await deleteWorkflow({
|
||||
page,
|
||||
workflowId: newWorkflowId,
|
||||
});
|
||||
await destroyWorkflow({
|
||||
page,
|
||||
workflowId: newWorkflowId,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,175 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '../lib/fixtures/blank-workflow';
|
||||
|
||||
test('Use an old version as draft', async ({ workflowVisualizer, page }) => {
|
||||
await workflowVisualizer.createInitialTrigger('record-created');
|
||||
|
||||
await workflowVisualizer.createStep('create-record');
|
||||
|
||||
await workflowVisualizer.background.click();
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
|
||||
|
||||
workflowVisualizer.activateWorkflowButton.click(),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.workflowStatus).toHaveText('Draft'),
|
||||
|
||||
workflowVisualizer.createStep('delete-record'),
|
||||
]);
|
||||
|
||||
await workflowVisualizer.background.click();
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
|
||||
|
||||
workflowVisualizer.activateWorkflowButton.click(),
|
||||
]);
|
||||
|
||||
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||
'Record is Created',
|
||||
);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Create Record',
|
||||
'Delete Record',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
|
||||
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
|
||||
|
||||
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
|
||||
await workflowsLink.click();
|
||||
|
||||
const recordTableRowForWorkflow = page.getByRole('row', {
|
||||
name: workflowVisualizer.workflowName,
|
||||
});
|
||||
|
||||
const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
|
||||
name: workflowVisualizer.workflowName,
|
||||
});
|
||||
expect(linkToWorkflow).toBeVisible();
|
||||
|
||||
const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
|
||||
'link',
|
||||
{
|
||||
name: 'v1',
|
||||
},
|
||||
);
|
||||
|
||||
await linkToFirstWorkflowVersion.click();
|
||||
|
||||
await expect(workflowVisualizer.workflowStatus).toHaveText('Archived');
|
||||
await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
|
||||
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||
'Record is Created',
|
||||
);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Create Record',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`),
|
||||
|
||||
workflowVisualizer.useAsDraftButton.click(),
|
||||
]);
|
||||
|
||||
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
|
||||
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
|
||||
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||
'Record is Created',
|
||||
);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Create Record',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test.fixme(
|
||||
'Use an old version as draft while having a pending draft version',
|
||||
async ({ workflowVisualizer, page }) => {
|
||||
await workflowVisualizer.createInitialTrigger('record-created');
|
||||
|
||||
await workflowVisualizer.createStep('create-record');
|
||||
|
||||
await workflowVisualizer.background.click();
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
|
||||
|
||||
workflowVisualizer.activateWorkflowButton.click(),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.workflowStatus).toHaveText('Draft'),
|
||||
|
||||
workflowVisualizer.createStep('delete-record'),
|
||||
]);
|
||||
|
||||
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||
'Record is Created',
|
||||
);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Create Record',
|
||||
'Delete Record',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
|
||||
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
|
||||
|
||||
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
|
||||
await workflowsLink.click();
|
||||
|
||||
const recordTableRowForWorkflow = page.getByRole('row', {
|
||||
name: workflowVisualizer.workflowName,
|
||||
});
|
||||
|
||||
const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
|
||||
name: workflowVisualizer.workflowName,
|
||||
});
|
||||
expect(linkToWorkflow).toBeVisible();
|
||||
|
||||
const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
|
||||
'link',
|
||||
{
|
||||
name: 'v1',
|
||||
},
|
||||
);
|
||||
|
||||
await linkToFirstWorkflowVersion.click();
|
||||
|
||||
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
|
||||
await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
|
||||
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||
'Record is Created',
|
||||
);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Create Record',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.overrideDraftButton).toBeVisible(),
|
||||
|
||||
workflowVisualizer.useAsDraftButton.click(),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`),
|
||||
|
||||
workflowVisualizer.overrideDraftButton.click(),
|
||||
]);
|
||||
|
||||
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
|
||||
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
|
||||
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||
'Record is Created',
|
||||
);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Create Record',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||
await expect(workflowVisualizer.activateWorkflowButton).toBeVisible();
|
||||
await expect(workflowVisualizer.discardDraftButton).toBeVisible();
|
||||
},
|
||||
);
|
||||
@@ -1,218 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '../lib/fixtures/blank-workflow';
|
||||
|
||||
test('Create workflow with every possible step', async ({
|
||||
workflowVisualizer,
|
||||
page,
|
||||
}) => {
|
||||
await workflowVisualizer.createInitialTrigger('record-created');
|
||||
|
||||
await workflowVisualizer.createStep('create-record');
|
||||
await workflowVisualizer.createStep('update-record');
|
||||
await workflowVisualizer.createStep('delete-record');
|
||||
await workflowVisualizer.createStep('code');
|
||||
await workflowVisualizer.createStep('send-email');
|
||||
|
||||
await workflowVisualizer.background.click();
|
||||
|
||||
const draftWorkflowStatus =
|
||||
workflowVisualizer.workflowStatus.getByText('Draft');
|
||||
|
||||
await expect(draftWorkflowStatus).toBeVisible();
|
||||
|
||||
await workflowVisualizer.activateWorkflowButton.click();
|
||||
|
||||
const activeWorkflowStatus =
|
||||
workflowVisualizer.workflowStatus.getByText('Active');
|
||||
|
||||
await expect(draftWorkflowStatus).not.toBeVisible();
|
||||
await expect(activeWorkflowStatus).toBeVisible();
|
||||
await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
|
||||
await expect(workflowVisualizer.deactivateWorkflowButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('Delete steps from draft version', async ({
|
||||
workflowVisualizer,
|
||||
page,
|
||||
}) => {
|
||||
await workflowVisualizer.createInitialTrigger('record-created');
|
||||
|
||||
const { createdStepId: firstStepId } =
|
||||
await workflowVisualizer.createStep('create-record');
|
||||
const { createdStepId: secondStepId } =
|
||||
await workflowVisualizer.createStep('update-record');
|
||||
const { createdStepId: thirdStepId } =
|
||||
await workflowVisualizer.createStep('delete-record');
|
||||
const { createdStepId: fourthStepId } =
|
||||
await workflowVisualizer.createStep('code');
|
||||
const { createdStepId: fifthStepId } =
|
||||
await workflowVisualizer.createStep('send-email');
|
||||
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Create Record',
|
||||
'Update Record',
|
||||
'Delete Record',
|
||||
'Code - Serverless Function',
|
||||
'Send Email',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(5);
|
||||
|
||||
await workflowVisualizer.deleteStep(firstStepId);
|
||||
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Update Record',
|
||||
'Delete Record',
|
||||
'Code - Serverless Function',
|
||||
'Send Email',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(4);
|
||||
|
||||
await workflowVisualizer.deleteStep(fifthStepId);
|
||||
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Update Record',
|
||||
'Delete Record',
|
||||
'Code - Serverless Function',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(3);
|
||||
|
||||
await workflowVisualizer.deleteStep(secondStepId);
|
||||
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Delete Record',
|
||||
'Code - Serverless Function',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
|
||||
|
||||
await workflowVisualizer.deleteStep(fourthStepId);
|
||||
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Delete Record',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||
|
||||
await workflowVisualizer.deleteStep(thirdStepId);
|
||||
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
|
||||
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
|
||||
expect(workflowVisualizer.triggerNode).toBeVisible(),
|
||||
]);
|
||||
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('Add a step to an active version', async ({
|
||||
workflowVisualizer,
|
||||
page,
|
||||
}) => {
|
||||
await workflowVisualizer.createInitialTrigger('record-created');
|
||||
|
||||
await workflowVisualizer.createStep('create-record');
|
||||
|
||||
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
|
||||
|
||||
await workflowVisualizer.background.click();
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
|
||||
|
||||
workflowVisualizer.activateWorkflowButton.click(),
|
||||
]);
|
||||
|
||||
await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
|
||||
|
||||
const assertEndState = async () => {
|
||||
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
|
||||
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||
'Record is Created',
|
||||
);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Create Record',
|
||||
]);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||
};
|
||||
|
||||
await assertEndState();
|
||||
|
||||
await page.reload();
|
||||
|
||||
await assertEndState();
|
||||
});
|
||||
|
||||
test('Replace the trigger of an active version', async ({
|
||||
workflowVisualizer,
|
||||
page,
|
||||
}) => {
|
||||
await workflowVisualizer.createInitialTrigger('record-created');
|
||||
|
||||
await workflowVisualizer.createStep('create-record');
|
||||
|
||||
await workflowVisualizer.background.click();
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
|
||||
|
||||
workflowVisualizer.activateWorkflowButton.click(),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.workflowStatus).toHaveText('Draft'),
|
||||
|
||||
workflowVisualizer.deleteTrigger(),
|
||||
]);
|
||||
|
||||
await workflowVisualizer.createInitialTrigger('record-deleted');
|
||||
|
||||
await workflowVisualizer.background.click();
|
||||
|
||||
await Promise.all([
|
||||
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
|
||||
|
||||
workflowVisualizer.activateWorkflowButton.click(),
|
||||
]);
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(workflowVisualizer.triggerNode).toContainText(
|
||||
'Record is Deleted',
|
||||
);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
|
||||
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
|
||||
'Create Record',
|
||||
]);
|
||||
});
|
||||
|
||||
test("Nodes can't be deleted by pressing Backspace or Delete keys", async ({
|
||||
workflowVisualizer,
|
||||
page,
|
||||
}) => {
|
||||
await workflowVisualizer.triggerNode.click();
|
||||
|
||||
await page.keyboard.press('Backspace');
|
||||
await page.keyboard.press('Delete');
|
||||
|
||||
await expect(workflowVisualizer.triggerNode).toBeVisible();
|
||||
|
||||
const { createdStepId: firstStepId } =
|
||||
await workflowVisualizer.createStep('create-record');
|
||||
const firstStep = workflowVisualizer.getStepNode(firstStepId);
|
||||
|
||||
await firstStep.click();
|
||||
|
||||
await expect(workflowVisualizer.getDeleteNodeButton(firstStep)).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Backspace');
|
||||
await page.keyboard.press('Delete');
|
||||
|
||||
await expect(firstStep).toBeVisible();
|
||||
|
||||
await workflowVisualizer.addStepButton.click();
|
||||
|
||||
await page.keyboard.press('Backspace');
|
||||
await page.keyboard.press('Delete');
|
||||
|
||||
await expect(workflowVisualizer.addStepButton).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { sh } from '../drivers/shell_driver';
|
||||
|
||||
test.describe('', () => {
|
||||
test('Testing logging', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Continue With Email' }).click();
|
||||
await page.getByPlaceholder('Email').fill('tim@apple.dev');
|
||||
await page.getByRole('button', { name: 'Continue', exact: true }).click();
|
||||
await page.getByPlaceholder('Password').fill('Applecar2025');
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
await expect(page.getByText('Welcome to Twenty')).not.toBeVisible();
|
||||
expect(page.url()).not.toContain('/welcome');
|
||||
await page.getByRole('link', { name: 'Opportunities' }).click();
|
||||
await expect(page.locator('tbody > tr')).toHaveCount(4);
|
||||
});
|
||||
|
||||
test('Creating new workspace', async ({ page, browserName }) => {
|
||||
// this test must use only 1 browser, otherwise it will lead to success and fail (1 workspace is created instead of x workspaces)
|
||||
if (browserName == 'chromium') {
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Continue With Email' }).click();
|
||||
await page.getByPlaceholder('Email').fill('test@apple.dev'); // email must be changed each time test is run
|
||||
await page.getByPlaceholder('Email').press('Enter'); // otherwise if tests fails after this step, new workspace is created
|
||||
await page.getByPlaceholder('Password').fill('Applecar2025');
|
||||
await page.getByPlaceholder('Password').press('Enter');
|
||||
await page.getByPlaceholder('Apple').fill('Test');
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByPlaceholder('Tim').click();
|
||||
await page.getByPlaceholder('Tim').fill('Test2');
|
||||
await page.getByPlaceholder('Cook').click();
|
||||
await page.getByPlaceholder('Cook').fill('Test2');
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByText('Continue without sync').click();
|
||||
await page.getByRole('button', { name: 'Finish' }).click();
|
||||
await expect(page.locator('table')).toBeVisible({ timeout: 1000 });
|
||||
}
|
||||
});
|
||||
|
||||
test('Syncing all workspaces', async () => {
|
||||
await sh('npx nx run twenty-server:command workspace:sync-metadata -f');
|
||||
await sh('npx nx run twenty-server:command workspace:sync-metadata -f');
|
||||
});
|
||||
|
||||
test('Resetting database', async ({ page, browserName }) => {
|
||||
if (browserName === 'chromium') {
|
||||
await sh('yarn nx database:reset twenty-server'); // if this command fails for any reason, database must be restarted manually using the same command because database is in unstable state
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Continue With Email' }).click();
|
||||
await page.getByPlaceholder('Email').fill('tim@apple.dev');
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByPlaceholder('Password').fill('Applecar2025');
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
await page.getByRole('link', { name: 'Companies' }).click();
|
||||
expect(page.url()).toContain('/companies');
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('Seeding database', async ({ page, browserName }) => {
|
||||
if (browserName === 'chromium') {
|
||||
await sh('npx nx workspace:seed:demo');
|
||||
await page.goto('/');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"experimental": {
|
||||
"plugins": [
|
||||
[
|
||||
"@lingui/swc-plugin",
|
||||
{
|
||||
"runtimeModules": {
|
||||
"i18n": ["@lingui/core", "i18n"],
|
||||
"trans": ["@lingui/react", "Trans"]
|
||||
},
|
||||
"stripNonEssentialFields": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { defineConfig } from '@lingui/cli';
|
||||
import { APP_LOCALES } from 'twenty-shared';
|
||||
|
||||
export default defineConfig({
|
||||
sourceLocale: 'en',
|
||||
locales: Object.values(APP_LOCALES),
|
||||
pseudoLocale: 'pseudo-en',
|
||||
fallbackLocales: {
|
||||
'pseudo-en': 'en',
|
||||
},
|
||||
extractorParserOptions: {
|
||||
tsExperimentalDecorators: true,
|
||||
},
|
||||
catalogs: [
|
||||
{
|
||||
path: '<rootDir>/src/locales/{locale}',
|
||||
include: ['src'],
|
||||
},
|
||||
],
|
||||
catalogsMergePath: '<rootDir>/src/locales/generated/{locale}',
|
||||
compileNamespace: 'ts',
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-emails",
|
||||
"version": "0.42.4",
|
||||
"version": "0.33.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -9,14 +9,10 @@
|
||||
"scripts": {
|
||||
"build": "npx vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-shared": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "{projectRoot}/dist"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
},
|
||||
"typecheck": {},
|
||||
"lint": {
|
||||
@@ -31,20 +30,6 @@
|
||||
"configurations": {
|
||||
"fix": {}
|
||||
}
|
||||
},
|
||||
"lingui:extract": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "lingui extract --overwrite"
|
||||
}
|
||||
},
|
||||
"lingui:compile": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "lingui compile --typescript"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,6 @@ const grayScale = {
|
||||
gray0: '#ffffff',
|
||||
};
|
||||
|
||||
const colors = {
|
||||
blue40: '#5e90f2',
|
||||
};
|
||||
|
||||
export const emailTheme = {
|
||||
font: {
|
||||
colors: {
|
||||
@@ -33,7 +29,6 @@ export const emailTheme = {
|
||||
primary: grayScale.gray50,
|
||||
tertiary: grayScale.gray35,
|
||||
inverted: grayScale.gray0,
|
||||
blue: colors.blue40,
|
||||
},
|
||||
family: 'Trebuchet MS', // Google Inter not working, we need to use a web safe font, see https://templates.mailchimp.com/design/typography/
|
||||
weight: {
|
||||
|
||||
@@ -1,102 +1,21 @@
|
||||
import { i18n, Messages } from '@lingui/core';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { Container, Html } from '@react-email/components';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Container, Html } from '@react-email/components';
|
||||
|
||||
import { BaseHead } from 'src/components/BaseHead';
|
||||
import { Logo } from 'src/components/Logo';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared';
|
||||
import { messages as afMessages } from '../locales/generated/af-ZA';
|
||||
import { messages as arMessages } from '../locales/generated/ar-SA';
|
||||
import { messages as caMessages } from '../locales/generated/ca-ES';
|
||||
import { messages as csMessages } from '../locales/generated/cs-CZ';
|
||||
import { messages as daMessages } from '../locales/generated/da-DK';
|
||||
import { messages as deMessages } from '../locales/generated/de-DE';
|
||||
import { messages as elMessages } from '../locales/generated/el-GR';
|
||||
import { messages as enMessages } from '../locales/generated/en';
|
||||
import { messages as esMessages } from '../locales/generated/es-ES';
|
||||
import { messages as fiMessages } from '../locales/generated/fi-FI';
|
||||
import { messages as frMessages } from '../locales/generated/fr-FR';
|
||||
import { messages as heMessages } from '../locales/generated/he-IL';
|
||||
import { messages as huMessages } from '../locales/generated/hu-HU';
|
||||
import { messages as itMessages } from '../locales/generated/it-IT';
|
||||
import { messages as jaMessages } from '../locales/generated/ja-JP';
|
||||
import { messages as koMessages } from '../locales/generated/ko-KR';
|
||||
import { messages as nlMessages } from '../locales/generated/nl-NL';
|
||||
import { messages as noMessages } from '../locales/generated/no-NO';
|
||||
import { messages as plMessages } from '../locales/generated/pl-PL';
|
||||
import { messages as pseudoEnMessages } from '../locales/generated/pseudo-en';
|
||||
import { messages as ptBRMessages } from '../locales/generated/pt-BR';
|
||||
import { messages as ptPTMessages } from '../locales/generated/pt-PT';
|
||||
import { messages as roMessages } from '../locales/generated/ro-RO';
|
||||
import { messages as ruMessages } from '../locales/generated/ru-RU';
|
||||
import { messages as srMessages } from '../locales/generated/sr-Cyrl';
|
||||
import { messages as svMessages } from '../locales/generated/sv-SE';
|
||||
import { messages as trMessages } from '../locales/generated/tr-TR';
|
||||
import { messages as ukMessages } from '../locales/generated/uk-UA';
|
||||
import { messages as viMessages } from '../locales/generated/vi-VN';
|
||||
import { messages as zhHansMessages } from '../locales/generated/zh-CN';
|
||||
import { messages as zhHantMessages } from '../locales/generated/zh-TW';
|
||||
|
||||
type BaseEmailProps = PropsWithChildren<{
|
||||
width?: number;
|
||||
locale: keyof typeof APP_LOCALES;
|
||||
}>;
|
||||
|
||||
const messages: Record<keyof typeof APP_LOCALES, Messages> = {
|
||||
en: enMessages,
|
||||
'pseudo-en': pseudoEnMessages,
|
||||
'af-ZA': afMessages,
|
||||
'ar-SA': arMessages,
|
||||
'ca-ES': caMessages,
|
||||
'cs-CZ': csMessages,
|
||||
'da-DK': daMessages,
|
||||
'de-DE': deMessages,
|
||||
'el-GR': elMessages,
|
||||
'es-ES': esMessages,
|
||||
'fi-FI': fiMessages,
|
||||
'fr-FR': frMessages,
|
||||
'he-IL': heMessages,
|
||||
'hu-HU': huMessages,
|
||||
'it-IT': itMessages,
|
||||
'ja-JP': jaMessages,
|
||||
'ko-KR': koMessages,
|
||||
'nl-NL': nlMessages,
|
||||
'no-NO': noMessages,
|
||||
'pl-PL': plMessages,
|
||||
'pt-BR': ptBRMessages,
|
||||
'pt-PT': ptPTMessages,
|
||||
'ro-RO': roMessages,
|
||||
'ru-RU': ruMessages,
|
||||
'sr-Cyrl': srMessages,
|
||||
'sv-SE': svMessages,
|
||||
'tr-TR': trMessages,
|
||||
'uk-UA': ukMessages,
|
||||
'vi-VN': viMessages,
|
||||
'zh-CN': zhHansMessages,
|
||||
'zh-TW': zhHantMessages,
|
||||
};
|
||||
|
||||
(Object.entries(messages) as [keyof typeof APP_LOCALES, any][]).forEach(
|
||||
([locale, message]) => {
|
||||
i18n.load(locale, message);
|
||||
},
|
||||
);
|
||||
|
||||
i18n.activate(SOURCE_LOCALE);
|
||||
|
||||
export const BaseEmail = ({ children, width, locale }: BaseEmailProps) => {
|
||||
i18n.activate(locale);
|
||||
|
||||
export const BaseEmail = ({ children, width }: BaseEmailProps) => {
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<Html lang={locale}>
|
||||
<BaseHead />
|
||||
<Container width={width || 290}>
|
||||
<Logo />
|
||||
{children}
|
||||
</Container>
|
||||
</Html>
|
||||
</I18nProvider>
|
||||
<Html lang="en">
|
||||
<BaseHead />
|
||||
<Container width={width || 290}>
|
||||
<Logo />
|
||||
{children}
|
||||
</Container>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Column, Row } from '@react-email/components';
|
||||
import { Link } from 'src/components/Link';
|
||||
import { ShadowText } from 'src/components/ShadowText';
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Column>
|
||||
<ShadowText>
|
||||
<Link
|
||||
href="https://twenty.com/"
|
||||
value="Website"
|
||||
aria-label="Visit Twenty's website"
|
||||
/>
|
||||
</ShadowText>
|
||||
</Column>
|
||||
<Column>
|
||||
<ShadowText>
|
||||
<Link
|
||||
href="https://github.com/twentyhq/twenty"
|
||||
value="Github"
|
||||
aria-label="Visit Twenty's GitHub repository"
|
||||
/>
|
||||
</ShadowText>
|
||||
</Column>
|
||||
<Column>
|
||||
<ShadowText>
|
||||
<Link
|
||||
href="https://twenty.com/user-guide"
|
||||
value="User guide"
|
||||
aria-label="Read Twenty's user guide"
|
||||
/>
|
||||
</ShadowText>
|
||||
</Column>
|
||||
<Column>
|
||||
<ShadowText>
|
||||
<Link
|
||||
href="https://docs.twenty.com/"
|
||||
value="Developers"
|
||||
aria-label="Visit Twenty's developer documentation"
|
||||
/>
|
||||
</ShadowText>
|
||||
</Column>
|
||||
</Row>
|
||||
<ShadowText>
|
||||
Twenty.com Public Benefit Corporation
|
||||
<br />
|
||||
2261 Market Street #5275
|
||||
<br />
|
||||
San Francisco, CA 94114
|
||||
</ShadowText>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +1,25 @@
|
||||
import { Column, Container, Row } from '@react-email/components';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Container } from '@react-email/components';
|
||||
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
type HighlightedContainerProps = PropsWithChildren;
|
||||
|
||||
const highlightedContainerStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: emailTheme.background.colors.highlight,
|
||||
border: `1px solid ${emailTheme.border.color.highlighted}`,
|
||||
borderRadius: emailTheme.border.radius.md,
|
||||
padding: '24px 48px',
|
||||
gap: '24px',
|
||||
};
|
||||
|
||||
const divStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
} as React.CSSProperties;
|
||||
|
||||
export const HighlightedContainer = ({
|
||||
@@ -17,11 +27,7 @@ export const HighlightedContainer = ({
|
||||
}: HighlightedContainerProps) => {
|
||||
return (
|
||||
<Container style={highlightedContainerStyle}>
|
||||
{React.Children.map(children, (child) => (
|
||||
<Row>
|
||||
<Column align="center">{child}</Column>
|
||||
</Row>
|
||||
))}
|
||||
<div style={divStyle}>{children}</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Text } from '@react-email/text';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
@@ -12,11 +12,19 @@ const highlightedStyle = {
|
||||
color: emailTheme.font.colors.highlighted,
|
||||
};
|
||||
|
||||
const divStyle = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
type HighlightedTextProps = {
|
||||
value: ReactNode;
|
||||
centered?: boolean;
|
||||
};
|
||||
|
||||
export const HighlightedText = ({ value }: HighlightedTextProps) => {
|
||||
return <Text style={highlightedStyle}>{value}</Text>;
|
||||
return (
|
||||
<div style={divStyle}>
|
||||
<Text style={highlightedStyle}>{value}</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import { Link as EmailLink } from '@react-email/components';
|
||||
import { ReactNode } from 'react';
|
||||
import { Link as EmailLink } from '@react-email/components';
|
||||
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
const linkStyle = {
|
||||
color: emailTheme.font.colors.tertiary,
|
||||
textDecoration: 'underline',
|
||||
};
|
||||
|
||||
type LinkProps = {
|
||||
value: ReactNode;
|
||||
href: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const Link = ({ value, href, color }: LinkProps) => {
|
||||
export const Link = ({ value, href }: LinkProps) => {
|
||||
return (
|
||||
<EmailLink
|
||||
href={href}
|
||||
style={{
|
||||
...linkStyle,
|
||||
color: color ?? emailTheme.font.colors.tertiary,
|
||||
}}
|
||||
>
|
||||
<EmailLink href={href} style={linkStyle}>
|
||||
{value}
|
||||
</EmailLink>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user