Compare commits

..
5 Commits
Author SHA1 Message Date
EtienneandThomas Trompette bed46f12df add more logs to cleaning command (#12219) 2025-05-22 15:31:16 +02:00
Thomas Trompette 26709bcc43 Improve logs in workflow trigger (#12215)
- distinguish logs coming from webhook and job triggers
- add workspace and workflow ids to help debugging

Hard to debug sentry issue:

https://twenty-v7.sentry.io/issues/6605607134/?project=4507072499810304&query=&referrer=issue-stream&stream_index=7
2025-05-22 15:30:39 +02:00
Charles BochetandCharles Bochet 35817d5fae Fix upgrade command (#12210) 2025-05-22 12:23:36 +02:00
EtienneandCharles Bochet 0a75cbd06c clean not found file - add logs (#12198) 2025-05-22 11:40:20 +02:00
Charles BochetandCharles Bochet 3e731da69c Fix docker setup (#12209)
For fresh install, we need the migrations to happen before the upgrade
command is triggered as the upgrade command is a NestJS command and the
app will try to load env variables from db
2025-05-22 11:32:26 +02:00
2090 changed files with 75920 additions and 117020 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ npx nx run twenty-server:test # Run unit tests
npx nx run twenty-server:test:integration:with-db-reset # Run integration tests
# Migrations
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/[name] -d src/database/typeorm/core/core.datasource.ts
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/metadata/migrations/[name] -d src/database/typeorm/metadata/metadata.datasource.ts
# Workspace
npx nx run twenty-server:command workspace:sync-metadata -f # Sync metadata
-33
View File
@@ -1,33 +0,0 @@
---
description: Guidelines and best practices for working with Nx in the Twenty workspace, including workspace architecture understanding, configuration management, and generator usage.
globs:
alwaysApply: false
---
// This file is automatically generated by Nx Console
You are in an nx workspace using Nx 18.3.3 and yarn as the package manager.
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
# General Guidelines
- When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture
- For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration
- If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors
- To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool
# Generation Guidelines
If the user wants to generate something, use the following flow:
- learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable
- get the available generators using the 'nx_generators' tool
- decide which generator to use. If no generators seem relevant, check the 'nx_available_plugins' tool to see if the user could install a plugin to help them
- get generator details using the 'nx_generator_schema' tool
- you may use the 'nx_docs' tool to learn more about a specific generator or technology if you're unsure
- decide which options to provide in order to best complete the user's request. Don't make any assumptions and keep the options minimalistic
- open the generator UI using the 'nx_open_generate_ui' tool
- wait for the user to finish the generator
- read the generator log file using the 'nx_read_generator_log' tool
- use the information provided in the log file to answer the user's question or continue with what they were doing
@@ -84,7 +84,7 @@ Twenty uses a combination of Recoil for global state and Apollo Client for serve
`;
export const GET_USER = gql`
query GetUser($id: UUID!) {
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
}
@@ -10,8 +10,7 @@ inputs:
configuration:
required: false
default: 'ci'
args:
required: false
runs:
using: "composite"
steps:
@@ -19,4 +18,4 @@ runs:
uses: nrwl/nx-set-shas@v4
- name: Run affected command
shell: bash
run: npx nx affected --nxBail --configuration=${{ inputs.configuration }} -t=${{ inputs.tasks }} --parallel=${{ inputs.parallel }} --exclude='*,!tag:${{ inputs.tag }}' ${{ inputs.args }}
run: npx nx affected --nxBail --configuration=${{ inputs.configuration }} -t=${{ inputs.tasks }} --parallel=${{ inputs.parallel }} --exclude='*,!tag:${{ inputs.tag }}'
@@ -2,7 +2,7 @@ name: Yarn Install
inputs:
node-version:
required: false
default: '22'
default: '18'
runs:
using: "composite"
-777
View File
@@ -1,777 +0,0 @@
name: GraphQL and OpenAPI Breaking Changes Detection
on:
pull_request:
types: [opened, synchronize, edited]
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
MAIN_SERVER_PORT: 3000
CURRENT_SERVER_PORT: 3002
permissions:
contents: read
pull-requests: write
checks: write
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-server/**
packages/twenty-emails/**
packages/twenty-shared/**
api-breaking-changes:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 45
runs-on: depot-ubuntu-24.04-8
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
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
clickhouse:
image: clickhouse/clickhouse-server:latest
env:
CLICKHOUSE_PASSWORD: clickhousePassword
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
ports:
- 8123:8123
- 9000:9000
options: >-
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout current branch
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Build shared dependencies
run: |
npx nx build twenty-shared
npx nx build twenty-emails
- name: Build current branch server
run: npx nx build twenty-server
- name: Setup databases
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "current_branch";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "main_branch";'
- name: Run ClickHouse migrations
run: npx nx clickhouse:migrate twenty-server
env:
CLICKHOUSE_URL: http://default:clickhousePassword@localhost:8123/twenty
CLICKHOUSE_PASSWORD: clickhousePassword
- name: Setup current branch database
run: |
npx nx reset:env twenty-server
# Function to set or update environment variable
set_env_var() {
local var_name="$1"
local var_value="$2"
local env_file="packages/twenty-server/.env"
if grep -q "^${var_name}=" "$env_file"; then
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
else
echo "${var_name}=${var_value}" >> "$env_file"
fi
}
set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/current_branch"
set_env_var "NODE_PORT" "${{ env.CURRENT_SERVER_PORT }}"
set_env_var "REDIS_URL" "redis://localhost:6379"
set_env_var "CLICKHOUSE_URL" "http://default:clickhousePassword@localhost:8123/twenty"
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Seed current branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
- name: Start current branch server in background
run: |
echo "=== Current branch .env file contents ==="
cat packages/twenty-server/.env
echo "=== Starting current branch server ==="
nohup npx nx run twenty-server:start:prod > /tmp/current-server.log 2>&1 &
echo $! > /tmp/current-server.pid
echo "Current server PID: $(cat /tmp/current-server.pid)"
- name: Wait for current branch server to be ready
run: |
echo "Waiting for current branch server to start..."
timeout=300
interval=5
elapsed=0
while [ $elapsed -lt $timeout ]; do
if curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
echo "Current branch server is ready!"
break
fi
echo "Current branch server not ready yet, waiting ${interval}s..."
sleep $interval
elapsed=$((elapsed + interval))
done
if [ $elapsed -ge $timeout ]; then
echo "Timeout waiting for current branch server to start"
echo "Current server log:"
cat /tmp/current-server.log || echo "No current server log found"
exit 1
fi
- name: Download GraphQL and REST responses from current branch
run: |
# Admin token from jest-integration.config.ts
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwiaWF0IjoxNzM5NTQ3NjYxLCJleHAiOjMzMjk3MTQ3NjYxfQ.fbOM9yhr3jWDicPZ1n771usUURiPGmNdeFApsgrbxOw"
# Load introspection query from file
INTROSPECTION_QUERY=$(cat packages/twenty-utils/graphql-introspection-query.graphql)
# Prepare the query payload
QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g')
echo "Downloading GraphQL schema from current server..."
curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o current-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
echo "Downloading GraphQL metadata schema from current server..."
curl -X POST "http://localhost:${{ env.CURRENT_SERVER_PORT }}/metadata" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o current-metadata-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
# Download current branch OpenAPI specs
echo "Downloading OpenAPI specifications from current server..."
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o current-rest-api.json \
-w "HTTP Status: %{http_code}\n"
curl -s "http://localhost:${{ env.CURRENT_SERVER_PORT }}/rest/open-api/metadata" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o current-rest-metadata-api.json \
-w "HTTP Status: %{http_code}\n"
# Verify the downloads
echo "Current branch files downloaded:"
ls -la current-*
- name: Preserve current branch files
run: |
# Create a temp directory to store current branch files
mkdir -p /tmp/current-branch-files
# Move current branch files to temp directory
mv current-* /tmp/current-branch-files/ 2>/dev/null || echo "No current-* files to preserve"
echo "Preserved current branch files for later restoration"
- name: Stop current branch server
run: |
if [ -f /tmp/current-server.pid ]; then
echo "Stopping current branch server..."
kill $(cat /tmp/current-server.pid) || true
# Wait a bit for graceful shutdown
sleep 5
# Force kill if still running
kill -9 $(cat /tmp/current-server.pid) 2>/dev/null || true
rm -f /tmp/current-server.pid
fi
- name: Checkout main branch
run: |
git stash
git checkout origin/main
git clean -fd
- name: Install dependencies for main branch
uses: ./.github/workflows/actions/yarn-install
- name: Build main branch dependencies
run: |
npx nx build twenty-shared
npx nx build twenty-emails
- name: Build main branch server
run: npx nx build twenty-server
- name: Setup main branch database
run: |
# Function to set or update environment variable
set_env_var() {
local var_name="$1"
local var_value="$2"
local env_file="packages/twenty-server/.env"
if grep -q "^${var_name}=" "$env_file"; then
sed -i "s|^${var_name}=.*|${var_name}=${var_value}|" "$env_file"
else
echo "${var_name}=${var_value}" >> "$env_file"
fi
}
set_env_var "PG_DATABASE_URL" "postgres://postgres:postgres@localhost:5432/main_branch"
set_env_var "NODE_PORT" "${{ env.MAIN_SERVER_PORT }}"
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Seed main branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
- name: Start main branch server in background
run: |
echo "=== Main branch .env file contents ==="
cat packages/twenty-server/.env
echo "=== Starting main branch server ==="
nohup npx nx run twenty-server:start:prod > /tmp/main-server.log 2>&1 &
echo $! > /tmp/main-server.pid
echo "Main server PID: $(cat /tmp/main-server.pid)"
- name: Wait for main branch server to be ready
run: |
echo "Waiting for main branch server to start..."
timeout=300
interval=5
elapsed=0
while [ $elapsed -lt $timeout ]; do
if curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" > /dev/null 2>&1 && \
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" > /dev/null 2>&1; then
echo "Main branch server is ready!"
break
fi
echo "Main branch server not ready yet, waiting ${interval}s..."
sleep $interval
elapsed=$((elapsed + interval))
done
if [ $elapsed -ge $timeout ]; then
echo "Timeout waiting for main branch server to start"
echo "Main server log:"
cat /tmp/main-server.log || echo "No main server log found"
exit 1
fi
- name: Download GraphQL and REST responses from main branch
run: |
# Admin token from jest-integration.config.ts
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtOWUzYi00NmQ0LWE1NTYtODhiOWRkYzJiMDM1IiwiaWF0IjoxNzM5NTQ3NjYxLCJleHAiOjMzMjk3MTQ3NjYxfQ.fbOM9yhr3jWDicPZ1n771usUURiPGmNdeFApsgrbxOw"
# Load introspection query from file
INTROSPECTION_QUERY=$(cat packages/twenty-utils/graphql-introspection-query.graphql)
# Prepare the query payload
QUERY_PAYLOAD=$(echo "$INTROSPECTION_QUERY" | tr '\n' ' ' | sed 's/"/\\"/g')
echo "Downloading GraphQL schema from main server..."
curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o main-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
echo "Downloading GraphQL metadata schema from main server..."
curl -X POST "http://localhost:${{ env.MAIN_SERVER_PORT }}/metadata" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-d "{\"query\":\"${QUERY_PAYLOAD}\"}" \
-o main-metadata-schema-introspection.json \
-w "HTTP Status: %{http_code}\n" \
-s
# Download main branch OpenAPI specs
echo "Downloading OpenAPI specifications from main server..."
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/core" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o main-rest-api.json \
-w "HTTP Status: %{http_code}\n"
curl -s "http://localhost:${{ env.MAIN_SERVER_PORT }}/rest/open-api/metadata" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-o main-rest-metadata-api.json \
-w "HTTP Status: %{http_code}\n"
# Verify the downloads
echo "Main branch files downloaded:"
ls -la main-*
- name: Restore current branch files
run: |
# Move current branch files back to working directory
mv /tmp/current-branch-files/* . 2>/dev/null || echo "No files to restore"
# Verify all files are present
echo "All API files restored:"
ls -la current-* main-* 2>/dev/null || echo "Some files may be missing"
# Clean up temp directory
rm -rf /tmp/current-branch-files
- name: Install OpenAPI Diff Tool
run: |
# Using the Java-based OpenAPITools/openapi-diff via Docker
echo "Using OpenAPITools/openapi-diff via Docker"
- name: Generate GraphQL Schema Diff Reports
run: |
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
npm install -g @graphql-inspector/cli
echo "=== GENERATING GRAPHQL DIFF REPORTS ==="
# Check if GraphQL schema has changes
echo "Checking GraphQL schema for changes..."
if graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL schema"
# Don't create a diff file for no changes
else
echo "⚠️ Changes detected in GraphQL schema, generating report..."
echo "# GraphQL Schema Changes" > graphql-schema-diff.md
echo "" >> graphql-schema-diff.md
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json >> graphql-schema-diff.md 2>&1 || {
echo "⚠️ **Breaking changes or errors detected in GraphQL schema**" >> graphql-schema-diff.md
echo "" >> graphql-schema-diff.md
echo "\`\`\`" >> graphql-schema-diff.md
graphql-inspector diff main-schema-introspection.json current-schema-introspection.json 2>&1 >> graphql-schema-diff.md || echo "Error generating diff" >> graphql-schema-diff.md
echo "\`\`\`" >> graphql-schema-diff.md
}
fi
# Check if GraphQL metadata schema has changes
echo "Checking GraphQL metadata schema for changes..."
if graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >/dev/null 2>&1; then
echo "✅ No changes in GraphQL metadata schema"
# Don't create a diff file for no changes
else
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
echo "# GraphQL Metadata Schema Changes" > graphql-metadata-diff.md
echo "" >> graphql-metadata-diff.md
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json >> graphql-metadata-diff.md 2>&1 || {
echo "⚠️ **Breaking changes or errors detected in GraphQL metadata schema**" >> graphql-metadata-diff.md
echo "" >> graphql-metadata-diff.md
echo "\`\`\`" >> graphql-metadata-diff.md
graphql-inspector diff main-metadata-schema-introspection.json current-metadata-schema-introspection.json 2>&1 >> graphql-metadata-diff.md || echo "Error generating diff" >> graphql-metadata-diff.md
echo "\`\`\`" >> graphql-metadata-diff.md
}
fi
# Show summary
echo "Generated diff files:"
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
- name: Check REST API Breaking Changes
run: |
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
# Use the Java-based openapi-diff via Docker
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \
--json /specs/rest-api-diff.json \
/specs/main-rest-api.json /specs/current-rest-api.json || echo "OpenAPI diff completed with exit code $?"
# Check if the output file was created and is valid JSON
if [ -f "rest-api-diff.json" ] && jq empty rest-api-diff.json 2>/dev/null; then
# Check for breaking changes using Java openapi-diff JSON structure
incompatible=$(jq -r '.incompatible // false' rest-api-diff.json)
different=$(jq -r '.different // false' rest-api-diff.json)
# Count changes
new_endpoints=$(jq -r '.newEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0")
missing_endpoints=$(jq -r '.missingEndpoints | length' rest-api-diff.json 2>/dev/null || echo "0")
changed_operations=$(jq -r '.changedOperations | length' rest-api-diff.json 2>/dev/null || echo "0")
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST API"
# Generate breaking changes report
echo "# REST API Breaking Changes" > rest-api-diff.md
echo "" >> rest-api-diff.md
echo "⚠️ **Breaking changes detected that may affect existing API consumers**" >> rest-api-diff.md
echo "" >> rest-api-diff.md
# Parse and format the changes from Java openapi-diff
jq -r '
if (.missingEndpoints | length) > 0 then
"## 🚨 Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" +
(.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n"))
else "" end,
if (.changedOperations | length) > 0 then
"\n## ⚠️ Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
(.changedOperations | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n"))
else "" end,
if (.newEndpoints | length) > 0 then
"\n## ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
else "" end
' rest-api-diff.json >> rest-api-diff.md
elif [ "$different" = "true" ]; then
echo "📝 Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed)"
# Generate non-breaking changes report
echo "# REST API Changes" > rest-api-diff.md
echo "" >> rest-api-diff.md
echo "## Summary" >> rest-api-diff.md
jq -r '
if (.newEndpoints | length) > 0 then
"### ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
else "" end,
if (.changedOperations | length) > 0 then
"\n### 🔄 Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
(.changedOperations | map("- " + .method + " " + .pathUrl + ": " + (.summary // "Modified operation")) | join("\n"))
else "" end
' rest-api-diff.json >> rest-api-diff.md
else
echo "✅ No changes detected in REST API"
# Don't create diff file for no changes
fi
else
echo "⚠️ OpenAPI diff tool could not process the files"
echo "# REST API Analysis Error" > rest-api-diff.md
echo "" >> rest-api-diff.md
echo "⚠️ **Error occurred while analyzing REST API changes**" >> rest-api-diff.md
echo "" >> rest-api-diff.md
echo "## Error Output" >> rest-api-diff.md
echo "\`\`\`" >> rest-api-diff.md
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-api.json /specs/current-rest-api.json 2>&1 >> rest-api-diff.md || echo "Could not capture error output"
echo "\`\`\`" >> rest-api-diff.md
# Don't fail the workflow for tool errors
echo "::warning::REST API analysis tool error - continuing workflow"
fi
- name: Check REST Metadata API Breaking Changes
run: |
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
# Use the Java-based openapi-diff for metadata API as well
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest \
--json /specs/rest-metadata-api-diff.json \
/specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json || echo "OpenAPI diff completed with exit code $?"
# Check if the output file was created and is valid JSON
if [ -f "rest-metadata-api-diff.json" ] && jq empty rest-metadata-api-diff.json 2>/dev/null; then
# Check for breaking changes using Java openapi-diff JSON structure
incompatible=$(jq -r '.incompatible // false' rest-metadata-api-diff.json)
different=$(jq -r '.different // false' rest-metadata-api-diff.json)
# Count changes
new_endpoints=$(jq -r '.newEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
missing_endpoints=$(jq -r '.missingEndpoints | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
changed_operations=$(jq -r '.changedOperations | length' rest-metadata-api-diff.json 2>/dev/null || echo "0")
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST Metadata API"
# Generate breaking changes report
echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "⚠️ **Breaking changes detected that may affect existing API consumers**" >> rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
# Parse and format the changes from Java openapi-diff
jq -r '
if (.missingEndpoints | length) > 0 then
"## 🚨 Removed Endpoints (" + (.missingEndpoints | length | tostring) + ")\n" +
(.missingEndpoints | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "")) | join("\n"))
else "" end,
if (.changedOperations | length) > 0 then
"\n## ⚠️ Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
(.changedOperations | map("- **" + .method + " " + .pathUrl + "**: " + (.summary // "Modified operation")) | join("\n"))
else "" end,
if (.newEndpoints | length) > 0 then
"\n## ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
else "" end
' rest-metadata-api-diff.json >> rest-metadata-api-diff.md
elif [ "$different" = "true" ]; then
echo "📝 Non-breaking changes detected ($new_endpoints new endpoints, $missing_endpoints removed, $changed_operations changed)"
# Generate non-breaking changes report
echo "# REST Metadata API Changes" > rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "## Summary" >> rest-metadata-api-diff.md
jq -r '
if (.newEndpoints | length) > 0 then
"### ✅ New Endpoints (" + (.newEndpoints | length | tostring) + ")\n" +
(.newEndpoints | map("- " + .method + " " + .pathUrl + ": " + (.summary // "")) | join("\n"))
else "" end,
if (.changedOperations | length) > 0 then
"\n### 🔄 Changed Operations (" + (.changedOperations | length | tostring) + ")\n" +
(.changedOperations | map("- " + .method + " " + .pathUrl + ": " + (.summary // "Modified operation")) | join("\n"))
else "" end
' rest-metadata-api-diff.json >> rest-metadata-api-diff.md
else
echo "✅ No changes detected in REST Metadata API"
# Don't create diff file for no changes
fi
else
echo "⚠️ OpenAPI diff tool could not process the metadata API files"
echo "# REST Metadata API Analysis Error" > rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "⚠️ **Error occurred while analyzing REST Metadata API changes**" >> rest-metadata-api-diff.md
echo "" >> rest-metadata-api-diff.md
echo "## Error Output" >> rest-metadata-api-diff.md
echo "\`\`\`" >> rest-metadata-api-diff.md
docker run --rm -v "$(pwd):/specs" openapitools/openapi-diff:latest /specs/main-rest-metadata-api.json /specs/current-rest-metadata-api.json 2>&1 >> rest-metadata-api-diff.md || echo "Could not capture error output"
echo "\`\`\`" >> rest-metadata-api-diff.md
# Don't fail the workflow for tool errors
echo "::warning::REST Metadata API analysis tool error - continuing workflow"
fi
- name: Comment API Changes on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let hasChanges = false;
let comment = '';
try {
if (fs.existsSync('graphql-schema-diff.md')) {
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
if (graphqlDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += '### GraphQL Schema Changes\n' + graphqlDiff + '\n\n';
}
}
if (fs.existsSync('graphql-metadata-diff.md')) {
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
if (graphqlMetadataDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += '### GraphQL Metadata Schema Changes\n' + graphqlMetadataDiff + '\n\n';
}
}
if (fs.existsSync('rest-api-diff.md')) {
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
if (restDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += restDiff + '\n\n';
}
}
if (fs.existsSync('rest-metadata-api-diff.md')) {
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
if (metadataDiff.trim()) {
if (!hasChanges) {
comment = '## 📊 API Changes Report\n\n';
hasChanges = true;
}
comment += metadataDiff + '\n\n';
}
}
// Only post comment if there are changes
if (hasChanges) {
// Check if there are any breaking changes detected
let hasBreakingChanges = false;
let breakingChangeNote = '';
// Check for breaking changes in any of the diff files
if (fs.existsSync('rest-api-diff.md')) {
const restDiff = fs.readFileSync('rest-api-diff.md', 'utf8');
if (restDiff.includes('Breaking Changes') || restDiff.includes('🚨') ||
restDiff.includes('Removed Endpoints') || restDiff.includes('Changed Operations')) {
hasBreakingChanges = true;
}
}
if (fs.existsSync('rest-metadata-api-diff.md')) {
const metadataDiff = fs.readFileSync('rest-metadata-api-diff.md', 'utf8');
if (metadataDiff.includes('Breaking Changes') || metadataDiff.includes('🚨') ||
metadataDiff.includes('Removed Endpoints') || metadataDiff.includes('Changed Operations')) {
hasBreakingChanges = true;
}
}
// Also check GraphQL changes for breaking changes indicators
if (fs.existsSync('graphql-schema-diff.md')) {
const graphqlDiff = fs.readFileSync('graphql-schema-diff.md', 'utf8');
if (graphqlDiff.includes('Breaking changes') || graphqlDiff.includes('BREAKING')) {
hasBreakingChanges = true;
}
}
if (fs.existsSync('graphql-metadata-diff.md')) {
const graphqlMetadataDiff = fs.readFileSync('graphql-metadata-diff.md', 'utf8');
if (graphqlMetadataDiff.includes('Breaking changes') || graphqlMetadataDiff.includes('BREAKING')) {
hasBreakingChanges = true;
}
}
// Check PR title for "breaking"
const prTitle = "${{ github.event.pull_request.title }}";
const titleContainsBreaking = prTitle.toLowerCase().includes('breaking');
if (hasBreakingChanges) {
if (titleContainsBreaking) {
breakingChangeNote = '\n\n## ✅ Breaking Change Protocol\n\n' +
'**This PR title contains "breaking" and breaking changes were detected - the CI will fail as expected.**\n\n' +
'📝 **Action Required**: Please add `BREAKING CHANGE:` to your commit message to trigger a major version bump.\n\n' +
'Example:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
} else {
breakingChangeNote = '\n\n## ⚠️ Breaking Change Protocol\n\n' +
'**Breaking changes detected but PR title does not contain "breaking" - CI will pass but action needed.**\n\n' +
'🔄 **Options**:\n' +
'1. **If this IS a breaking change**: Add "breaking" to your PR title and add `BREAKING CHANGE:` to your commit message\n' +
'2. **If this is NOT a breaking change**: The API diff tool may have false positives - please review carefully\n\n' +
'For breaking changes, add to commit message:\n```\nfeat: add new API endpoint\n\nBREAKING CHANGE: removed deprecated field from User schema\n```';
}
}
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
const commentBody = COMMENT_MARKER + '\n' + comment + '⚠️ **Please review these API changes carefully before merging.**' + breakingChangeNote;
// Get all comments to find existing API changes comment
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
// Find our existing comment
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing API changes comment');
} else {
// Create new comment
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
console.log('Created new API changes comment');
}
} else {
console.log('No API changes detected - skipping PR comment');
// Check if there's an existing comment to remove
const {data: comments} = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const COMMENT_MARKER = '<!-- API_CHANGES_REPORT -->';
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
if (botComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
});
console.log('Deleted existing API changes comment (no changes detected)');
}
}
} catch (error) {
console.log('Could not post comment:', error);
}
- name: Cleanup servers
if: always()
run: |
if [ -f /tmp/current-server.pid ]; then
kill $(cat /tmp/current-server.pid) || true
fi
if [ -f /tmp/main-server.pid ]; then
kill $(cat /tmp/main-server.pid) || true
fi
- name: Upload API specifications and diffs
if: always()
uses: actions/upload-artifact@v4
with:
name: api-specifications-and-diffs
path: |
/tmp/main-server.log
/tmp/current-server.log
*-api.json
*-schema-introspection.json
*-diff.md
*-diff.json
+12 -8
View File
@@ -89,15 +89,19 @@ jobs:
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 "$CORE_MIGRATION_FILE" ]; then
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/*core-migration-check.ts
rm -f packages/twenty-server/*metadata-migration-check.ts packages/twenty-server/*core-migration-check.ts
exit 1
fi
@@ -145,10 +149,6 @@ jobs:
timeout-minutes: 30
runs-on: depot-ubuntu-24.04-8
needs: server-setup
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
services:
postgres:
image: twentycrm/twenty-postgres-spilo
@@ -187,7 +187,6 @@ jobs:
ANALYTICS_ENABLED: true
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
CLICKHOUSE_PASSWORD: clickhousePassword
SHARD_COUNTER: 4
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
@@ -225,7 +224,12 @@ jobs:
tag: scope:backend
tasks: 'test:integration'
configuration: 'with-db-reset'
args: --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
- 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: 5
+1 -3
View File
@@ -43,6 +43,4 @@ dump.rdb
/flake.nix
.crowdin.yml
.react-email/
mcp.json
.react-email/
+1 -1
View File
@@ -1 +1 @@
22.12.0
18.17.1
+1 -1
View File
@@ -37,7 +37,7 @@
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeVersion": "^22.12.0",
"runtimeVersion": "18",
"runtimeArgs": [
"nx",
"run",
+2 -1
View File
@@ -43,11 +43,12 @@
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"search.exclude": {
"**/.yarn": true
"**/.yarn": true,
},
"eslint.debug": true,
"files.associations": {
".cursorrules": "markdown"
},
"jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}"
}
}
+4 -4
View File
@@ -1,12 +1,12 @@
postgres-on-docker:
docker run -d \
--name twenty_pg \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e PGUSER_SUPERUSER=postgres \
-e PGPASSWORD_SUPERUSER=postgres \
-e ALLOW_NOSSL=true \
-v twenty_db_data:/var/lib/postgresql/data \
-v twenty_db_data:/home/postgres/pgdata \
-p 5432:5432 \
postgres:16
twentycrm/twenty-postgres-spilo:latest
@echo "Waiting for PostgreSQL to be ready..."
@until docker exec twenty_pg psql -U postgres -d postgres \
-c 'SELECT pg_is_in_recovery();' 2>/dev/null | grep -q 'f'; do \
+23 -19
View File
@@ -1,12 +1,16 @@
{
"private": true,
"dependencies": {
"@air/react-drag-to-select": "^5.0.8",
"@apollo/client": "^3.7.17",
"@apollo/server": "^4.7.3",
"@aws-sdk/client-lambda": "^3.700.0",
"@aws-sdk/client-s3": "^3.700.0",
"@aws-sdk/client-sts": "^3.700.0",
"@aws-sdk/credential-providers": "^3.700.0",
"@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",
"@codesandbox/sandpack-react": "^2.13.5",
"@dagrejs/dagre": "^1.1.2",
"@emotion/react": "^11.11.1",
@@ -42,9 +46,9 @@
"@ptc-org/nestjs-query-typeorm": "4.2.1-alpha.2",
"@react-email/components": "0.0.35",
"@react-email/render": "0.0.17",
"@sentry/node": "^9.26.0",
"@sentry/profiling-node": "^9.26.0",
"@sentry/react": "^9.26.0",
"@sentry/node": "^8",
"@sentry/profiling-node": "^8",
"@sentry/react": "^8",
"@sniptt/guards": "^0.2.0",
"@stoplight/elements": "^8.0.5",
"@swc/jest": "^0.2.29",
@@ -67,6 +71,7 @@
"archiver": "^7.0.1",
"axios": "^1.6.2",
"bcrypt": "^5.1.1",
"better-sqlite3": "^9.2.2",
"body-parser": "^1.20.2",
"bullmq": "^5.40.0",
"bytes": "^3.1.2",
@@ -89,7 +94,6 @@
"facepaint": "^1.2.1",
"file-type": "16.5.4",
"framer-motion": "^11.18.0",
"fuse.js": "^7.1.0",
"googleapis": "105",
"graphiql": "^3.1.1",
"graphql": "16.8.0",
@@ -217,7 +221,7 @@
"@nx/vite": "18.3.3",
"@nx/web": "18.3.3",
"@playwright/test": "^1.46.0",
"@sentry/types": "^8",
"@sentry/types": "^7.109.0",
"@storybook/addon-actions": "^7.6.3",
"@storybook/addon-coverage": "^1.0.0",
"@storybook/addon-essentials": "^7.6.7",
@@ -238,11 +242,12 @@
"@swc/cli": "^0.3.12",
"@swc/core": "1.7.42",
"@swc/helpers": "~0.5.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "14.0.0",
"@types/addressparser": "^1.0.3",
"@types/apollo-upload-client": "^17.0.2",
"@types/bcrypt": "^5.0.0",
"@types/better-sqlite3": "^7.6.8",
"@types/bytes": "^3.1.1",
"@types/chrome": "^0.0.267",
"@types/deep-equal": "^1.0.1",
@@ -269,7 +274,7 @@
"@types/lodash.upperfirst": "^4.3.7",
"@types/luxon": "^3.3.0",
"@types/ms": "^0.7.31",
"@types/node": "^22.0.0",
"@types/node": "18.19.26",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/pluralize": "^0.0.33",
@@ -311,7 +316,7 @@
"eslint-plugin-unused-imports": "^3.0.0",
"http-server": "^14.1.1",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"jest-environment-jsdom": "29.7.0",
"jest-environment-node": "^29.4.1",
"jest-fetch-mock": "^3.0.3",
"jsdom": "~22.1.0",
@@ -333,15 +338,14 @@
"ts-node": "10.9.1",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"vite": "^6.3.5",
"vite": "^5.4.0",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-cjs-interop": "^2.2.0",
"vite-plugin-dts": "3.8.1",
"vite-plugin-svgr": "^4.2.0",
"vitest": "1.4.0"
},
"engines": {
"node": "^22.12.0",
"node": "^18.17.1",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
@@ -352,9 +356,9 @@
"graphql": "16.8.0",
"type-fest": "4.10.1",
"typescript": "5.3.3",
"graphql-redis-subscriptions/ioredis": "^5.6.0",
"prosemirror-view": "1.40.0",
"prosemirror-transform": "1.10.4"
"prosemirror-model": "1.23.0",
"yjs": "13.6.18",
"graphql-redis-subscriptions/ioredis": "^5.6.0"
},
"version": "0.2.1",
"nx": {},
@@ -6358,7 +6358,7 @@ export type RelationConnection = {
};
export type RelationDefinition = {
direction: RelationType;
direction: RelationDefinitionType;
relationId: Scalars['UUID'];
sourceFieldMetadata: Field;
sourceObjectMetadata: Object;
@@ -6367,7 +6367,7 @@ export type RelationDefinition = {
};
/** Relation definition type */
export enum RelationType {
export enum RelationDefinitionType {
ManyToMany = 'MANY_TO_MANY',
ManyToOne = 'MANY_TO_ONE',
OneToMany = 'ONE_TO_MANY',
+1
View File
@@ -7,6 +7,7 @@ TAG=latest
#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
@@ -1,4 +1,4 @@
FROM node:22-alpine as twenty-website-build
FROM node:18.17.1-alpine as twenty-website-build
WORKDIR /app
@@ -23,7 +23,7 @@ COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-website /app/packages/twenty-website
RUN npx nx build twenty-website
FROM node:22-alpine as twenty-website
FROM node:18.17.1-alpine as twenty-website
WORKDIR /app/packages/twenty-website
+2 -2
View File
@@ -1,5 +1,5 @@
# Base image for common dependencies
FROM node:22-alpine as common-deps
FROM node:18.17.1-alpine as common-deps
WORKDIR /app
@@ -49,7 +49,7 @@ RUN npx nx build twenty-front
# Final stage: Run the application
FROM node:22-alpine as twenty
FROM node:18.17.1-alpine as twenty
# Used to run healthcheck in docker
RUN apk add --no-cache curl jq
@@ -19,7 +19,7 @@ export const deleteWorkflow = async ({
operationName: 'DeleteOneWorkflow',
variables: { idToDelete: workflowId },
query:
'mutation DeleteOneWorkflow($idToDelete: UUID!) {\n deleteWorkflow(id: $idToDelete) {\n __typename\n deletedAt\n id\n }\n}',
'mutation DeleteOneWorkflow($idToDelete: ID!) {\n deleteWorkflow(id: $idToDelete) {\n __typename\n deletedAt\n id\n }\n}',
},
});
};
@@ -19,7 +19,7 @@ export const destroyWorkflow = async ({
operationName: 'DestroyOneWorkflow',
variables: { idToDestroy: workflowId },
query:
'mutation DestroyOneWorkflow($idToDestroy: UUID!) {\n destroyWorkflow(id: $idToDestroy) {\n id\n __typename\n }\n}',
'mutation DestroyOneWorkflow($idToDestroy: ID!) {\n destroyWorkflow(id: $idToDestroy) {\n id\n __typename\n }\n}',
},
});
};
@@ -30,31 +30,21 @@ test('The workflow run visualizer shows the executed draft version without the l
await launchTestButton.click();
await workflowVisualizer.closeSidePanel();
const goToExecutionPageLink = page.getByRole('link', {
name: 'View execution details',
});
const executionPageUrl = await goToExecutionPageLink.getAttribute('href');
expect(executionPageUrl).not.toBeNull();
await workflowVisualizer.deleteStep(firstStepId);
await page.goto('/objects/workflowRuns');
await page.goto(executionPageUrl!);
const recordTableRowForWorkflowRun = page
.getByRole('row', {
name: workflowVisualizer.workflowName,
})
.first();
const linkToWorkflowRun = recordTableRowForWorkflowRun
.getByRole('link', {
name: workflowVisualizer.workflowName,
})
.first();
await linkToWorkflowRun.click({ force: true });
const workflowRunNameElement = page
const workflowRunName = page
.getByText(`#1 - ${workflowVisualizer.workflowName}`)
.nth(1);
await expect(workflowRunNameElement).toBeVisible();
await expect(workflowRunName).toBeVisible();
const executedFirstStepNode = workflowVisualizer.getStepNode(firstStepId);
@@ -96,6 +86,12 @@ test('Workflow Runs with a pending form step can be opened in the side panel and
await launchTestButton.click();
const goToExecutionPageLink = page.getByRole('link', {
name: 'View execution details',
});
await expect(goToExecutionPageLink).toBeVisible();
await workflowVisualizer.seeRunsButton.click();
const workflowRunName = `#1 - ${workflowVisualizer.workflowName}`;
+3 -3
View File
@@ -27,13 +27,13 @@
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"engines": {
"node": "^22.12.0",
"node": "^18.17.1",
"npm": "please-use-yarn",
"yarn": "^4.0.2"
}
@@ -7,7 +7,7 @@ const logoStyle = {
export const Logo = () => {
return (
<Img
src="https://app.twenty.com/images/icons/windows11/Square150x150Logo.scale-100.png"
src="https://app.twenty.com/icons/windows11/Square150x150Logo.scale-100.png"
alt="Twenty logo"
width="40"
height="40"
+1 -2
View File
@@ -11,5 +11,4 @@ VITE_DISABLE_ESLINT_CHECKER=true
# VITE_ENABLE_SSL=false
# VITE_HOST=localhost.com
# SSL_KEY_PATH="./certs/your-cert.key"
# SSL_CERT_PATH="./certs/your-cert.crt"
# IS_DEBUG_MODE=false
# SSL_CERT_PATH="./certs/your-cert.crt"
+2 -2
View File
@@ -7,10 +7,10 @@ import { useDarkMode } from 'storybook-dark-mode';
import { RootDecorator } from '../src/testing/decorators/RootDecorator';
import { mockedUserJWT } from '../src/testing/mock-data/jwt';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
import 'react-loading-skeleton/dist/skeleton.css';
import 'twenty-ui/style.css';
import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui/theme';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
initialize({
onUnhandledRequest: async (request: Request) => {
@@ -48,7 +48,7 @@ const preview: Preview = {
<ThemeProvider theme={theme}>
<ThemeContextProvider theme={theme}>
<ClickOutsideListenerContext.Provider
value={{ excludedClickOutsideId: undefined }}
value={{ excludeClassName: undefined }}
>
<Story />
</ClickOutsideListenerContext.Provider>
@@ -1,5 +1,3 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
module.exports = {
schema:
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
-2
View File
@@ -1,5 +1,3 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
module.exports = {
schema:
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
+3 -3
View File
@@ -6,11 +6,11 @@
<link
rel="icon"
type="image/x-icon"
href="/images/icons/android/android-launchericon-48-48.png"
href="images/icons/android/android-launchericon-48-48.png"
data-rh="true"
/>
<link rel="apple-touch-icon" href="/images/icons/ios/192.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="images/icons/ios/192.png" />
<link rel="manifest" href="manifest.json" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="A modern open-source CRM" />
+1 -1
View File
@@ -56,7 +56,7 @@ const jestConfig: JestConfigWithTsJest = {
global: {
statements: 57,
lines: 55,
functions: 46,
functions: 47,
},
},
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
+7 -10
View File
@@ -3,13 +3,13 @@
"private": true,
"type": "module",
"scripts": {
"build": "NODE_ENV=production VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh",
"build:sourcemaps": "NODE_ENV=production VITE_BUILD_SOURCEMAP=true VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh",
"start:prod": "NODE_ENV=production npx serve -s build",
"build": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=4500 npx vite build && sh ./scripts/inject-runtime-env.sh",
"build:sourcemaps": "VITE_BUILD_SOURCEMAP=true VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=7000 npx vite build && sh ./scripts/inject-runtime-env.sh",
"start:prod": "NODE_ENV=production npx vite --host",
"tsup": "npx tsup"
},
"engines": {
"node": "^22.12.0",
"node": "^18.17.1",
"npm": "please-use-yarn",
"yarn": "^4.0.2"
},
@@ -29,10 +29,8 @@
"workerDirectory": "public"
},
"dependencies": {
"@blocknote/mantine": "^0.31.1",
"@blocknote/react": "^0.31.1",
"@blocknote/xl-docx-exporter": "^0.31.1",
"@blocknote/xl-pdf-exporter": "^0.31.1",
"@blocknote/xl-docx-exporter": "^0.22.0",
"@blocknote/xl-pdf-exporter": "^0.22.0",
"@cyntler/react-doc-viewer": "^1.17.0",
"@lingui/core": "^5.1.2",
"@lingui/detect-locale": "^5.2.0",
@@ -84,7 +82,6 @@
"eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-unicorn": "^51.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"optionator": "^0.9.1",
"rollup-plugin-visualizer": "^5.14.0"
"optionator": "^0.9.1"
}
}
@@ -26,12 +26,14 @@ const documents = {
"\n \n query GetOneDatabaseConnection($input: RemoteServerIdInput!) {\n findOneRemoteServerById(input: $input) {\n ...RemoteServerFields\n }\n }\n": types.GetOneDatabaseConnectionDocument,
"\n mutation CreateOneObjectMetadataItem($input: CreateOneObjectInput!) {\n createOneObject(input: $input) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.CreateOneObjectMetadataItemDocument,
"\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n defaultValue\n options\n isLabelSyncedWithName\n }\n }\n": types.CreateOneFieldMetadataItemDocument,
"\n mutation CreateOneRelationMetadataItem(\n $input: CreateOneRelationMetadataInput!\n ) {\n createOneRelationMetadata(input: $input) {\n id\n relationType\n fromObjectMetadataId\n toObjectMetadataId\n fromFieldMetadataId\n toFieldMetadataId\n createdAt\n updatedAt\n }\n }\n": types.CreateOneRelationMetadataItemDocument,
"\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n isLabelSyncedWithName\n }\n }\n": types.UpdateOneFieldMetadataItemDocument,
"\n mutation UpdateOneObjectMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateObjectPayload!\n ) {\n updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.UpdateOneObjectMetadataItemDocument,
"\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n isSearchable\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n isLabelSyncedWithName\n }\n }\n": types.DeleteOneObjectMetadataItemDocument,
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n logs\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
@@ -109,6 +111,10 @@ export function graphql(source: "\n mutation CreateOneObjectMetadataItem($input
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n defaultValue\n options\n isLabelSyncedWithName\n }\n }\n"): (typeof documents)["\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n defaultValue\n options\n isLabelSyncedWithName\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation CreateOneRelationMetadataItem(\n $input: CreateOneRelationMetadataInput!\n ) {\n createOneRelationMetadata(input: $input) {\n id\n relationType\n fromObjectMetadataId\n toObjectMetadataId\n fromFieldMetadataId\n toFieldMetadataId\n createdAt\n updatedAt\n }\n }\n"): (typeof documents)["\n mutation CreateOneRelationMetadataItem(\n $input: CreateOneRelationMetadataInput!\n ) {\n createOneRelationMetadata(input: $input) {\n id\n relationType\n fromObjectMetadataId\n toObjectMetadataId\n fromFieldMetadataId\n toFieldMetadataId\n createdAt\n updatedAt\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -128,11 +134,15 @@ export function graphql(source: "\n mutation DeleteOneFieldMetadataItem($idToDe
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relation {\n type\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
export function graphql(source: "\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n"): (typeof documents)["\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"];
export function graphql(source: "\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
import { renderHook } from '@testing-library/react';
import { useIsMatchingLocation } from '../useIsMatchingLocation';
import { MemoryRouter } from 'react-router-dom';
import { AppBasePath } from '@/types/AppBasePath';
const Wrapper =
(initialIndex = 0) =>
({ children }: { children: React.ReactNode }) => (
<MemoryRouter
initialEntries={['/example', '/other', `${AppBasePath.Settings}/example`]}
initialIndex={initialIndex}
>
{children}
</MemoryRouter>
);
describe('useIsMatchingLocation', () => {
it('returns true when paths match with no basePath', () => {
const { result } = renderHook(() => useIsMatchingLocation(), {
wrapper: Wrapper(),
});
expect(result.current.isMatchingLocation('/example')).toBe(true);
});
it('returns false when paths do not match with no basePath', () => {
const { result } = renderHook(() => useIsMatchingLocation(), {
wrapper: Wrapper(),
});
expect(result.current.isMatchingLocation('/non-match')).toBe(false);
});
it('returns true when paths match with basePath', () => {
const { result } = renderHook(() => useIsMatchingLocation(), {
wrapper: Wrapper(2),
});
expect(
result.current.isMatchingLocation('example', AppBasePath.Settings),
).toBe(true);
});
it('returns false when paths do not match with basePath', () => {
const { result } = renderHook(() => useIsMatchingLocation(), {
wrapper: Wrapper(),
});
expect(
result.current.isMatchingLocation('non-match', AppBasePath.Settings),
).toBe(false);
});
it('handles trailing slashes in basePath correctly', () => {
const { result } = renderHook(() => useIsMatchingLocation(), {
wrapper: Wrapper(2),
});
expect(
result.current.isMatchingLocation(
'example',
(AppBasePath.Settings + '/') as AppBasePath,
),
).toBe(true);
});
it('handles without basePath correctly', () => {
const { result } = renderHook(() => useIsMatchingLocation(), {
wrapper: Wrapper(),
});
expect(result.current.isMatchingLocation('example')).toBe(true);
});
});
@@ -3,15 +3,14 @@ import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePat
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath';
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
import { expect } from '@storybook/test';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { OnboardingStatus } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
import { UNTESTED_APP_PATHS } from '~/testing/constants/UntestedAppPaths';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
jest.mock('@/onboarding/hooks/useOnboardingStatus');
const setupMockOnboardingStatus = (
@@ -29,13 +28,13 @@ const setupMockIsWorkspaceActivationStatusEqualsTo = (
.mockReturnValueOnce(isWorkspaceSuspended);
};
jest.mock('~/utils/isMatchingLocation');
const mockIsMatchingLocation = jest.mocked(isMatchingLocation);
jest.mock('~/hooks/useIsMatchingLocation');
const mockUseIsMatchingLocation = jest.mocked(useIsMatchingLocation);
const setupMockIsMatchingLocation = (pathname: string) => {
mockIsMatchingLocation.mockImplementation(
(_location, path) => path === pathname,
);
mockUseIsMatchingLocation.mockReturnValueOnce({
isMatchingLocation: (path: string) => path === pathname,
});
};
jest.mock('@/auth/hooks/useIsLogged');
@@ -58,14 +57,10 @@ const setupMockUseParams = (objectNamePlural?: string) => {
};
jest.mock('recoil');
const setupMockRecoil = (
objectNamePlural?: string,
verifyEmailNextPath?: string,
) => {
const setupMockRecoil = (objectNamePlural?: string) => {
jest
.mocked(useRecoilValue)
.mockReturnValueOnce([{ namePlural: objectNamePlural ?? '' }])
.mockReturnValueOnce(verifyEmailNextPath);
.mockReturnValueOnce([{ namePlural: objectNamePlural ?? '' }]);
};
// prettier-ignore
@@ -77,7 +72,6 @@ const testCases: {
res: string | undefined;
objectNamePluralFromParams?: string;
objectNamePluralFromMetadata?: string;
verifyEmailNextPath?: string;
}[] = [
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' },
@@ -116,9 +110,7 @@ const testCases: {
{ loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, verifyEmailNextPath: '/nextPath?key=value', res: '/nextPath?key=value' },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' },
{ loc: AppPath.VerifyEmail, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, verifyEmailNextPath: '/nextPath?key=value', res: undefined },
{ loc: AppPath.VerifyEmail, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
{ loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
@@ -283,7 +275,6 @@ describe('usePageChangeEffectNavigateLocation', () => {
isLoggedIn,
objectNamePluralFromParams,
objectNamePluralFromMetadata,
verifyEmailNextPath,
res,
}) => {
setupMockIsMatchingLocation(loc);
@@ -291,7 +282,7 @@ describe('usePageChangeEffectNavigateLocation', () => {
setupMockIsWorkspaceActivationStatusEqualsTo(isWorkspaceSuspended);
setupMockIsLogged(isLoggedIn);
setupMockUseParams(objectNamePluralFromParams);
setupMockRecoil(objectNamePluralFromMetadata, verifyEmailNextPath);
setupMockRecoil(objectNamePluralFromMetadata);
expect(usePageChangeEffectNavigateLocation()).toEqual(res);
},
@@ -303,8 +294,7 @@ describe('usePageChangeEffectNavigateLocation', () => {
(Object.keys(OnboardingStatus).length +
['isWorkspaceSuspended:true', 'isWorkspaceSuspended:false']
.length) +
['nonExistingObjectInParam', 'existingObjectInParam:false'].length +
['caseWithRedirectionToVerifyEmailNextPath', 'caseWithout'].length,
['nonExistingObjectInParam', 'existingObjectInParam:false'].length,
);
});
});
@@ -0,0 +1,34 @@
import { matchPath, useLocation } from 'react-router-dom';
import { AppBasePath } from '@/types/AppBasePath';
import { isNonEmptyString } from '@sniptt/guards';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
const addTrailingSlash = (path: string) =>
path.endsWith('/') ? path : path + '/';
const getConstructedPath = (path: string, basePath?: AppBasePath) => {
if (!isNonEmptyString(basePath)) return path;
return addTrailingSlash(basePath) + path;
};
export const useIsMatchingLocation = () => {
const location = useLocation();
const isMatchingLocation = useCallback(
(path: string, basePath?: AppBasePath) => {
const match = matchPath(
getConstructedPath(path, basePath),
location.pathname,
);
return isDefined(match);
},
[location.pathname],
);
return {
isMatchingLocation,
};
};
@@ -1,4 +1,3 @@
import { verifyEmailNextPathState } from '@/app/states/verifyEmailNextPathState';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@@ -6,24 +5,24 @@ import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
import { useLocation, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { OnboardingStatus } from '~/generated/graphql';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
export const usePageChangeEffectNavigateLocation = () => {
const { isMatchingLocation } = useIsMatchingLocation();
const isLoggedIn = useIsLogged();
const onboardingStatus = useOnboardingStatus();
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
WorkspaceActivationStatus.SUSPENDED,
);
const { defaultHomePagePath } = useDefaultHomePagePath();
const location = useLocation();
const someMatchingLocationOf = (appPaths: AppPath[]): boolean =>
appPaths.some((appPath) => isMatchingLocation(location, appPath));
appPaths.some((appPath) => isMatchingLocation(appPath));
const onGoingUserCreationPaths = [
AppPath.Invite,
AppPath.SignInUp,
@@ -44,7 +43,6 @@ export const usePageChangeEffectNavigateLocation = () => {
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
);
const verifyEmailNextPath = useRecoilValue(verifyEmailNextPathState);
if (
!isLoggedIn &&
@@ -60,19 +58,10 @@ export const usePageChangeEffectNavigateLocation = () => {
onboardingStatus === OnboardingStatus.PLAN_REQUIRED &&
!someMatchingLocationOf([AppPath.PlanRequired, AppPath.PlanRequiredSuccess])
) {
if (
isMatchingLocation(location, AppPath.VerifyEmail) &&
isDefined(verifyEmailNextPath)
) {
return verifyEmailNextPath;
}
return AppPath.PlanRequired;
}
if (
isWorkspaceSuspended &&
!isMatchingLocation(location, AppPath.SettingsCatchAll)
) {
if (isWorkspaceSuspended && !isMatchingLocation(AppPath.SettingsCatchAll)) {
return `${AppPath.SettingsCatchAll.replace('/*', '')}/${
SettingsPath.Billing
}`;
@@ -90,21 +79,21 @@ export const usePageChangeEffectNavigateLocation = () => {
if (
onboardingStatus === OnboardingStatus.PROFILE_CREATION &&
!isMatchingLocation(location, AppPath.CreateProfile)
!isMatchingLocation(AppPath.CreateProfile)
) {
return AppPath.CreateProfile;
}
if (
onboardingStatus === OnboardingStatus.SYNC_EMAIL &&
!isMatchingLocation(location, AppPath.SyncEmails)
!isMatchingLocation(AppPath.SyncEmails)
) {
return AppPath.SyncEmails;
}
if (
onboardingStatus === OnboardingStatus.INVITE_TEAM &&
!isMatchingLocation(location, AppPath.InviteTeam)
!isMatchingLocation(AppPath.InviteTeam)
) {
return AppPath.InviteTeam;
}
@@ -112,18 +101,18 @@ export const usePageChangeEffectNavigateLocation = () => {
if (
onboardingStatus === OnboardingStatus.COMPLETED &&
someMatchingLocationOf([...onboardingPaths, ...onGoingUserCreationPaths]) &&
!isMatchingLocation(location, AppPath.ResetPassword) &&
!isMatchingLocation(AppPath.ResetPassword) &&
isLoggedIn
) {
return defaultHomePagePath;
}
if (isMatchingLocation(location, AppPath.Index) && isLoggedIn) {
if (isMatchingLocation(AppPath.Index) && isLoggedIn) {
return defaultHomePagePath;
}
if (
isMatchingLocation(location, AppPath.RecordIndexPage) &&
isMatchingLocation(AppPath.RecordIndexPage) &&
!isDefined(objectMetadataItem)
) {
return AppPath.NotFound;
@@ -2,13 +2,12 @@ import { getOperationName } from '@apollo/client/utilities';
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { HttpResponse, graphql, http } from 'msw';
import { HttpResponse, graphql } from 'msw';
import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain';
import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage';
import {
PageDecorator,
@@ -22,9 +21,6 @@ import { mockedUserData } from '~/testing/mock-data/users';
const userMetadataLoaderMocks = {
msw: {
handlers: [
http.get(`${REACT_APP_SERVER_BASE_URL}/client-config`, () => {
return HttpResponse.json(mockedClientConfig);
}),
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
data: {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,17 +0,0 @@
import { Locale } from 'date-fns';
import { enUS } from 'date-fns/locale';
import { APP_LOCALES } from 'twenty-shared/translations';
import { createState } from 'twenty-ui/utilities';
type DateLocaleState = {
locale?: keyof typeof APP_LOCALES;
localeCatalog: Locale;
};
export const dateLocaleState = createState<DateLocaleState>({
key: 'dateLocaleState',
defaultValue: {
locale: undefined,
localeCatalog: enUS,
},
});

Some files were not shown because too many files have changed in this diff Show More