Compare commits

..
Author SHA1 Message Date
Abdullah 9483104ee4 feat(website): add sliding hero tabs and improve mobile nav color sync
Introduce a sliding tab indicator for the product AI hero and align the
sticky nav transition on mobile with the intro/AI boundary using measured
nav height, earlier lead offset, and blur-free solid backgrounds.
2026-05-20 04:58:57 +05:00
Abdullah e37517dc3d fix(website): polish product hero opacity sync and hydration
Align dark overlay and nav opacity with the AI panel when fully in view,
use stable tab ids and remove Edit icon clipPath useId to fix SSR mismatches,
and stabilize menu scroll effect dependencies.
2026-05-20 04:22:02 +05:00
Abdullah a4161833d0 feat(website): sync product nav with hero scroll color transition
Drive the product page navbar from hero scroll progress with opacity-based
light-to-dark transitions, including fade back to light when leaving the AI section.
2026-05-20 04:09:13 +05:00
Abdullah ac03df1f5f feat(website): add scroll-driven product hero with stacked mobile layout
Replace the static product hero with a unified intro-to-AI scroll transition on
desktop and a stacked mobile layout, including taller preview sizing and updated
AI heading copy.
2026-05-20 04:09:08 +05:00
4272 changed files with 81976 additions and 190947 deletions
+1 -1
View File
@@ -50,4 +50,4 @@ runs:
- name: Deploy
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty app:publish --private --remote target
run: yarn twenty deploy --remote target
@@ -50,4 +50,4 @@ runs:
- name: Install
shell: bash
working-directory: ${{ inputs.app-path }}
run: yarn twenty app:install --remote target
run: yarn twenty install --remote target
+3 -28
View File
@@ -6,43 +6,18 @@ permissions:
on:
push:
tags:
- 'twenty/v*'
- 'sdk/v*'
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
- 'v*'
jobs:
dispatch-tag:
deploy-tag:
timeout-minutes: 3
runs-on: ubuntu-latest
steps:
- name: Resolve dispatch event from tag family
id: target
env:
REF_NAME: ${{ github.ref_name }}
run: |
case "$REF_NAME" in
twenty/v*)
event_type=auto-deploy-twenty
;;
sdk/v*)
event_type=auto-publish-npm
;;
*)
echo "Unsupported tag '$REF_NAME', expected 'twenty/v*' or 'sdk/v*'." >&2
exit 1
;;
esac
printf 'event_type=%s\n' "$event_type" >> "$GITHUB_OUTPUT"
- name: Repository Dispatch
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
EVENT_TYPE: ${{ steps.target.outputs.event_type }}
REF_NAME: ${{ github.ref_name }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches \
-f "event_type=$EVENT_TYPE" \
-f event_type=auto-deploy-tag \
-f "client_payload[github][ref_name]=$REF_NAME"
+5 -91
View File
@@ -144,9 +144,6 @@ jobs:
npx nx run twenty-server:database:init:prod
- name: Flush cache before seeding current branch
run: npx nx command-no-deps twenty-server -- cache:flush
- name: Seed current branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
@@ -163,7 +160,7 @@ jobs:
- name: Wait for current branch server to be ready
run: |
echo "Waiting for current branch server to start..."
timeout=60
timeout=300
interval=5
elapsed=0
@@ -188,10 +185,9 @@ jobs:
done
if [ $elapsed -ge $timeout ]; then
echo "Timed out waiting for current branch server to serve a valid schema."
echo "::warning::Timed out waiting for current branch server to serve a valid schema. Validation will skip the API diff."
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
@@ -315,9 +311,6 @@ jobs:
npx nx run twenty-server:database:init:prod
- name: Flush cache before seeding main branch
run: npx nx command-no-deps twenty-server -- cache:flush
- name: Seed main branch database with test data
run: |
npx nx command-no-deps twenty-server -- workspace:seed:dev
@@ -359,10 +352,9 @@ jobs:
done
if [ $elapsed -ge $timeout ]; then
echo "Timed out waiting for main branch server to serve a valid schema."
echo "::warning::Timed out waiting for main branch server to serve a valid schema. Validation will skip the API diff."
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
@@ -458,7 +450,6 @@ jobs:
echo "Using OpenAPITools/openapi-diff via Docker"
- name: Generate GraphQL Schema Diff Reports
id: graphql-diff
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== INSTALLING GRAPHQL INSPECTOR CLI ==="
@@ -472,7 +463,6 @@ jobs:
echo "✅ No changes in GraphQL schema"
else
echo "⚠️ Changes detected in GraphQL schema, generating report..."
echo "core_breaking=true" >> $GITHUB_OUTPUT
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 || {
@@ -490,7 +480,6 @@ jobs:
echo "✅ No changes in GraphQL metadata schema"
else
echo "⚠️ Changes detected in GraphQL metadata schema, generating report..."
echo "metadata_breaking=true" >> $GITHUB_OUTPUT
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 || {
@@ -507,7 +496,6 @@ jobs:
ls -la *-diff.md 2>/dev/null || echo "No diff files generated (no changes detected)"
- name: Check REST API Breaking Changes
id: rest-diff
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST API FOR BREAKING CHANGES ==="
@@ -530,7 +518,6 @@ jobs:
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST API"
echo "breaking=true" >> $GITHUB_OUTPUT
# Generate breaking changes report
echo "# REST API Breaking Changes" > rest-api-diff.md
@@ -578,7 +565,6 @@ jobs:
fi
- name: Check REST Metadata API Breaking Changes
id: rest-metadata-diff
if: steps.validate-schemas.outputs.valid == 'true'
run: |
echo "=== CHECKING REST METADATA API FOR BREAKING CHANGES ==="
@@ -601,7 +587,6 @@ jobs:
if [ "$incompatible" = "true" ]; then
echo "❌ Breaking changes detected in REST Metadata API"
echo "breaking=true" >> $GITHUB_OUTPUT
# Generate breaking changes report (only for breaking changes)
echo "# REST Metadata API Breaking Changes" > rest-metadata-api-diff.md
@@ -647,79 +632,6 @@ jobs:
echo "::warning::REST Metadata API analysis tool error - continuing workflow"
fi
- name: Fail on breaking changes
if: steps.validate-schemas.outputs.valid == 'true'
run: |
breaking=false
if [ "${{ steps.graphql-diff.outputs.core_breaking }}" = "true" ]; then
echo "❌ GraphQL core schema has breaking changes"
breaking=true
if [ -f graphql-schema-diff.md ]; then
echo ""
cat graphql-schema-diff.md
echo ""
fi
fi
if [ "${{ steps.graphql-diff.outputs.metadata_breaking }}" = "true" ]; then
echo "❌ GraphQL metadata schema has breaking changes"
breaking=true
if [ -f graphql-metadata-diff.md ]; then
echo ""
cat graphql-metadata-diff.md
echo ""
fi
fi
if [ "${{ steps.rest-diff.outputs.breaking }}" = "true" ]; then
echo "❌ REST core API has breaking changes"
breaking=true
if [ -f rest-api-diff.json ]; then
echo ""
jq -r '
(if (.missingEndpoints | length) > 0 then
" Removed endpoints:\n" +
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end),
(if (.changedOperations | length) > 0 then
" Changed operations:\n" +
(.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end)
' rest-api-diff.json | sed '/^$/d'
echo ""
fi
fi
if [ "${{ steps.rest-metadata-diff.outputs.breaking }}" = "true" ]; then
echo "❌ REST metadata API has breaking changes"
breaking=true
if [ -f rest-metadata-api-diff.json ]; then
echo ""
jq -r '
(if (.missingEndpoints | length) > 0 then
" Removed endpoints:\n" +
(.missingEndpoints | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end),
(if (.changedOperations | length) > 0 then
" Changed operations:\n" +
(.changedOperations | map(" - " + (.method // "?") + " " + (.pathUrl // "?")) | join("\n"))
else "" end)
' rest-metadata-api-diff.json | sed '/^$/d'
echo ""
fi
fi
if [ "$breaking" = "true" ]; then
echo ""
echo "This PR introduces breaking changes to the public API."
echo "If intentional, deprecate the old endpoint and introduce a new one."
echo "See the breaking changes report artifact and PR comment for details."
exit 1
fi
echo "✅ No breaking API changes detected"
- name: Upload breaking changes report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
@@ -740,3 +652,5 @@ jobs:
if [ -f /tmp/main-server.pid ]; then
kill $(cat /tmp/main-server.pid) || true
fi
-44
View File
@@ -1,44 +0,0 @@
name: CI Codex Plugin
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
changed-files-check:
uses: ./.github/workflows/changed-files.yaml
with:
files: |
package.json
packages/twenty-codex-plugin/**
.github/workflows/ci-codex-plugin.yaml
codex-plugin-validate:
needs: changed-files-check
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Fetch local actions
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 10
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Codex Plugin / Validate
run: npx nx run twenty-codex-plugin:validate
- name: Codex Plugin / Test
run: npx nx run twenty-codex-plugin:test
@@ -105,7 +105,7 @@ jobs:
create-twenty-app --version
mkdir -p /tmp/e2e-test-workspace
cd /tmp/e2e-test-workspace
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --url http://localhost:3000
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --workspace-url http://localhost:3000
- name: Install scaffolded app dependencies
run: |
@@ -120,7 +120,7 @@ jobs:
echo "--- Checking package.json references correct SDK version ---"
node -e "
const pkg = require('./package.json');
const sdkVersion = pkg.devDependencies['twenty-sdk'];
const sdkVersion = pkg.dependencies['twenty-sdk'];
if (!sdkVersion.startsWith('0.0.0-ci.')) {
console.error('Expected twenty-sdk version to start with 0.0.0-ci., got:', sdkVersion);
process.exit(1);
@@ -153,7 +153,7 @@ jobs:
- name: Authenticate with twenty-server
run: |
cd /tmp/e2e-test-workspace/test-app
npx --no-install twenty remote:add --api-key ${{ env.TWENTY_API_KEY }} --url ${{ env.TWENTY_API_URL }}
npx --no-install twenty remote add --api-key ${{ env.TWENTY_API_KEY }} --api-url ${{ env.TWENTY_API_URL }}
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
run: |
@@ -80,20 +80,10 @@ jobs:
with:
name: storybook-twenty-front-component-renderer
path: packages/twenty-front-component-renderer/storybook-static
- name: Resolve Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
cd packages/twenty-front-component-renderer
npx playwright install chromium
npx playwright install
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-front-component-renderer/storybook-static --port 6008 --silent &
+1 -11
View File
@@ -96,20 +96,10 @@ jobs:
with:
name: storybook-static
path: packages/twenty-front/storybook-static
- name: Resolve Playwright version
id: playwright-version
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: v4-playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
cd packages/twenty-front
npx playwright install chromium
npx playwright install
- name: Front / Write .env
run: npx nx reset:env twenty-front
- name: Serve storybook & run tests
+63
View File
@@ -0,0 +1,63 @@
name: "Release: on merge"
permissions:
contents: write
on:
pull_request:
types:
- closed
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
jobs:
tag_and_release:
timeout-minutes: 10
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
steps:
- name: Check PR Author
id: check_author
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
set -euo pipefail
if [[ "$PR_AUTHOR" != "github-actions[bot]" ]]; then
echo "PR author ($PR_AUTHOR) is not trusted. Exiting."
exit 1
fi
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: main
- name: Get version from PR title
id: extract_version
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
VERSION=$(printf '%s' "$PR_TITLE" | sed -n 's/.*Release v\([0-9][0-9.]*\).*/\1/p')
if [ -z "$VERSION" ]; then
echo "No valid version found in PR title. Exiting."
exit 1
fi
printf 'VERSION=%s\n' "$VERSION" >> "$GITHUB_ENV"
- name: Push new tag
run: |
set -euo pipefail
git config --global user.name 'Github Action Deploy'
git config --global user.email '[email protected]'
git tag "v${{ env.VERSION }}"
git push origin "v${{ env.VERSION }}"
- uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5
if: contains(github.event.pull_request.labels.*.name, 'create_release')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: v${{ env.VERSION }}
+10 -12
View File
@@ -27,12 +27,11 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# Pull base images through Google's Docker Hub mirror — avoids Docker Hub
# rate limits and needs no credentials (this repo is public).
- name: Configure Docker Hub mirror
run: |
echo '{"registry-mirrors":["https://mirror.gcr.io"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Run compose
run: |
echo "Patching docker-compose.yml..."
@@ -103,12 +102,11 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
# Pull base images through Google's Docker Hub mirror — avoids Docker Hub
# rate limits and needs no credentials (this repo is public).
- name: Configure Docker Hub mirror
run: |
echo '{"registry-mirrors":["https://mirror.gcr.io"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Create frontend placeholder
run: |
mkdir -p packages/twenty-front/build
+16 -18
View File
@@ -3,8 +3,6 @@ name: CI UI
on:
pull_request:
merge_group:
push:
branches: [main]
permissions:
contents: read
@@ -15,7 +13,7 @@ concurrency:
jobs:
changed-files-check:
if: github.event_name == 'pull_request'
if: github.event_name != 'merge_group'
uses: ./.github/workflows/changed-files.yaml
with:
files: |
@@ -42,10 +40,7 @@ jobs:
run: npx nx ${{ matrix.task }} twenty-ui
ui-sb-build:
needs: changed-files-check
if: >-
always() &&
(github.event_name == 'push' ||
needs.changed-files-check.outputs.any_changed == 'true')
if: needs.changed-files-check.outputs.any_changed == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
@@ -67,7 +62,8 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
needs: ui-sb-build
if: always() && needs.ui-sb-build.result == 'success'
env:
STORYBOOK_URL: http://localhost:6007
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@@ -86,20 +82,22 @@ jobs:
run: |
cd packages/twenty-ui
npx playwright install
- name: Run storybook tests
run: npx nx storybook:test twenty-ui
- name: Upload screenshots for visual regression
if: always() && !cancelled()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: argos-screenshots-twenty-ui
path: packages/twenty-ui/screenshots
retention-days: 1
- name: Serve storybook & run tests
run: |
npx http-server packages/twenty-ui/storybook-static --port 6007 --silent &
timeout 30 bash -c 'until curl -sf http://localhost:6007 > /dev/null 2>&1; do sleep 1; done'
npx nx storybook:test twenty-ui
ci-ui-status-check:
if: always() && !cancelled()
timeout-minutes: 5
runs-on: ubuntu-latest
needs: [changed-files-check, ui-task, ui-sb-build, ui-sb-test]
needs:
[
changed-files-check,
ui-task,
ui-sb-build,
ui-sb-test,
]
steps:
- name: Fail job if any needs failed
if: contains(needs.*.result, 'failure')
+19 -4
View File
@@ -1,6 +1,7 @@
name: 'Preview Environment Dispatch'
permissions: {}
permissions:
contents: write
on:
pull_request_target:
@@ -10,6 +11,7 @@ on:
- packages/twenty-server/**
- packages/twenty-front/**
- .github/workflows/preview-env-dispatch.yaml
- .github/workflows/preview-env-keepalive.yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -33,15 +35,28 @@ jobs:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Dispatch preview-env to ci-privileged
- name: Trigger preview environment workflow
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
gh api repos/"$REPOSITORY"/dispatches \
-f event_type=preview-environment \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
-f "client_payload[repo_full_name]=$REPOSITORY"
- name: Dispatch to ci-privileged for PR comment
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
KEEPALIVE_DISPATCH_TIME: ${{ github.event.pull_request.updated_at }}
REPOSITORY: ${{ github.repository }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=preview-env-url \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[keepalive_dispatch_time]=$KEEPALIVE_DISPATCH_TIME" \
-f "client_payload[repo]=$REPOSITORY"
@@ -0,0 +1,186 @@
name: 'Preview Environment Keep Alive'
permissions:
contents: read
on:
repository_dispatch:
types: [preview-environment]
jobs:
preview-environment:
timeout-minutes: 310
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.event.client_payload.pr_head_sha }}
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Run compose setup
run: |
echo "Patching docker-compose.yml..."
# change image to localbuild using yq
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.server.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
yq eval '.services.worker.build.target = "twenty"' -i packages/twenty-docker/docker-compose.yml
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
echo "Setting up .env file..."
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
echo "Generating secrets..."
echo "" >> packages/twenty-docker/.env
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
echo "APP_SECRET=$(openssl rand -base64 32)" >> packages/twenty-docker/.env
echo "PG_DATABASE_PASSWORD=$(openssl rand -hex 16)" >> packages/twenty-docker/.env
echo "SIGN_IN_PREFILLED=true" >> packages/twenty-docker/.env
echo "Docker compose build..."
cd packages/twenty-docker/
docker compose build
working-directory: ./
- name: Create Tunnel
id: expose-tunnel
env:
CLOUDFLARED_VERSION: '2026.3.0'
run: |
set -euo pipefail
# Install cloudflared (pinned for reproducibility)
sudo curl -fsSL -o /usr/local/bin/cloudflared \
"https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64"
sudo chmod +x /usr/local/bin/cloudflared
cloudflared --version
# Start an account-less "quick tunnel" pointing at the server container.
# Cloudflare prints the assigned https://*.trycloudflare.com URL into the log.
log_file="$RUNNER_TEMP/cloudflared.log"
: > "$log_file"
cloudflared tunnel \
--url http://localhost:3000 \
--no-autoupdate \
--logfile "$log_file" \
--loglevel info \
> "$RUNNER_TEMP/cloudflared.stdout" 2>&1 &
pid=$!
echo "$pid" > "$RUNNER_TEMP/cloudflared.pid"
echo "cloudflared PID: $pid"
# Wait up to 2 minutes for the URL to appear; fail fast if cloudflared exits.
url=''
for _ in $(seq 1 60); do
url=$(grep -oE 'https://[a-zA-Z0-9-]+\.trycloudflare\.com' "$log_file" 2>/dev/null | head -n1 || true)
[ -n "$url" ] && break
if ! kill -0 "$pid" 2>/dev/null; then
echo "cloudflared exited before producing a URL"
cat "$log_file" || true
exit 1
fi
sleep 2
done
if [ -z "$url" ]; then
echo "Timed out waiting for tunnel URL"
cat "$log_file" || true
exit 1
fi
echo "Tunnel URL: $url"
echo "tunnel-url=$url" >> "$GITHUB_OUTPUT"
- name: Start services with correct SERVER_URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
cd packages/twenty-docker/
echo "Setting SERVER_URL to $TUNNEL_URL"
sed -i '/SERVER_URL=/d' .env
echo "" >> .env
echo "SERVER_URL=$TUNNEL_URL" >> .env
# Start the services
echo "Docker compose up..."
docker compose up -d || {
echo "Docker compose failed to start"
docker compose logs
exit 1
}
echo "Waiting for services to be ready..."
count=0
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
sleep 5
count=$((count+1))
if [ $count -gt 60 ]; then
echo "Timeout waiting for services to be ready"
docker compose logs
exit 1
fi
echo "Still waiting for services... ($count/60)"
done
echo "All services are up and running!"
working-directory: ./
- name: Seed Dev Workspace
run: |
cd packages/twenty-docker/
echo "Seeding light dev workspace (Apple only)..."
if ! docker compose exec -T server yarn command:prod workspace:seed:dev --light; then
echo "❌ Seeding light dev workspace failed. Dumping server logs..."
docker compose logs server
exit 1
fi
working-directory: ./
- name: Output tunnel URL
env:
TUNNEL_URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}
run: |
echo "✅ Preview Environment Ready!"
echo "🔗 Preview URL: $TUNNEL_URL"
echo "⏱️ This environment will be available for 5 hours"
echo "## 🚀 Preview Environment Ready!" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Preview URL: $TUNNEL_URL" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "This environment will automatically shut down after 5 hours." >> "$GITHUB_STEP_SUMMARY"
echo "$TUNNEL_URL" > tunnel-url.txt
- name: Upload tunnel URL artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: tunnel-url
path: tunnel-url.txt
retention-days: 1
- name: Keep tunnel alive for 5 hours
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
- name: Cleanup
if: always()
run: |
if [ -f "$RUNNER_TEMP/cloudflared.pid" ]; then
kill "$(cat "$RUNNER_TEMP/cloudflared.pid")" 2>/dev/null || true
fi
cd packages/twenty-docker/
docker compose down -v
working-directory: ./
@@ -1,33 +1,52 @@
name: Visual Regression Dispatch
# Dispatches visual regression processing to ci-privileged after CI completes.
# Runs in the context of the base repo (not the fork) so it has access to secrets,
# making it work for external contributor PRs.
# Uses workflow_run to dispatch visual regression to ci-privileged.
# This runs in the context of the base repo (not the fork), so it has
# access to secrets — making it work for external contributor PRs.
on:
workflow_run:
workflows: ['CI UI']
workflows: ['CI Front', 'CI UI']
types: [completed]
permissions:
actions: read
contents: read
pull-requests: read
jobs:
dispatch-pr:
dispatch:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check if screenshots artifact exists
- name: Determine project and artifact name
id: project
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const workflowName = context.payload.workflow_run.name;
if (workflowName === 'CI Front') {
core.setOutput('project', 'twenty-front');
core.setOutput('artifact_name', 'storybook-static');
core.setOutput('tarball_name', 'storybook-twenty-front-tarball');
core.setOutput('tarball_file', 'storybook-twenty-front.tar.gz');
} else if (workflowName === 'CI UI') {
core.setOutput('project', 'twenty-ui');
core.setOutput('artifact_name', 'storybook-twenty-ui');
core.setOutput('tarball_name', 'storybook-twenty-ui-tarball');
core.setOutput('tarball_file', 'storybook-twenty-ui.tar.gz');
} else {
core.setFailed(`Unexpected workflow: ${workflowName}`);
}
- name: Check if storybook artifact exists
id: check-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const artifactName = 'argos-screenshots-twenty-ui';
const artifactName = '${{ steps.project.outputs.artifact_name }}';
const runId = context.payload.workflow_run.id;
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
@@ -40,7 +59,7 @@ jobs:
core.setOutput('exists', found ? 'true' : 'false');
if (!found) {
core.info(`Artifact "${artifactName}" not found in run ${runId} — skipping`);
core.info(`Artifact "${artifactName}" not found in run ${runId} — storybook build was likely skipped`);
}
- name: Get PR number
@@ -52,6 +71,8 @@ jobs:
const headBranch = context.payload.workflow_run.head_branch;
const headRepo = context.payload.workflow_run.head_repository;
// workflow_run.pull_requests is empty for fork PRs,
// so fall back to searching by head label (owner:branch)
let pullRequests = context.payload.workflow_run.pull_requests;
let prNumber;
@@ -59,7 +80,7 @@ jobs:
prNumber = pullRequests[0].number;
} else {
const headLabel = `${headRepo.owner.login}:${headBranch}`;
core.info(`Searching for PR by head label: ${headLabel}`);
core.info(`pull_requests is empty (likely a fork PR), searching by head label: ${headLabel}`);
const { data: prs } = await github.rest.pulls.list({
owner: context.repo.owner,
@@ -75,7 +96,7 @@ jobs:
}
if (!prNumber) {
core.info('No pull request found — skipping');
core.info('No pull request found for this workflow run — skipping');
core.setOutput('has_pr', 'false');
return;
}
@@ -84,100 +105,43 @@ jobs:
core.setOutput('has_pr', 'true');
core.info(`PR #${prNumber}`);
- name: Compute merge-base for Argos reference
- name: Download storybook artifact from triggering run
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
id: merge-base
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
script: |
const headSha = context.payload.workflow_run.head_sha;
name: ${{ steps.project.outputs.artifact_name }}
path: storybook-static
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
try {
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: `main...${headSha}`,
});
- name: Package storybook
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
run: tar -czf /tmp/${{ steps.project.outputs.tarball_file }} -C storybook-static .
if (comparison.merge_base_commit?.sha) {
core.setOutput('sha', comparison.merge_base_commit.sha);
core.info(`Merge base: ${comparison.merge_base_commit.sha}`);
} else {
core.info('Could not determine merge base — will skip reference_commit');
core.setOutput('sha', '');
}
} catch (error) {
core.warning(`Failed to compute merge base: ${error instanceof Error ? error.message : String(error)}`);
core.setOutput('sha', '');
}
- name: Upload storybook tarball
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ steps.project.outputs.tarball_name }}
path: /tmp/${{ steps.project.outputs.tarball_file }}
retention-days: 1
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
REFERENCE_COMMIT: ${{ steps.merge-base.outputs.sha }}
run: |
ARGS=(
--method POST
-f event_type=visual-regression
-f "client_payload[pr_number]=$PR_NUMBER"
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
-f "client_payload[repo]=$REPOSITORY"
-f "client_payload[branch]=$BRANCH"
-f "client_payload[commit]=$COMMIT"
)
if [ -n "$REFERENCE_COMMIT" ]; then
ARGS+=(-f "client_payload[reference_commit]=$REFERENCE_COMMIT")
fi
gh api repos/twentyhq/ci-privileged/dispatches "${ARGS[@]}"
dispatch-main:
if: >-
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check if screenshots artifact exists
id: check-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const runId = context.payload.workflow_run.id;
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId,
});
const found = artifacts.artifacts.some(a => a.name === 'argos-screenshots-twenty-ui');
core.setOutput('exists', found ? 'true' : 'false');
if (!found) {
core.info(`Artifact not found in run ${runId} — skipping`);
}
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true'
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
RUN_ID: ${{ github.run_id }}
REPOSITORY: ${{ github.repository }}
PROJECT: ${{ steps.project.outputs.project }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
--method POST \
-f event_type=visual-regression \
-f "client_payload[run_id]=$WORKFLOW_RUN_ID" \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[run_id]=$RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[project]=$PROJECT" \
-f "client_payload[branch]=$BRANCH" \
-f "client_payload[commit]=$COMMIT"
@@ -1,69 +0,0 @@
name: 'Website Preview Dispatch'
permissions:
contents: read
on:
pull_request:
types: [opened, synchronize, reopened, closed, labeled]
paths:
- packages/twenty-website/**
- .github/workflows/website-preview-dispatch.yaml
concurrency:
# Keyed on PR number so independent PRs don't cancel each other. `github.ref`
# would resolve to the base branch under pull_request and collide.
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
trigger-build:
# Same fork PRs from outside the org don't have `secrets.*` so the dispatch
# call would fail anyway — skip explicitly to avoid noise.
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.action != 'closed' && (
(github.event.action == 'labeled' && github.event.label.name == 'preview-website') ||
(
(
github.event.pull_request.author_association == 'MEMBER' ||
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'COLLABORATOR'
) && contains(fromJSON('["opened","synchronize","reopened"]'), github.event.action)
)
)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch website-preview-build to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=website-preview-build \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[pr_head_sha]=$PR_HEAD_SHA" \
-f "client_payload[pr_head_ref]=$PR_HEAD_REF"
trigger-cleanup:
# Covers both merge and close-without-merge — pull_request `closed` fires
# for both. PRs left open forever are covered by OpenNext's
# `maxVersionAgeDays: 14` + `maxNumberOfVersions: 50` auto-pruning in
# open-next.config.ts, so nothing leaks even if cleanup never runs.
if: |
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.action == 'closed'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Dispatch website-preview-cleanup to ci-privileged
env:
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api repos/twentyhq/ci-privileged/dispatches \
-f event_type=website-preview-cleanup \
-f "client_payload[pr_number]=$PR_NUMBER"
-2
View File
@@ -51,9 +51,7 @@ dump.rdb
mcp.json
/.junie/
/.agents/plugins/marketplace.json
TRANSLATION_QA_REPORT.md
.playwright-mcp/
.playwright-cli/
output/playwright/
screenshots/
-27
View File
@@ -1,27 +0,0 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf",
"printWidth": 80,
"sortPackageJson": false,
"ignorePatterns": [
"**/dist/**",
"**/build/**",
"**/lib/**",
"**/.next/**",
"**/coverage/**",
"**/generated/**",
"**/generated-admin/**",
"**/generated-metadata/**",
"**/.cache/**",
"**/node_modules/**",
"**/*.min.js",
"**/*.snap",
"**/*.md",
"**/*.mdx",
"**/seed-project/**/*.mjs",
"packages/twenty-zapier/build/**",
"**/upgrade-version-command/**"
]
}
-4
View File
@@ -10,8 +10,4 @@ nodeLinker: node-modules
npmMinimalAgeGate: 3d
npmPreapprovedPackages:
- twenty-sdk
- twenty-client-sdk
yarnPath: .yarn/releases/yarn-4.13.0.cjs
-159
View File
@@ -1,159 +0,0 @@
# Twenty Website — DESIGN.md
> Visual system for the Twenty marketing site. Distilled from `packages/twenty-website/src/theme/`. Loaded by every `impeccable` invocation alongside PRODUCT.md.
## Theme
**Light by default.** A founder browsing a partner profile in daylight on a 1427 inch monitor is the default scene. The site does ship a `data-scheme="dark"` override (see `css-variables.ts`), but no current public page opts into it. Treat dark as a deferred surface.
## Color
Palette is OKLCH-equivalent neutrals at the surface level. The brand accents (blue, pink, yellow, green) are present in the token system but used sparingly — none of them appear on the partner pages.
### Strategy: Restrained
Tinted neutrals + one accent ≤10%. The accent for partner pages is the deep ink black (`var(--color-black-100)`) used in CTAs and hover states. Anything beyond a hairline border, an icon glyph, or a primary CTA should question whether it needs color at all.
### Tokens (from `src/theme/colors.ts` + `css-variables.ts`)
Neutrals (the workhorses):
| Token | Hex (computed) | Role |
| --- | --- | --- |
| `colors.primary.background[100]` | `#ffffff` | Page + card surface |
| `colors.primary.text[100]` | `#1c1c1c` | Headlines, primary text |
| `colors.primary.text[80]` | `#1c1c1ccc` | Body text |
| `colors.primary.text[60]` | `#1c1c1c99` | Eyebrows, meta, captions |
| `colors.primary.text[40]` | `#1c1c1c66` | Disabled / placeholder |
| `colors.primary.text[20]` | `#1c1c1c33` | Subtle separators |
| `colors.primary.text[10]` | `#1c1c1c1a` | Hairline borders |
| `colors.primary.text[5]` | `#1c1c1c0d` | Subtle fills (rates panel, skill chips) |
| `colors.primary.border[10]` | `#1c1c1c1a` | Default border |
| `colors.primary.border[20]` | `#1c1c1c33` | Hover border |
Reverse palette (for dark CTAs):
| Token | Role |
| --- | --- |
| `colors.secondary.background[100]` | Filled CTA background (deep ink) |
| `colors.secondary.text[100]` | Filled CTA text (white) |
Brand accents (currently absent from partner pages; available if needed):
- `colors.accent.blue``#4a38f5` / `#8174f8`
- `colors.accent.pink``#ed87fc` / `#f3abfd`
- `colors.accent.yellow``#feffb7` / `#feffd9`
- `colors.accent.green``#89fc9a` / `#b0fdbe`
- `colors.highlight` — same hue as blue accent
**Do not introduce gradients, glass blurs, or saturated fills on partner pages.** Color is conviction here, not decoration.
## Typography
Three families, each load-balanced via CSS variables:
| Family | Var | Use |
| --- | --- | --- |
| `theme.font.family.serif` | `--font-serif` | Headlines, partner names, headline values |
| `theme.font.family.sans` | `--font-sans` | Body, prose, interactive labels |
| `theme.font.family.mono` | `--font-mono` | Eyebrows, meta, currency labels, tabular numerics |
| `theme.font.family.retro` | `--font-retro` | Reserved (not used on partner pages) |
### Weight + Size Contrast
Weights: `light: 300`, `regular: 400`, `medium: 500`. No bold. Hierarchy is driven by scale and family contrast, never by weight alone.
Scale (`theme.font.size(n)``calc(var(--font-base) * n)`, where `--font-base: 0.25rem` ≈ 4px):
- Display / h1: size 912 (3648px)
- h2 / section heads: size 78 (2832px)
- h3 / card heads: size 56 (2024px)
- Body / prose: size 45 (1620px)
- Eyebrow / meta: size 3 (12px) with `letter-spacing: 0.060.08em` and `text-transform: uppercase`
Body line length: cap at 6575ch (the existing `PartnerProfileIntro` uses `max-width: 62ch` — keep that order of magnitude).
### Hierarchy contract
- A serif `<h1>` at size 9 light reads as a partner's name on the detail page.
- A mono eyebrow above or below it locates the partner (region · city · country).
- A serif size 6 light reads as a section head.
- Body prose is sans regular.
- Currency values are serif (they read as headline numbers, not stats).
- Currency labels and meta are mono.
## Spacing & Layout
Base unit `4px`. Spacing helper `theme.spacing(n)` returns `n * 4px`. Common rhythms on the partner pages:
- Inter-section gap on the detail page: `theme.spacing(1014)` — generous, editorial breathing room.
- Inter-element gap inside a section: `theme.spacing(35)`.
- Card padding: `theme.spacing(6)`.
- Page horizontal padding: `theme.spacing(4)` mobile, `theme.spacing(10)` ≥ md breakpoint.
### Radius
`theme.radius(n)` returns `n * 2px`. The default card radius is `theme.radius(2)` = 4px. Pills use `999px`. No softer rounding than that.
### Borders
Borders are hairline (`1px solid theme.colors.primary.border[10]`). They define edges quietly. On hover they step to `border[20]`. Never use a chunky border as decoration.
## Components
### Card (PartnerCard, RatesPanel)
White surface, hairline border, 4px radius, 24px padding, soft shadow on hover only:
```css
background-color: ${theme.colors.primary.background[100]};
border: 1px solid ${theme.colors.primary.border[10]};
border-radius: ${theme.radius(2)};
padding: ${theme.spacing(6)};
&:hover {
border-color: ${theme.colors.primary.border[20]};
box-shadow: 0 12px 32px -16px rgba(0, 0, 0, 0.18);
transform: translateY(-2px);
}
```
### Chip / Pill
Rounded `999px`, 1px border, subtle background fill (`primary.text[5]` for filter pills, transparent for chip rows), `text[80]` color, mono or sans font.
### Button / LinkButton
Lives in `@/design-system/components`. Two color modes: `primary` (deep ink fill, white text) and `secondary` (transparent fill, ink text + 1px border). `variant="contained"` is what partner pages use.
### Avatar
`PartnerAvatar` is a deterministic generated mark from name + slug. Used as fallback when `profilePictureUrl` is missing. The real photo overlays it at 120px circle on the detail page, 56px on the list card.
## Motion
- Hover transitions: 250ms, ease-out (cubic-bezier curve in `PartnerCard`: `0.25s ease`).
- Card entrance: 700ms cubic-bezier `0.22, 1, 0.36, 1` (ease-out-quart), 90ms stagger per index.
- All motion respects `@media (prefers-reduced-motion: reduce)` — animations stop, hover translate disabled.
- **No bounce, no elastic, no parallax.** Editorial restraint.
## Iconography
`@tabler/icons-react`, 1416px on body-level chips, 1824px on buttons. Always `aria-hidden="true"` when decorative. Stroke width `2` (default).
## Accessibility Defaults
- Focus ring: `outline: 2px solid theme.colors.primary.text[100]; outline-offset: 4px` (already used on the card link).
- Touch target ≥ 40×40px on mobile.
- `aria-label` on icon-only buttons, `aria-labelledby` on sectioned regions.
- All `<a target="_blank">` includes `rel="noopener noreferrer"`.
- Color is never the sole carrier of meaning. The money pills carry both an icon and a text label.
## Anti-patterns (project-specific)
In addition to the impeccable shared absolute bans:
- **Do not use the brand accent colors (blue/pink/yellow/green) on partner pages** unless we have a stronger reason than "to add color".
- **No skeuomorphic shadows on cards.** The hover shadow is `0 12px 32px -16px rgba(0,0,0,0.18)` — that's the ceiling.
- **No gradients on anything.** Including text, borders, and backgrounds.
- **No floating "Trusted by" logo bars** on partner pages.
-80
View File
@@ -1,80 +0,0 @@
# Twenty Website — Product & Brand Context
> Strategic context for design work on the Twenty marketing site (`packages/twenty-website`). Loaded by every `impeccable` invocation.
## Register
**Brand.** The marketing site is a public-facing surface where the design itself is part of the credibility argument. Prospects evaluate Twenty partly by how the site feels. The product app (`packages/twenty-front`) is a separate product-register surface, governed elsewhere.
## Users & Purpose
The primary audience varies by route, but the working assumption for partner-related pages is:
- **Who:** A budget-holding decision maker (founder, RevOps lead, or COO) shopping for a CRM implementation partner. Already on Twenty's site, evaluating a shortlist of partners.
- **Context:** Doing a side-by-side comparison across 25 candidates over a single browsing session. Will spend 3090 seconds on each profile before deciding whether to book a call.
- **Decision being made:** "Is this partner credible, the right size, the right specialty, and within budget? Do I trust them enough to commit 30 minutes to a discovery call?"
What the partner pages must do, in priority order:
1. Communicate credibility (real firm, real person, real work).
2. Surface fit signals fast (skills, region, languages, deployment expertise, budget range).
3. Give the visitor a confident "next step" affordance (book a call or vet via LinkedIn) without pressure.
## Desired Outcome
The redesign should make `/partners/profile/[slug]` feel like a *thoughtfully curated profile of a top-tier partner*, not a generic templated card. A visitor should leave thinking "this firm is serious" even if they don't book a call this session.
Specifically:
- **Confidence over information density.** A short, well-typeset profile beats a packed-but-busy one.
- **Editorial restraint.** White space, deliberate type hierarchy, and a few well-chosen details say more than dozens of small components.
- **Quiet conviction.** No hype copy, no growth-hack patterns, no "Trusted by" logo strips. The partner's own work and intro speak for themselves.
## Brand Personality
**Editorial · Founder-led · Considered.**
The site reads like a thoughtful indie publication, not a SaaS landing page. Serif headlines, plenty of whitespace, deliberate typographic rhythm. Quietly opinionated — Twenty has a point of view about CRM (open-source, customizable, well-designed) and the site reflects that without shouting.
Tonal anchors:
- Stripe's documentation for clarity, Linear's marketing for restraint, an editorial print magazine for typography choices.
## Anti-references
**Reject these patterns. They make the work read as generic AI / generic SaaS:**
- **Generic SaaS landing.** Big-number heroes, identical icon-grid cards, gradient text, navy + lime accent color schemes, "supercharge your workflow" language.
- **Corporate enterprise tone.** Stock photos of diverse handshakes. "Trusted by Fortune 500" logo strips as the primary credibility move. Trust-badge bars.
- **Bento templates.** Repetitive same-size cards. Vercel-style scroll-pin animations on every section.
- **Side-stripe borders, gradient text, glassmorphism, hero-metric templates, identical card grids** — see impeccable's shared absolute bans.
## Strategic Design Principles
1. **Typography carries the design.** The brand has a serif/sans/mono trio. Hierarchy is set by scale + weight contrast, not by color or borders.
2. **Restrained palette.** Tinted neutrals (black/white via CSS variables, with alpha-tone variants for text and borders) carry 90%+ of the surface. Accent color used sparingly when it appears at all.
3. **Whitespace is a feature.** Tight cards feel cheap. Pages should breathe.
4. **Asymmetry over grid.** A 12-col bento is the wrong shape for a profile page. Use asymmetric two-column layouts where one column does heavy lifting.
5. **One opinionated detail per page.** Each surface should have one moment of editorial conviction (a typographic flourish, a precise micro-interaction, a deliberate space) rather than five generic flourishes.
## Accessibility
**WCAG AA + keyboard + screen reader baseline:**
- All interactive elements reachable by keyboard, focus visible (`outline: 2px solid`, not just color shift).
- Semantic landmarks: `<header>`, `<main>`, `<nav>`, `<section aria-labelledby=…>`, headings in order.
- All images with informational content have alt text. Decorative icons have `aria-hidden="true"`.
- Body text ≥ 4.5:1 contrast; large text (≥18pt or 14pt bold) ≥ 3:1.
- Respect `prefers-reduced-motion`. Animations stop, don't slow.
- Forms have explicit labels. Errors are announced.
## Tech & Constraints
- Next.js 16 app router (Server Components by default, `'use client'` for interactivity).
- Linaria styled-components (`@linaria/react`) for zero-runtime CSS-in-JS.
- Lingui (`@lingui/react`) for i18n; never hardcode user-visible strings.
- Theme tokens in `packages/twenty-website/src/theme/`. Colors are CSS variables resolved to OKLCH-tinted neutrals.
- `@tabler/icons-react` for iconography (no Heroicons, no custom SVGs unless purposeful).
- `@radix-ui/react-*` for primitives (Popover etc) where headless behavior is needed.
## Out of Scope for This File
- Detailed visual tokens (colors, type scale, motion specs) live in `DESIGN.md`.
- Per-page IA decisions live in shape briefs (`docs/superpowers/specs/`).
+7 -2
View File
@@ -63,7 +63,7 @@ export default defineObject({
Then ship it to your workspace:
```bash
npx twenty app:publish --private
npx twenty deploy
```
See the [app development guide](https://docs.twenty.com/developers/extend/apps/getting-started) for objects, views, agents, and logic functions.
@@ -147,9 +147,13 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
- <a href="https://nestjs.com/"><img src="./packages/twenty-website/public/images/readme/stack-nestjs.svg" width="14" height="14"/> NestJS</a>, with <a href="https://bullmq.io/">BullMQ</a>, <a href="https://www.postgresql.org/"><img src="./packages/twenty-website/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
- <a href="https://reactjs.org/"><img src="./packages/twenty-website/public/images/readme/stack-react.svg" width="14" height="14"/> React</a>, with <a href="https://jotai.org/">Jotai</a>, <a href="https://linaria.dev/">Linaria</a> and <a href="https://lingui.dev/">Lingui</a>
# Thanks
<p align="center">
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.webp" height="28" alt="Chromatic" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.webp" height="28" alt="Greptile" /></a>
&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.webp" height="28" alt="Sentry" /></a>
@@ -157,7 +161,8 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.webp" height="28" alt="Crowdin" /></a>
</p>
Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
# Join the Community
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

+32 -9
View File
@@ -44,12 +44,12 @@
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "npx oxlint -c .oxlintrc.json . && (npx oxfmt --check . || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
"command": "npx oxlint -c .oxlintrc.json . && (prettier . --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
},
"configurations": {
"ci": {},
"fix": {
"command": "npx oxlint --fix -c .oxlintrc.json . && npx oxfmt ."
"command": "npx oxlint --fix -c .oxlintrc.json . && prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
}
},
"dependsOn": ["^build", "twenty-oxlint-rules:build"]
@@ -59,12 +59,12 @@
"cache": false,
"dependsOn": ["twenty-oxlint-rules:build"],
"options": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (npx oxfmt --check $FILES || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint -c {projectRoot}/.oxlintrc.json $FILES && (prettier --check $FILES || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint:diff-with-main --configuration=fix' && false)))",
"pattern": "\\.(ts|tsx|js|jsx)$"
},
"configurations": {
"fix": {
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && npx oxfmt $FILES)"
"command": "FILES=$(git diff --name-only --diff-filter=d main -- {projectRoot}/ | grep -E '{args.pattern}'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --fix -c {projectRoot}/.oxlintrc.json $FILES && prettier --write $FILES)"
}
}
},
@@ -73,14 +73,18 @@
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "npx oxfmt --check {args.files} {args.write}",
"files": ".",
"write": ""
"command": "prettier {args.files} --check --cache {args.cache} --cache-location {args.cacheLocation} --write {args.write} --cache-strategy {args.cacheStrategy}",
"cache": true,
"cacheLocation": "../../.cache/prettier/{projectRoot}",
"cacheStrategy": "metadata",
"write": false
},
"configurations": {
"ci": {},
"ci": {
"cacheStrategy": "content"
},
"fix": {
"command": "npx oxfmt {args.files}"
"write": true
}
},
"dependsOn": ["^build"]
@@ -220,6 +224,25 @@
}
}
},
"chromatic": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"commands": [
{
"command": "nx storybook:build {projectName}",
"forwardAllArgs": false
},
"chromatic --storybook-build-dir=storybook-static {args.ci}"
],
"parallel": false
},
"configurations": {
"ci": {
"ci": "--exit-zero-on-changes"
}
}
},
"@nx/jest:jest": {
"cache": true,
"inputs": [
+1 -4
View File
@@ -13,7 +13,6 @@
"concurrently": "^8.2.2",
"http-server": "^14.1.1",
"nx": "22.5.4",
"oxfmt": "0.50.0",
"tsx": "^4.17.0",
"verdaccio": "^6.3.1"
},
@@ -28,7 +27,7 @@
"resolutions": {
"graphql": "16.8.1",
"type-fest": "4.10.1",
"typescript": "5.9.3",
"typescript": "5.9.2",
"nodemailer": "8.0.4",
"graphql-redis-subscriptions/ioredis": "^5.6.0",
"@lingui/core": "5.1.2",
@@ -52,7 +51,6 @@
"packages/twenty-server",
"packages/twenty-emails",
"packages/twenty-ui",
"packages/twenty-new-ui",
"packages/twenty-utils",
"packages/twenty-zapier",
"packages/twenty-website",
@@ -64,7 +62,6 @@
"packages/twenty-client-sdk",
"packages/twenty-cli",
"packages/create-twenty-app",
"packages/twenty-codex-plugin",
"packages/twenty-oxlint-rules",
"packages/twenty-companion",
"packages/twenty-claude-skills"
+3 -3
View File
@@ -35,7 +35,7 @@ The scaffolder will:
| `--name <name>` | Set the app name |
| `--display-name <displayName>` | Set the display name |
| `--description <description>` | Set the description |
| `--url <url>` | Twenty workspace URL (default: `http://localhost:2020`) |
| `--workspace-url <url>` | Twenty workspace URL (default: `http://localhost:2020`) |
| `--authentication-method <method>` | `oauth` or `apiKey` (default: `apiKey` for local, `oauth` for remote) |
## Documentation
@@ -48,8 +48,8 @@ Full documentation is available at **[docs.twenty.com/developers/extend/apps](ht
## Troubleshooting
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty docker:logs`.
- Auth not working: run `yarn twenty remote:add --local` to re-authenticate.
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
- Auth not working: run `yarn twenty remote add --local` to re-authenticate.
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
## Contributing
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "2.10.0",
"version": "2.5.1",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -50,7 +50,7 @@
"jest": "29.7.0",
"jest-environment-node": "^29.4.1",
"twenty-shared": "workspace:*",
"typescript": "^5.9.3",
"typescript": "^5.9.2",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^4.2.1"
+10 -7
View File
@@ -18,8 +18,11 @@ const program = new Command(packageJson.name)
.option('-n, --name <name>', 'Application name')
.option('-d, --display-name <displayName>', 'Application display name')
.option('--description <description>', 'Application description')
.option('--url <url>', 'Twenty server URL (default: http://localhost:2020)')
.option('--api-url <apiUrl>', '[deprecated: use --url]')
.option(
'--workspace-url <workspaceUrl>',
'Twenty workspace URL (default: http://localhost:2020)',
)
.option('--api-url <apiUrl>', '[deprecated: use --workspace-url]')
.option(
'--authentication-method <method>',
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)',
@@ -32,7 +35,7 @@ const program = new Command(packageJson.name)
name?: string;
displayName?: string;
description?: string;
url?: string;
workspaceUrl?: string;
apiUrl?: string;
authenticationMethod?: AuthenticationMethod;
},
@@ -65,18 +68,18 @@ const program = new Command(packageJson.name)
if (options?.apiUrl) {
console.warn(
chalk.yellow('Warning: --api-url is deprecated. Use --url instead.'),
chalk.yellow(
'Warning: --api-url is deprecated. Use --workspace-url instead.',
),
);
}
const serverUrl = (options?.url ?? options?.apiUrl)?.replace(/\/+$/, '');
await new CreateAppCommand().execute({
directory,
name: options?.name,
displayName: options?.displayName,
description: options?.description,
serverUrl,
workspaceUrl: options?.workspaceUrl ?? options?.apiUrl,
authenticationMethod: options?.authenticationMethod,
});
},
@@ -49,19 +49,19 @@
## Best practice
It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options:
It's highly recommended to create new app entities using `yarn twenty add`. These are the options:
| Entity type | Command | Generated file |
| -------------------- | ---------------------------------------- | ------------------------------------- |
| Object | `yarn twenty dev:add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty dev:add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty dev:add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty dev:add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty dev:add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty dev:add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/<name>.ts` |
| Entity type | Command | Generated file |
| -------------------- | ------------------------------------ | ------------------------------------- |
| Object | `yarn twenty add object` | `src/objects/<name>.ts` |
| Field | `yarn twenty add field` | `src/fields/<name>.ts` |
| Logic function | `yarn twenty add logicFunction` | `src/logic-functions/<name>.ts` |
| Front component | `yarn twenty add frontComponent` | `src/front-components/<name>.tsx` |
| Role | `yarn twenty add role` | `src/roles/<name>.ts` |
| Skill | `yarn twenty add skill` | `src/skills/<name>.ts` |
| Agent | `yarn twenty add agent` | `src/agents/<name>.ts` |
| View | `yarn twenty add view` | `src/views/<name>.ts` |
| Navigation menu item | `yarn twenty add navigationMenuItem` | `src/navigation-menu-items/<name>.ts` |
| Page layout | `yarn twenty add pageLayout` | `src/page-layouts/<name>.ts` |
This helps automatically generate required IDs etc.
@@ -11,8 +11,8 @@ Run `yarn twenty help` to list all available commands.
## Useful Commands
- `yarn twenty dev` - Start the development server and sync your app
- `yarn twenty docker:status` - Check the local Twenty server status
- `yarn twenty docker:start` - Start the local Twenty server
- `yarn twenty server status` - Check the local Twenty server status
- `yarn twenty server start` - Start the local Twenty server
- `yarn test` - Run integration tests
## Learn More
@@ -17,7 +17,8 @@
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "TO-BE-GENERATED"
"twenty-client-sdk": "TO-BE-GENERATED",
"twenty-sdk": "TO-BE-GENERATED"
},
"devDependencies": {
"@types/node": "^24.7.2",
@@ -25,7 +26,6 @@
"oxlint": "^0.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"twenty-sdk": "TO-BE-GENERATED",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
@@ -1,11 +1,4 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
Avatar,
IconBox,
IconHierarchy,
IconLayout,
IconSettingsAutomation,
} from 'twenty-sdk/ui';
import { useState } from 'react';
import {
@@ -37,7 +30,7 @@ const CATEGORIES = [
href: `${DOCS_BASE_URL}/logic/logic-functions`,
},
{
label: 'LOGIC FUNCTION',
label: 'SERVERLESS FUNCT.',
href: `${DOCS_BASE_URL}/logic/logic-functions`,
},
{
@@ -66,6 +59,15 @@ const CATEGORIES = [
},
] as const;
const ItemIcon = ({ color }: { color: string }) => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="2" y="2" width="5" height="5" rx="1" fill={color} />
<rect x="9" y="2" width="5" height="5" rx="1" fill={color} opacity="0.6" />
<rect x="2" y="9" width="5" height="5" rx="1" fill={color} opacity="0.6" />
<rect x="9" y="9" width="5" height="5" rx="1" fill={color} opacity="0.3" />
</svg>
);
const ArrowUpRight = ({ color = '#999' }: { color?: string }) => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path
@@ -91,18 +93,6 @@ const CategoryCard = ({
}) => {
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
const CategoryIcon = () => {
if (title === 'Data model') {
return <IconHierarchy color={color} size={'20px'} />;
}
if (title === 'Logic') {
return <IconSettingsAutomation color={color} size={'20px'} />;
}
if (title === 'Layout') {
return <IconLayout color={color} size={'20px'} />;
}
};
return (
<div
style={{
@@ -121,12 +111,8 @@ const CategoryCard = ({
style={{
padding: '16px 20px',
background: `${color}22`,
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<CategoryIcon />
<span
style={{
fontSize: '16px',
@@ -168,7 +154,7 @@ const CategoryCard = ({
transition: 'background 0.15s',
}}
>
<IconBox color={color} size={'20px'} />
<ItemIcon color={color} />
<span
style={{
fontSize: '13px',
@@ -204,11 +190,13 @@ const MainPage = () => {
padding: '40px',
}}
>
<Avatar
{/*<Avatar
avatarUrl={getPublicAssetUrl('logo.svg')}
placeholder={APP_DISPLAY_NAME}
placeholderColorSeed={APP_DISPLAY_NAME}
type="squared"
size="xl"
/>
/>*/}
<span
style={{
fontSize: '24px',
@@ -232,7 +220,7 @@ const MainPage = () => {
You can now add content to your app.
</span>
<a
href="/settings/applications#installed"
href="/settings/applications"
style={{
display: 'inline-flex',
alignItems: 'center',
@@ -1 +0,0 @@
nodeLinker: node-modules
@@ -17,7 +17,7 @@ import {
DEV_API_URL,
serverStart,
} from 'twenty-sdk/cli';
import { isDefined, normalizeUrl } from 'twenty-shared/utils';
import { isDefined } from 'twenty-shared/utils';
import {
getDockerInstallInstructions,
isDockerInstalled,
@@ -33,7 +33,7 @@ type CreateAppOptions = {
name?: string;
displayName?: string;
description?: string;
serverUrl?: string;
workspaceUrl?: string;
authenticationMethod?: AuthenticationMethod;
};
@@ -45,9 +45,9 @@ export class CreateAppCommand {
const { appName, appDisplayName, appDirectory, appDescription } =
this.getAppInfos(options);
const serverUrl = options.serverUrl ?? DEV_API_URL;
const workspaceUrl = options.workspaceUrl ?? DEV_API_URL;
const skipLocalInstance = serverUrl !== DEV_API_URL;
const skipLocalInstance = workspaceUrl !== DEV_API_URL;
if (!skipLocalInstance && !isDockerInstalled()) {
console.log(chalk.yellow('\n' + getDockerInstallInstructions() + '\n'));
@@ -118,7 +118,7 @@ export class CreateAppCommand {
console.log('');
let authSucceeded = false;
let resolvedServerUrl = serverUrl;
let resolvedWorkspaceUrl = workspaceUrl;
let serverReady = skipLocalInstance;
if (!skipLocalInstance) {
@@ -126,7 +126,7 @@ export class CreateAppCommand {
const serverResult = await this.ensureDockerServer(dockerPullPromise);
if (isDefined(serverResult.url)) {
resolvedServerUrl = serverResult.url;
resolvedWorkspaceUrl = serverResult.url;
serverReady = true;
}
}
@@ -134,16 +134,18 @@ export class CreateAppCommand {
if (serverReady) {
this.logNextStep('Authenticating');
authSucceeded = await this.tryExistingAuth(resolvedServerUrl);
authSucceeded = await this.tryExistingAuth(resolvedWorkspaceUrl);
if (authSucceeded) {
this.logDetail('Reusing existing credentials');
} else if (authenticationMethod === 'oauth') {
this.logDetail('Starting OAuth flow');
authSucceeded = await this.authenticateWithOAuth(resolvedServerUrl);
authSucceeded =
await this.authenticateWithOAuth(resolvedWorkspaceUrl);
} else {
this.logDetail('Using development API key');
authSucceeded = await this.authenticateWithDevKey(resolvedServerUrl);
authSucceeded =
await this.authenticateWithDevKey(resolvedWorkspaceUrl);
}
}
@@ -163,10 +165,10 @@ export class CreateAppCommand {
}
if (syncSucceeded) {
await this.openMainPage(appDirectory, resolvedServerUrl);
await this.openMainPage(appDirectory, resolvedWorkspaceUrl);
}
this.logSuccess(appDirectory, resolvedServerUrl, authSucceeded);
this.logSuccess(appDirectory, resolvedWorkspaceUrl, authSucceeded);
} catch (error) {
console.error(
chalk.red('\nCreate application failed:'),
@@ -313,7 +315,7 @@ export class CreateAppCommand {
private async openMainPage(
appDirectory: string,
serverUrl: string,
workspaceUrl: string,
): Promise<void> {
try {
const configService = new ConfigService();
@@ -326,7 +328,7 @@ export class CreateAppCommand {
const [universalIdentifier, frontUrl] = await Promise.all([
this.readMainPageLayoutUniversalIdentifier(appDirectory),
this.resolveWorkspaceFrontUrl(serverUrl, token),
this.resolveWorkspaceFrontUrl(workspaceUrl, token),
]);
if (!universalIdentifier || !frontUrl) {
@@ -334,7 +336,7 @@ export class CreateAppCommand {
}
const pageLayoutId = await this.resolvePageLayoutId(
serverUrl,
workspaceUrl,
universalIdentifier,
token,
);
@@ -353,12 +355,12 @@ export class CreateAppCommand {
}
private async resolveWorkspaceFrontUrl(
serverUrl: string,
workspaceUrl: string,
token: string,
): Promise<string | null> {
const query = `{ currentWorkspace { workspaceUrls { subdomainUrl customUrl } } }`;
const response = await fetch(`${serverUrl}/metadata`, {
const response = await fetch(`${workspaceUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -387,7 +389,7 @@ export class CreateAppCommand {
const frontUrl = urls.customUrl ?? urls.subdomainUrl;
return frontUrl ? normalizeUrl(frontUrl) : null;
return frontUrl?.replace(/\/+$/, '') ?? null;
}
private async readMainPageLayoutUniversalIdentifier(
@@ -408,13 +410,13 @@ export class CreateAppCommand {
}
private async resolvePageLayoutId(
serverUrl: string,
workspaceUrl: string,
universalIdentifier: string,
token: string,
): Promise<string | null> {
const query = `{ getPageLayouts { id universalIdentifier } }`;
const response = await fetch(`${serverUrl}/metadata`, {
const response = await fetch(`${workspaceUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -501,7 +503,7 @@ export class CreateAppCommand {
});
}
private async tryExistingAuth(serverUrl: string): Promise<boolean> {
private async tryExistingAuth(workspaceUrl: string): Promise<boolean> {
try {
const configService = new ConfigService();
const remoteNames = await configService.getRemotes();
@@ -509,7 +511,7 @@ export class CreateAppCommand {
for (const remoteName of remoteNames) {
const remoteConfig = await configService.getConfigForRemote(remoteName);
if (remoteConfig.apiUrl !== serverUrl) {
if (remoteConfig.apiUrl !== workspaceUrl) {
continue;
}
@@ -519,7 +521,7 @@ export class CreateAppCommand {
continue;
}
const response = await fetch(`${serverUrl}/metadata`, {
const response = await fetch(`${workspaceUrl}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -553,11 +555,11 @@ export class CreateAppCommand {
}
}
private async authenticateWithDevKey(serverUrl: string): Promise<boolean> {
private async authenticateWithDevKey(workspaceUrl: string): Promise<boolean> {
try {
const result = await authLogin({
apiKey: DEV_API_KEY,
apiUrl: serverUrl,
apiUrl: workspaceUrl,
remote: 'local',
});
@@ -572,7 +574,7 @@ export class CreateAppCommand {
console.log(
chalk.yellow(
' Authentication failed. Run `yarn twenty remote:add --local` manually.',
' Authentication failed. Run `yarn twenty remote add --local` manually.',
),
);
@@ -580,7 +582,7 @@ export class CreateAppCommand {
} catch {
console.log(
chalk.yellow(
' Authentication failed. Run `yarn twenty remote:add --local` manually.',
' Authentication failed. Run `yarn twenty remote add --local` manually.',
),
);
@@ -596,21 +598,21 @@ export class CreateAppCommand {
}
}
private async authenticateWithOAuth(serverUrl: string): Promise<boolean> {
private async authenticateWithOAuth(workspaceUrl: string): Promise<boolean> {
try {
const remoteName = this.deriveRemoteName(serverUrl);
const remoteName = this.deriveRemoteName(workspaceUrl);
ConfigService.setActiveRemote(remoteName);
this.logDetail('Opening browser for OAuth...');
const result = await authLoginOAuth({ apiUrl: serverUrl });
const result = await authLoginOAuth({ apiUrl: workspaceUrl });
if (result.success) {
const configService = new ConfigService();
await configService.setDefaultRemote(remoteName);
this.logDetail(`Authenticated via OAuth to ${serverUrl}`);
this.logDetail(`Authenticated via OAuth to ${workspaceUrl}`);
return true;
}
@@ -618,7 +620,7 @@ export class CreateAppCommand {
console.log(
chalk.yellow(
` OAuth failed: ${result.error.message}\n` +
` Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
` Run \`yarn twenty remote add --api-url ${workspaceUrl}\` manually.`,
),
);
@@ -626,7 +628,7 @@ export class CreateAppCommand {
} catch {
console.log(
chalk.yellow(
` Authentication failed. Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
` Authentication failed. Run \`yarn twenty remote add --api-url ${workspaceUrl}\` manually.`,
),
);
@@ -636,7 +638,7 @@ export class CreateAppCommand {
private logSuccess(
appDirectory: string,
serverUrl: string,
workspaceUrl: string,
authSucceeded: boolean,
): void {
const dirName = basename(appDirectory);
@@ -654,7 +656,9 @@ export class CreateAppCommand {
if (!authSucceeded) {
console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`));
console.log(
chalk.cyan(' yarn twenty remote:add --url <your-instance-url>\n'),
chalk.cyan(
' yarn twenty remote add --api-url <your-instance-url>\n',
),
);
stepNumber++;
}
@@ -664,7 +668,7 @@ export class CreateAppCommand {
stepNumber++;
console.log(chalk.white(` ${stepNumber}. Open your twenty instance`));
console.log(chalk.cyan(` ${serverUrl}\n`));
console.log(chalk.cyan(` ${workspaceUrl}\n`));
console.log(
chalk.gray(
@@ -17,7 +17,6 @@ const UNIVERSAL_IDENTIFIERS_PATH = join(
'constants',
'universal-identifiers.ts',
);
const YARNRC_PATH = 'yarnrc.yml';
// Template content matching template/src/constants/universal-identifiers.ts
const TEMPLATE_UNIVERSAL_IDENTIFIERS = `export const APP_DISPLAY_NAME = 'DISPLAY-NAME-TO-BE-GENERATED';
@@ -33,10 +32,8 @@ const TEMPLATE_PACKAGE_JSON = {
license: 'MIT',
scripts: { twenty: 'twenty' },
dependencies: {
'twenty-client-sdk': '0.0.0',
},
devDependencies: {
'twenty-sdk': '0.0.0',
'twenty-client-sdk': '0.0.0',
},
};
@@ -149,7 +146,7 @@ describe('copyBaseApplicationProject', () => {
join(testAppDirectory, 'package.json'),
);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.devDependencies['twenty-sdk']).toBe(
expect(packageJson.dependencies['twenty-sdk']).toBe(
createTwentyAppPackageJson.version,
);
expect(packageJson.dependencies['twenty-client-sdk']).toBe(
@@ -176,27 +173,6 @@ describe('copyBaseApplicationProject', () => {
expect(publicDirectoryContents).toHaveLength(0);
});
it('should rename yarnrc.yml to .yarnrc.yml in the scaffolded project', async () => {
await fs.writeFile(
join(testAppDirectory, YARNRC_PATH),
'nodeLinker: node-modules',
);
await copyBaseApplicationProject({
appName: 'my-test-app',
appDisplayName: 'My Test App',
appDescription: 'A test application',
appDirectory: testAppDirectory,
});
expect(await fs.pathExists(join(testAppDirectory, YARNRC_PATH))).toBe(
false,
);
expect(await fs.pathExists(join(testAppDirectory, '.yarnrc.yml'))).toBe(
true,
);
});
it('should handle empty description', async () => {
await copyBaseApplicationProject({
appName: 'my-test-app',
@@ -22,7 +22,7 @@ export const copyBaseApplicationProject = async ({
onProgress?.('Copying base template');
await fs.copy(join(__dirname, './constants/template'), appDirectory);
onProgress?.('Configuring dotfiles (.gitignore, .github, .yarnrc.yml)');
onProgress?.('Configuring dotfiles (.gitignore, .github)');
await renameDotfiles({ appDirectory });
onProgress?.('Mirroring AGENTS.md to CLAUDE.md');
@@ -47,7 +47,6 @@ const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
const renames = [
{ from: 'gitignore', to: '.gitignore' },
{ from: 'github', to: '.github' },
{ from: 'yarnrc.yml', to: '.yarnrc.yml' },
];
for (const { from, to } of renames) {
@@ -120,8 +119,7 @@ const updatePackageJson = async ({
const packageJson = await fs.readJson(join(appDirectory, 'package.json'));
packageJson.name = appName;
packageJson.devDependencies['twenty-sdk'] =
createTwentyAppPackageJson.version;
packageJson.dependencies['twenty-sdk'] = createTwentyAppPackageJson.version;
packageJson.dependencies['twenty-client-sdk'] =
createTwentyAppPackageJson.version;
@@ -28,6 +28,6 @@ export const getDockerInstallInstructions = (): string => {
' Then run this command again.',
'',
' Alternatively, connect to an existing Twenty instance:',
' npx create-twenty-app@latest my-twenty-app --url <your-instance-url>',
' npx create-twenty-app@latest my-twenty-app --workspace-url <your-instance-url>',
].join('\n');
};
@@ -93,7 +93,7 @@ yarn install
# Register your local Twenty server as a remote (interactive prompt).
# When asked for the URL use http://localhost:2021 and paste an API key
# from Settings -> Developers in the Twenty UI.
yarn twenty remote:add
yarn twenty remote add
# Build, install, and watch for changes.
yarn twenty dev
@@ -107,8 +107,8 @@ watching `src/`. Edit any file and the change is re-synced within seconds.
```bash
cd packages/twenty-apps/community/github-connector
yarn install
yarn twenty remote:add # same prompts as above
yarn twenty app:install # builds and installs once
yarn twenty remote add # same prompts as above
yarn twenty install # builds and installs once
```
## Configure authentication
@@ -1,11 +0,0 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '7d3f2b9e-5c0a-4e8b-ad6e-2f9c3b4d5e6a',
frontComponentUniversalIdentifier: '6c2f1a8d-4b9e-4f7a-9c5d-1e8b2a3d4c5f',
label: 'Contributor Stats',
icon: 'IconChartBar',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
});
@@ -1,11 +0,0 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '4640992f-c2c9-4bba-b5df-9c8f05dc9e80',
frontComponentUniversalIdentifier: '08f40f82-24ed-4f3e-8c99-695151e90e38',
label: 'Fetch Contributors',
icon: 'IconUsers',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
});
@@ -1,6 +1,10 @@
import { useEffect, useMemo, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar, useRecordId } from 'twenty-sdk/front-component';
import {
enqueueSnackbar,
objectMetadataItem,
useRecordId,
} from 'twenty-sdk/front-component';
import { THEME } from 'src/modules/github/contributor/components/theme';
import { callAppRoute } from 'src/modules/shared/call-app-route';
@@ -678,4 +682,12 @@ export default defineFrontComponent({
description:
'Displays time-bucketed charts of PRs authored (merged only), merged and reviewed by a contributor over the last week, month, 3 months or year.',
component: ContributorStats,
command: {
universalIdentifier: '7d3f2b9e-5c0a-4e8b-ad6e-2f9c3b4d5e6a',
label: 'Contributor Stats',
icon: 'IconChartBar',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
},
});
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -94,4 +95,12 @@ export default defineFrontComponent({
'Fetches contributors from every configured GitHub repo (GITHUB_REPOS) and upserts them as Contributor records.',
isHeadless: true,
component: FetchContributors,
command: {
universalIdentifier: '4640992f-c2c9-4bba-b5df-9c8f05dc9e80',
label: 'Fetch Contributors',
icon: 'IconUsers',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'contributor',
},
});
@@ -1,11 +0,0 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: 'c34f56aa-ff65-43a4-9db2-774945dbcc53',
frontComponentUniversalIdentifier: '9430e4fc-9ecb-428d-9bde-2babeb1f452f',
label: 'Fetch Issues',
icon: 'IconBug',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'issue',
});
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -97,4 +98,12 @@ export default defineFrontComponent({
description: 'Fetches issues from GitHub repos',
isHeadless: true,
component: FetchIssues,
command: {
universalIdentifier: 'c34f56aa-ff65-43a4-9db2-774945dbcc53',
label: 'Fetch Issues',
icon: 'IconBug',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'issue',
},
});
@@ -1,11 +0,0 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '719cfe1c-d570-4c8c-89e6-88671c6ba1ea',
frontComponentUniversalIdentifier: '7c397b0c-8b19-4fac-924a-8f6aa1dece78',
label: 'Fetch Project Items',
icon: 'IconLayoutKanban',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'projectItem',
});
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -96,4 +97,12 @@ export default defineFrontComponent({
description: 'Fetches project items from GitHub Projects V2',
isHeadless: true,
component: FetchProjectItems,
command: {
universalIdentifier: '719cfe1c-d570-4c8c-89e6-88671c6ba1ea',
label: 'Fetch Project Items',
icon: 'IconLayoutKanban',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'projectItem',
},
});
@@ -1,11 +0,0 @@
import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define';
export default defineCommandMenuItem({
universalIdentifier: '0b24e6d6-da0c-4c0e-8d88-5bfd7f3cd75a',
frontComponentUniversalIdentifier: '5e2ba8df-1db5-4964-94d3-44cccfd791a0',
label: 'Fetch Pull Requests',
icon: 'IconGitPullRequest',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'pullRequest',
});
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
enqueueSnackbar,
objectMetadataItem,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
@@ -100,4 +101,12 @@ export default defineFrontComponent({
description: 'Fetches pull requests and reviews from GitHub repos',
isHeadless: true,
component: FetchPrs,
command: {
universalIdentifier: '0b24e6d6-da0c-4c0e-8d88-5bfd7f3cd75a',
label: 'Fetch Pull Requests',
icon: 'IconGitPullRequest',
isPinned: false,
conditionalAvailabilityExpression:
objectMetadataItem.nameSingular === 'pullRequest',
},
});
@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty remote:add --api-url http://localhost:2020 --as local
yarn twenty remote add --api-url http://localhost:2020 --as local
```
Then, start development mode to sync your app and watch for changes:
@@ -22,18 +22,18 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & Authentication
yarn twenty remote:add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote:status # Check auth status
yarn twenty remote:use # Set default remote
yarn twenty remote:list # List all configured remotes
yarn twenty remote:remove <name> # Remove a remote
yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
# Application
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty dev:add # Scaffold a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty dev:function:logs # Stream function logs
yarn twenty dev:function:exec # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
```
## Integration Tests
@@ -13,7 +13,7 @@ beforeAll(async () => {
if (!apiUrl || !token) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
@@ -14,7 +14,7 @@ function validateEnv(): { apiUrl: string; apiKey: string } {
if (!apiUrl || !apiKey) {
throw new Error(
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
'Start a local server: yarn twenty docker:start\n' +
'Start a local server: yarn twenty server start\n' +
'Or set them in vitest env config.',
);
}
@@ -80,8 +80,8 @@ export async function teardown() {
const uninstallResult = await appUninstall({ appPath: APP_PATH });
if (!uninstallResult.success) {
throw new Error(
`App uninstall failed during teardown: ${JSON.stringify(uninstallResult.error, null, 2)}`,
console.warn(
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
);
}
}
@@ -1,4 +1,4 @@
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';
import { PermissionFlag, defineRole } from 'twenty-sdk/define';
import {
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
POST_CARD_UNIVERSAL_IDENTIFIER,
@@ -36,5 +36,5 @@ export default defineRole({
canUpdateFieldValue: true,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
@@ -10,8 +10,8 @@
"packageManager": "[email protected]",
"scripts": {
"dev": "twenty dev",
"exec": "twenty dev:fn-exec",
"uninstall": "twenty app:uninstall",
"exec": "twenty exec",
"uninstall": "twenty uninstall",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
},
@@ -1,17 +0,0 @@
import { defineIndex } from 'twenty-sdk/define';
import {
POST_CARD_UNIVERSAL_IDENTIFIER,
STATUS_FIELD_UNIVERSAL_IDENTIFIER,
} from '../objects/post-card.object';
export default defineIndex({
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff0',
objectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
fields: [
{
universalIdentifier: 'b6e9d2a1-5a4c-46ca-9d52-42c8f02d1ff1',
fieldUniversalIdentifier: STATUS_FIELD_UNIVERSAL_IDENTIFIER,
},
],
});
@@ -3,7 +3,7 @@ import { NavigationMenuItemType } from 'twenty-shared/types';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineNavigationMenuItem({
universalIdentifier: 'e8031eca-d6ea-4a4b-b828-38227dba896a',
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
position: 0,
type: NavigationMenuItemType.OBJECT,
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
@@ -1,4 +1,4 @@
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';
import { PermissionFlag, defineRole } from 'twenty-sdk/define';
import {
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
POST_CARD_UNIVERSAL_IDENTIFIER,
@@ -36,5 +36,5 @@ export default defineRole({
canUpdateFieldValue: false,
},
],
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
permissionFlags: [PermissionFlag.APPLICATIONS],
});
@@ -1,10 +0,0 @@
import { defineViewField } from 'twenty-sdk/define';
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
export default defineViewField({
fieldMetadataUniversalIdentifier: '7b57bd63-5a4c-46ca-9d52-42c8f02d1df6',
position: 5,
universalIdentifier: 'cd582d11-ea21-4dc3-b9c1-0298ce3b6b54',
viewUniversalIdentifier: ALL_POST_CARDS_VIEW_ID,
isVisible: true,
});
@@ -5,7 +5,7 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
First, authenticate to your workspace:
```bash
yarn twenty remote:add --api-url http://localhost:2020 --as local
yarn twenty remote add --api-url http://localhost:2020 --as local
```
Then, start development mode to sync your app and watch for changes:
@@ -22,18 +22,18 @@ Run `yarn twenty help` to list all available commands. Common commands:
```bash
# Remotes & Authentication
yarn twenty remote:add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote:status # Check auth status
yarn twenty remote:use # Set default remote
yarn twenty remote:list # List all configured remotes
yarn twenty remote:remove <name> # Remove a remote
yarn twenty remote add --api-url http://localhost:2020 --as local # Authenticate with Twenty
yarn twenty remote status # Check auth status
yarn twenty remote switch # Switch default remote
yarn twenty remote list # List all configured remotes
yarn twenty remote remove <name> # Remove a remote
# Application
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty dev:add # Scaffold a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty dev:function:logs # Stream function logs
yarn twenty dev:function:exec # Execute a function with JSON payload
yarn twenty app:uninstall # Uninstall app from workspace
yarn twenty dev # Start dev mode (watch, build, sync, and auto-generate typed client)
yarn twenty add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
yarn twenty logs # Stream function logs
yarn twenty exec # Execute a function with JSON payload
yarn twenty uninstall # Uninstall app from workspace
```
## LLMs instructions
@@ -43,7 +43,7 @@ A brief note on the overall tone of the conversation (collaborative, tense, expl
- If the transcript is very short (under 200 words), provide a brief 2-3 sentence summary instead of the full structure.
## Saving the Summary
After generating the summary, use \`update_one_call_recording\` to save it in the \`summary\` field with the format:
After generating the summary, use \`update_callRecording\` to save it in the \`summary\` field with the format:
\`\`\`json
{ "summary": { "blocknote": null, "markdown": "<your summary>" } }
\`\`\`
@@ -1,32 +0,0 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn
# codegen
generated
# dev
/dist/
.twenty
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files
.env*
# typescript
*.tsbuildinfo
*.d.ts
@@ -1,70 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": [
"warn",
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"typescript/no-explicit-any": "off"
},
"overrides": [
{
"files": ["**/*.logic-function.ts", "**/logic-functions/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["twenty-shared", "twenty-shared/*"],
"message": "Logic functions must not import from twenty-shared directly. Import runtime types and helpers from `twenty-sdk/logic-function` instead so the logic-function bundle stays minimal."
}
]
}
]
}
}
]
}
@@ -1,114 +0,0 @@
# People Data Labs enrichment app
Enriches **Person** and **Company** records with [People Data Labs](https://www.peopledatalabs.com/) (PDL) data.
> **Status: data-model scaffold.** This package defines the fields, relation, indexes,
> views, role, and manifest. The enrichment **logic function (the "mapper") is not yet
> implemented** — see [What the mapper must do](#what-the-mapper-must-do).
---
## Data-model decisions
### Bundle scope
Only the core PDL company fields are defined. Premium / Comprehensive / specialized fields
(`inferred_revenue`, `linkedin_follower_count`, employee growth/churn/tenure, parent /
subsidiary, exec movement, top employers, `funding_details`, …) are **out of scope** for this app.
### Enums → SELECT / MULTI_SELECT
Every PDL enum that has a canonical file is a SELECT, **validated 0-missing/0-extra against
PDL schema v34.1**:
| Field | Type | Options |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------ |
| `pdlSeniority` (`job_title_levels`, array) | MULTI_SELECT | 10 |
| `pdlFundingStages` (`funding_stages`, array) | MULTI_SELECT | 29 |
| `pdlIndustry` / `pdlJobCompanyIndustry` (`industry`) | SELECT | 147 |
| `pdlJobTitleSubRole` (`job_title_sub_role`) | SELECT | 106 |
| `pdlJobTitleClass`, `pdlInferredSalary`, `pdlSex`, `pdlCompanyType`, `pdlSizeRange`, `pdlJobCompanySize`, `pdlLatestFundingStage`, `pdlLocationContinent`, `pdlLocationMetro`, `pdlMicExchange` | SELECT | 5 / 11 / 2 / 6 / 8 / 8 / 29 / 7 / 384 / 70 |
- Option `value`s are normalized to **GraphQL enum names** (`united states``UNITED_STATES`):
uppercase, accents stripped, non-alphanumeric → `_`, digit-leading prefixed.
- Option `universalIdentifier`s are **unique per field** (shared enums like industry get a
separate id-set per field).
- **Stays `TEXT`** (no canonical PDL enum file exists): `pdlIndustryDetail` (`industry_v2`),
`pdlJobOnetCode`, `pdlLocationRegion`.
### Standard-field mapping
`pdl*` shadows are **removed** where an equivalent standard field exists; the mapper writes
the standard field instead:
| Object | Removed shadow → standard target |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Person | `pdlLinkedinUrl``linkedinLink`, `pdlJobTitle``jobTitle`, `pdlFullName``name`, `pdlWorkEmail`/`pdlPersonalEmails``emails`, `pdlMobilePhone`/`pdlPhoneNumbers``phones` |
| Company | `pdlLinkedinUrl``linkedinLink`, `pdlWebsite``domainName`, `pdlDisplayName``name` |
Shadows are **kept** where no reliable standard field is available: `pdlEmployeeCount`,
`pdlTwitterUrl`.
_Trade-off:_ PDL's work/personal-email and mobile/other-phone distinction is dropped (folded
into the standard bags).
### Location → ADDRESS composite
- **Company** location → the **standard `address`** composite (street/city/state/postcode/country/geo).
- **Person** has no standard address field → dedicated **`pdlLocation` (ADDRESS)**.
- `pdlLocationMetro` (both) and `pdlLocationContinent` (company) stay SELECT — ADDRESS has no slot.
_Trade-off:_ ADDRESS `country` is free text, so the country SELECT was dropped.
### Relation
Dedicated **`pdlCurrentCompany`** (Person `MANY_TO_ONE` → Company) ↔ inverse
**`pdlCurrentEmployees`** (Company `ONE_TO_MANY` → Person). Deliberately **not** the standard
`company` relation, so PDL's detected employer can't overwrite the user's CRM account link.
### Enrichment metadata
- `pdlId` — PDL record id (re-enrich by id: more precise than by email).
- `pdlLikelihood` (Person, NUMBER) — PDL match confidence 110.
- `pdlEnrichmentStatus` (SELECT: `MATCHED` / `NOT_FOUND` / `ERROR`) — distinguishes
"no match" from "never tried" (drives re-enrichment scheduling).
- `pdlLastEnrichedAt` (DATE_TIME), `pdlRawPayload` (RAW_JSON, full response).
### Other
- `pdlTotalFunding` is `CURRENCY` (mapper must convert the bare USD float → micros).
- **Indexes**: `pdlId` and `pdlLastEnrichedAt` on both objects.
- **Views**: a curated "People Data Labs" TABLE view per object.
- **Role**: read/update on Person & Company (object-level; tighten to field-scoped later).
---
## What the mapper must do
The logic function (to be built) must:
**Orchestration**
1. Trigger via manual command-menu action / record create / batch (TBD).
2. Call PDL Person and/or Company Enrichment with `PDL_API_KEY`; pass `min_likelihood` /
`required_fields` to control match quality.
3. On `200` → write fields + set `pdlEnrichmentStatus = MATCHED`; on `404`
`NOT_FOUND`; on error → `ERROR`.
4. Respect PDL rate limits (queue / throttle on `429`).
5. **TTL guard**: skip re-enrichment if `pdlLastEnrichedAt` is recent; prefer re-enriching by `pdlId`.
**Field writing**
6. Write **standard fields** (fill-only-if-empty to avoid overwriting user data):
Person `name`, `emails`, `phones`, `linkedinLink`, `jobTitle`; Company `name`,
`domainName`, `linkedinLink`, `address`.
7. Write `pdl*` fields for everything else.
8. **SELECT guard**: only write a SELECT/MULTI_SELECT value if the normalized value exists in
the field's option set; otherwise skip and keep it in `pdlRawPayload` (handles PDL schema
versions newer than v34.1). Use the same normalization as the option `value`s.
9. **MULTI_SELECT** arrays: `job_title_levels``pdlSeniority`; `funding_stages``pdlFundingStages`.
10. **CURRENCY**: `total_funding_raised` (USD float) → `{ amountMicros: value × 1_000_000, currencyCode: 'USD' }`.
11. **ADDRESS**: split PDL `location.*` into the composite — Company → standard `address`,
Person → `pdlLocation`.
12. **Relation**: resolve `job_company_id` → find/upsert a Company record → link `pdlCurrentCompany`.
13. **Dates**: handle partial PDL dates (`YYYY`, `YYYY-MM`) for `job_start_date`,
`last_funding_date`, `birth_date`.
14. Always set `pdlId`, `pdlLastEnrichedAt`, `pdlRawPayload`, `pdlLikelihood` (person).
@@ -1,36 +0,0 @@
{
"name": "people-data-labs",
"version": "0.1.0",
"description": "People Data Labs enrichment app for Twenty",
"license": "MIT",
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": ">=4.0.2"
},
"keywords": [
"twenty-app"
],
"packageManager": "[email protected]",
"scripts": {
"twenty": "twenty",
"lint": "oxlint -c .oxlintrc.json .",
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest"
},
"dependencies": {
"twenty-client-sdk": "2.4.0",
"twenty-sdk": "2.4.0"
},
"devDependencies": {
"@types/node": "^24.7.2",
"@types/react": "^19.0.0",
"oxlint": "^0.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^3.1.1"
}
}
@@ -1,16 +0,0 @@
import { defineApplication } from 'twenty-sdk/define';
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
displayName: 'People Data Labs',
description: 'Enrich People and Companies with People Data Labs data.',
serverVariables: {
PDL_API_KEY: {
description: 'People Data Labs API key',
isSecret: true,
isRequired: true,
},
},
});
@@ -1,19 +0,0 @@
import { type SelectOptionMeta } from 'src/types/select-option-meta.type';
export const ENRICHMENT_STATUS_OPTIONS: readonly SelectOptionMeta[] = [
{
key: 'matched',
value: 'MATCHED',
label: 'Matched',
color: 'green',
position: 0,
},
{
key: 'notFound',
value: 'NOT_FOUND',
label: 'No Match',
color: 'gray',
position: 1,
},
{ key: 'error', value: 'ERROR', label: 'Error', color: 'red', position: 2 },
];
@@ -1,195 +0,0 @@
import { type SelectOptionMeta } from 'src/types/select-option-meta.type';
export const FUNDING_STAGE_OPTIONS: readonly SelectOptionMeta[] = [
{
key: 'angel',
value: 'ANGEL',
label: 'Angel',
color: 'blue',
position: 0,
},
{
key: 'convertibleNote',
value: 'CONVERTIBLE_NOTE',
label: 'Convertible Note',
color: 'red',
position: 1,
},
{
key: 'corporateRound',
value: 'CORPORATE_ROUND',
label: 'Corporate Round',
color: 'green',
position: 2,
},
{
key: 'debtFinancing',
value: 'DEBT_FINANCING',
label: 'Debt Financing',
color: 'orange',
position: 3,
},
{
key: 'equityCrowdfunding',
value: 'EQUITY_CROWDFUNDING',
label: 'Equity Crowdfunding',
color: 'purple',
position: 4,
},
{
key: 'fundingRound',
value: 'FUNDING_ROUND',
label: 'Funding Round',
color: 'yellow',
position: 5,
},
{ key: 'grant', value: 'GRANT', label: 'Grant', color: 'pink', position: 6 },
{
key: 'initialCoinOffering',
value: 'INITIAL_COIN_OFFERING',
label: 'Initial Coin Offering',
color: 'cyan',
position: 7,
},
{
key: 'nonEquityAssistance',
value: 'NON_EQUITY_ASSISTANCE',
label: 'Non-Equity Assistance',
color: 'brown',
position: 8,
},
{
key: 'postIpoDebt',
value: 'POST_IPO_DEBT',
label: 'Post-IPO Debt',
color: 'lime',
position: 9,
},
{
key: 'postIpoEquity',
value: 'POST_IPO_EQUITY',
label: 'Post-IPO Equity',
color: 'violet',
position: 10,
},
{
key: 'postIpoSecondary',
value: 'POST_IPO_SECONDARY',
label: 'Post-IPO Secondary',
color: 'gold',
position: 11,
},
{
key: 'preSeed',
value: 'PRE_SEED',
label: 'Pre-Seed',
color: 'turquoise',
position: 12,
},
{
key: 'privateEquity',
value: 'PRIVATE_EQUITY',
label: 'Private Equity',
color: 'crimson',
position: 13,
},
{
key: 'productCrowdfunding',
value: 'PRODUCT_CROWDFUNDING',
label: 'Product Crowdfunding',
color: 'sky',
position: 14,
},
{
key: 'secondaryMarket',
value: 'SECONDARY_MARKET',
label: 'Secondary Market',
color: 'amber',
position: 15,
},
{ key: 'seed', value: 'SEED', label: 'Seed', color: 'plum', position: 16 },
{
key: 'seriesA',
value: 'SERIES_A',
label: 'Series A',
color: 'grass',
position: 17,
},
{
key: 'seriesB',
value: 'SERIES_B',
label: 'Series B',
color: 'tomato',
position: 18,
},
{
key: 'seriesC',
value: 'SERIES_C',
label: 'Series C',
color: 'iris',
position: 19,
},
{
key: 'seriesD',
value: 'SERIES_D',
label: 'Series D',
color: 'mint',
position: 20,
},
{
key: 'seriesE',
value: 'SERIES_E',
label: 'Series E',
color: 'ruby',
position: 21,
},
{
key: 'seriesF',
value: 'SERIES_F',
label: 'Series F',
color: 'bronze',
position: 22,
},
{
key: 'seriesG',
value: 'SERIES_G',
label: 'Series G',
color: 'jade',
position: 23,
},
{
key: 'seriesH',
value: 'SERIES_H',
label: 'Series H',
color: 'gray',
position: 24,
},
{
key: 'seriesI',
value: 'SERIES_I',
label: 'Series I',
color: 'blue',
position: 25,
},
{
key: 'seriesJ',
value: 'SERIES_J',
label: 'Series J',
color: 'red',
position: 26,
},
{
key: 'seriesUnknown',
value: 'SERIES_UNKNOWN',
label: 'Series Unknown',
color: 'green',
position: 27,
},
{
key: 'undisclosed',
value: 'UNDISCLOSED',
label: 'Undisclosed',
color: 'orange',
position: 28,
},
];
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,60 +0,0 @@
import { type SelectOptionMeta } from 'src/types/select-option-meta.type';
export const SIZE_OPTIONS: readonly SelectOptionMeta[] = [
{
key: 'oneToTen',
value: 'ONE_TO_TEN',
label: '1-10',
color: 'blue',
position: 0,
},
{
key: 'elevenToFifty',
value: 'ELEVEN_TO_FIFTY',
label: '11-50',
color: 'red',
position: 1,
},
{
key: 'fiftyOneToTwoHundred',
value: 'FIFTY_ONE_TO_TWO_HUNDRED',
label: '51-200',
color: 'green',
position: 2,
},
{
key: 'twoHundredOneToFiveHundred',
value: 'TWO_HUNDRED_ONE_TO_FIVE_HUNDRED',
label: '201-500',
color: 'orange',
position: 3,
},
{
key: 'fiveHundredOneToOneThousand',
value: 'FIVE_HUNDRED_ONE_TO_ONE_THOUSAND',
label: '501-1000',
color: 'purple',
position: 4,
},
{
key: 'oneThousandOneToFiveThousand',
value: 'ONE_THOUSAND_ONE_TO_FIVE_THOUSAND',
label: '1001-5000',
color: 'yellow',
position: 5,
},
{
key: 'fiveThousandOneToTenThousand',
value: 'FIVE_THOUSAND_ONE_TO_TEN_THOUSAND',
label: '5001-10000',
color: 'pink',
position: 6,
},
{
key: 'tenThousandOnePlus',
value: 'TEN_THOUSAND_ONE_PLUS',
label: '10001+',
color: 'cyan',
position: 7,
},
];
@@ -1,19 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlAffiliatedProfiles,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.ARRAY,
name: 'pdlAffiliatedProfiles',
label: 'Affiliated Profiles',
description: 'Affiliated company profiles returned by People Data Labs.',
isNullable: true,
});
@@ -1,19 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlAlternativeDomains,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.ARRAY,
name: 'pdlAlternativeDomains',
label: 'Alternative Domains',
description: 'Alternative domains returned by People Data Labs.',
isNullable: true,
});
@@ -1,19 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlAlternativeNames,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.ARRAY,
name: 'pdlAlternativeNames',
label: 'Alternative Names',
description: 'Alternative names returned by People Data Labs.',
isNullable: true,
});
@@ -1,65 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlCompanyType,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlCompanyType',
label: 'Company Type',
description: 'People Data Labs canonical company type.',
isNullable: true,
options: [
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.public,
value: 'PUBLIC',
label: 'Public',
color: 'blue',
position: 0,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.private,
value: 'PRIVATE',
label: 'Private',
color: 'red',
position: 1,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.publicSubsidiary,
value: 'PUBLIC_SUBSIDIARY',
label: 'Public Subsidiary',
color: 'green',
position: 2,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.educational,
value: 'EDUCATIONAL',
label: 'Educational',
color: 'orange',
position: 3,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.government,
value: 'GOVERNMENT',
label: 'Government',
color: 'purple',
position: 4,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyType.nonprofit,
value: 'NONPROFIT',
label: 'Nonprofit',
color: 'yellow',
position: 5,
},
],
});
@@ -1,28 +0,0 @@
import {
defineField,
FieldType,
RelationType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlCurrentEmployees,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RELATION,
name: 'pdlCurrentEmployees',
label: 'PDL Current Employees',
description:
'People whose current employer is this company, per People Data Labs.',
relationTargetObjectMetadataUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
relationTargetFieldMetadataUniversalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.person.pdlCurrentCompany,
universalSettings: {
relationType: RelationType.ONE_TO_MANY,
},
icon: 'IconUsers',
});
@@ -1,20 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlEmployeeCountByCountry,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlEmployeeCountByCountry',
label: 'Employee Count by Country',
description:
'Employee count broken down by country returned by People Data Labs.',
isNullable: true,
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlEmployeeCount,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.NUMBER,
name: 'pdlEmployeeCount',
label: 'Employees',
description: 'Exact employee count returned by People Data Labs.',
isNullable: true,
});
@@ -1,28 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { ENRICHMENT_STATUS_OPTIONS } from 'src/constants/enrichment-status-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlEnrichmentStatus,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlEnrichmentStatus',
label: 'Enrichment Status',
description: 'Outcome of the latest People Data Labs enrichment attempt.',
isNullable: true,
options: buildSelectOptions(
ENRICHMENT_STATUS_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyEnrichmentStatus,
),
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlFacebookUrl,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.LINKS,
name: 'pdlFacebookUrl',
label: 'Facebook',
description: 'Facebook page returned by People Data Labs.',
isNullable: true,
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlFoundedYear,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.NUMBER,
name: 'pdlFoundedYear',
label: 'Founded Year',
description: 'Founding year returned by People Data Labs.',
isNullable: true,
});
@@ -1,27 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { FUNDING_STAGE_OPTIONS } from 'src/constants/funding-stage-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlFundingStages,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.MULTI_SELECT,
name: 'pdlFundingStages',
label: 'Funding Stages',
description: 'People Data Labs canonical funding stages.',
isNullable: true,
options: buildSelectOptions(
FUNDING_STAGE_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.fundingStage,
),
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlHeadline,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlHeadline',
label: 'Headline',
description: 'People Data Labs company headline or summary.',
isNullable: true,
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlId,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlId',
label: 'PDL ID',
description: 'People Data Labs company identifier.',
isNullable: true,
});
@@ -1,19 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlIndustryDetail,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlIndustryDetail',
label: 'Industry (detailed)',
description: 'Detailed People Data Labs industry v2 value.',
isNullable: true,
});
@@ -1,27 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { INDUSTRY_OPTIONS } from 'src/constants/industry-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlIndustry,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlIndustry',
label: 'Industry',
description: 'People Data Labs canonical industry.',
isNullable: true,
options: buildSelectOptions(
INDUSTRY_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyIndustry,
),
});
@@ -1,19 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLastEnrichedAt,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.DATE_TIME,
name: 'pdlLastEnrichedAt',
label: 'Last Enriched At',
description: 'Timestamp of the latest People Data Labs enrichment.',
isNullable: true,
});
@@ -1,19 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLastFundingDate,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.DATE,
name: 'pdlLastFundingDate',
label: 'Last Funding Date',
description: 'Date of the latest funding round returned by People Data Labs.',
isNullable: true,
});
@@ -1,28 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { FUNDING_STAGE_OPTIONS } from 'src/constants/funding-stage-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLatestFundingStage,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlLatestFundingStage',
label: 'Latest Funding Stage',
description: 'People Data Labs canonical latest funding stage.',
isNullable: true,
options: buildSelectOptions(
FUNDING_STAGE_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.fundingStage,
),
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLinkedinId,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlLinkedinId',
label: 'LinkedIn ID',
description: 'LinkedIn identifier returned by People Data Labs.',
isNullable: true,
});
@@ -1,79 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLocationContinent,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlLocationContinent',
label: 'Continent',
description: 'People Data Labs canonical continent.',
isNullable: true,
options: [
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.africa,
value: 'AFRICA',
label: 'Africa',
color: 'blue',
position: 0,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.antarctica,
value: 'ANTARCTICA',
label: 'Antarctica',
color: 'red',
position: 1,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent.asia,
value: 'ASIA',
label: 'Asia',
color: 'green',
position: 2,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.europe,
value: 'EUROPE',
label: 'Europe',
color: 'orange',
position: 3,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.northAmerica,
value: 'NORTH_AMERICA',
label: 'North America',
color: 'purple',
position: 4,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.oceania,
value: 'OCEANIA',
label: 'Oceania',
color: 'yellow',
position: 5,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationContinent
.southAmerica,
value: 'SOUTH_AMERICA',
label: 'South America',
color: 'pink',
position: 6,
},
],
});
@@ -1,27 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { METRO_OPTIONS } from 'src/constants/metro-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlLocationMetro,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlLocationMetro',
label: 'Metro Area',
description: 'People Data Labs canonical metro area.',
isNullable: true,
options: buildSelectOptions(
METRO_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyLocationMetro,
),
});
@@ -1,513 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlMicExchange,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlMicExchange',
label: 'Stock Exchange (MIC)',
description: 'People Data Labs canonical market identifier code.',
isNullable: true,
options: [
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.aqse,
value: 'AQSE',
label: 'AQSE',
color: 'blue',
position: 0,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.arcx,
value: 'ARCX',
label: 'ARCX',
color: 'red',
position: 1,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.asex,
value: 'ASEX',
label: 'ASEX',
color: 'green',
position: 2,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.bcxe,
value: 'BCXE',
label: 'BCXE',
color: 'orange',
position: 3,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.bvmf,
value: 'BVMF',
label: 'BVMF',
color: 'purple',
position: 4,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.dsmd,
value: 'DSMD',
label: 'DSMD',
color: 'yellow',
position: 5,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.misx,
value: 'MISX',
label: 'MISX',
color: 'pink',
position: 6,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.neoe,
value: 'NEOE',
label: 'NEOE',
color: 'cyan',
position: 7,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xams,
value: 'XAMS',
label: 'XAMS',
color: 'brown',
position: 8,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xase,
value: 'XASE',
label: 'XASE',
color: 'lime',
position: 9,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xasx,
value: 'XASX',
label: 'XASX',
color: 'violet',
position: 10,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xber,
value: 'XBER',
label: 'XBER',
color: 'gold',
position: 11,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbkk,
value: 'XBKK',
label: 'XBKK',
color: 'turquoise',
position: 12,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbog,
value: 'XBOG',
label: 'XBOG',
color: 'crimson',
position: 13,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbom,
value: 'XBOM',
label: 'XBOM',
color: 'sky',
position: 14,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbru,
value: 'XBRU',
label: 'XBRU',
color: 'amber',
position: 15,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbsp,
value: 'XBSP',
label: 'XBSP',
color: 'plum',
position: 16,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbud,
value: 'XBUD',
label: 'XBUD',
color: 'grass',
position: 17,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xbue,
value: 'XBUE',
label: 'XBUE',
color: 'tomato',
position: 18,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcai,
value: 'XCAI',
label: 'XCAI',
color: 'iris',
position: 19,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcbo,
value: 'XCBO',
label: 'XCBO',
color: 'mint',
position: 20,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcnq,
value: 'XCNQ',
label: 'XCNQ',
color: 'ruby',
position: 21,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xcse,
value: 'XCSE',
label: 'XCSE',
color: 'bronze',
position: 22,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xdfm,
value: 'XDFM',
label: 'XDFM',
color: 'jade',
position: 23,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xdub,
value: 'XDUB',
label: 'XDUB',
color: 'gray',
position: 24,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xdus,
value: 'XDUS',
label: 'XDUS',
color: 'blue',
position: 25,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xeqy,
value: 'XEQY',
label: 'XEQY',
color: 'red',
position: 26,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xfra,
value: 'XFRA',
label: 'XFRA',
color: 'green',
position: 27,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xham,
value: 'XHAM',
label: 'XHAM',
color: 'orange',
position: 28,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xhel,
value: 'XHEL',
label: 'XHEL',
color: 'purple',
position: 29,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xhkg,
value: 'XHKG',
label: 'XHKG',
color: 'yellow',
position: 30,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xice,
value: 'XICE',
label: 'XICE',
color: 'pink',
position: 31,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xidx,
value: 'XIDX',
label: 'XIDX',
color: 'cyan',
position: 32,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xjpx,
value: 'XJPX',
label: 'XJPX',
color: 'brown',
position: 33,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xjse,
value: 'XJSE',
label: 'XJSE',
color: 'lime',
position: 34,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkls,
value: 'XKLS',
label: 'XKLS',
color: 'violet',
position: 35,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkos,
value: 'XKOS',
label: 'XKOS',
color: 'gold',
position: 36,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkrx,
value: 'XKRX',
label: 'XKRX',
color: 'turquoise',
position: 37,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xkuw,
value: 'XKUW',
label: 'XKUW',
color: 'crimson',
position: 38,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xlis,
value: 'XLIS',
label: 'XLIS',
color: 'sky',
position: 39,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xlon,
value: 'XLON',
label: 'XLON',
color: 'amber',
position: 40,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmad,
value: 'XMAD',
label: 'XMAD',
color: 'plum',
position: 41,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmex,
value: 'XMEX',
label: 'XMEX',
color: 'grass',
position: 42,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmil,
value: 'XMIL',
label: 'XMIL',
color: 'tomato',
position: 43,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xmun,
value: 'XMUN',
label: 'XMUN',
color: 'iris',
position: 44,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnas,
value: 'XNAS',
label: 'XNAS',
color: 'mint',
position: 45,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnse,
value: 'XNSE',
label: 'XNSE',
color: 'ruby',
position: 46,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnys,
value: 'XNYS',
label: 'XNYS',
color: 'bronze',
position: 47,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xnze,
value: 'XNZE',
label: 'XNZE',
color: 'jade',
position: 48,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xosl,
value: 'XOSL',
label: 'XOSL',
color: 'gray',
position: 49,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xotc,
value: 'XOTC',
label: 'XOTC',
color: 'blue',
position: 50,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xpar,
value: 'XPAR',
label: 'XPAR',
color: 'red',
position: 51,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xpra,
value: 'XPRA',
label: 'XPRA',
color: 'green',
position: 52,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xris,
value: 'XRIS',
label: 'XRIS',
color: 'orange',
position: 53,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xsau,
value: 'XSAU',
label: 'XSAU',
color: 'purple',
position: 54,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xses,
value: 'XSES',
label: 'XSES',
color: 'yellow',
position: 55,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xsgo,
value: 'XSGO',
label: 'XSGO',
color: 'pink',
position: 56,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xshe,
value: 'XSHE',
label: 'XSHE',
color: 'cyan',
position: 57,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xshg,
value: 'XSHG',
label: 'XSHG',
color: 'brown',
position: 58,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xstc,
value: 'XSTC',
label: 'XSTC',
color: 'lime',
position: 59,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xsto,
value: 'XSTO',
label: 'XSTO',
color: 'violet',
position: 60,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xstu,
value: 'XSTU',
label: 'XSTU',
color: 'gold',
position: 61,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xswx,
value: 'XSWX',
label: 'XSWX',
color: 'turquoise',
position: 62,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtae,
value: 'XTAE',
label: 'XTAE',
color: 'crimson',
position: 63,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtai,
value: 'XTAI',
label: 'XTAI',
color: 'sky',
position: 64,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtal,
value: 'XTAL',
label: 'XTAL',
color: 'amber',
position: 65,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtse,
value: 'XTSE',
label: 'XTSE',
color: 'plum',
position: 66,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xtsx,
value: 'XTSX',
label: 'XTSX',
color: 'grass',
position: 67,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xwar,
value: 'XWAR',
label: 'XWAR',
color: 'tomato',
position: 68,
},
{
id: PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.companyMicExchange.xwbo,
value: 'XWBO',
label: 'XWBO',
color: 'iris',
position: 69,
},
],
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlNaics,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlNaics',
label: 'NAICS Codes',
description: 'NAICS classification codes returned by People Data Labs.',
isNullable: true,
});
@@ -1,19 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier:
PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlNumberFundingRounds,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.NUMBER,
name: 'pdlNumberFundingRounds',
label: 'Number of Funding Rounds',
description: 'Number of funding rounds returned by People Data Labs.',
isNullable: true,
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlRawPayload,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlRawPayload',
label: 'PDL Raw Payload',
description: 'Full People Data Labs company enrichment response.',
isNullable: true,
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlSic,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.RAW_JSON,
name: 'pdlSic',
label: 'SIC Codes',
description: 'SIC classification codes returned by People Data Labs.',
isNullable: true,
});
@@ -1,27 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { SIZE_OPTIONS } from 'src/constants/size-options';
import {
PDL_FIELD_UNIVERSAL_IDENTIFIERS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS,
} from 'src/constants/universal-identifiers';
import { buildSelectOptions } from 'src/utils/build-select-options';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlSizeRange,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.SELECT,
name: 'pdlSizeRange',
label: 'Employee Range',
description: 'People Data Labs canonical self-reported employee range.',
isNullable: true,
options: buildSelectOptions(
SIZE_OPTIONS,
PDL_SELECT_OPTION_UNIVERSAL_IDENTIFIERS.sizeRange,
),
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlSummary,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.TEXT,
name: 'pdlSummary',
label: 'Summary',
description: 'Company summary returned by People Data Labs.',
isNullable: true,
});
@@ -1,18 +0,0 @@
import {
defineField,
FieldType,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { PDL_FIELD_UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers';
export default defineField({
universalIdentifier: PDL_FIELD_UNIVERSAL_IDENTIFIERS.company.pdlTags,
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
type: FieldType.ARRAY,
name: 'pdlTags',
label: 'Tags',
description: 'Tags returned by People Data Labs.',
isNullable: true,
});

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