Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ad5616c76 |
@@ -106,24 +106,24 @@ Replace `{VERSION}` with the actual version number (e.g., `1.9.0`)
|
||||
### 2. Create File Structure
|
||||
|
||||
**Create changelog file:**
|
||||
- Path: `packages/twenty-website/src/content/releases/{VERSION}.mdx`
|
||||
- Example: `packages/twenty-website/src/content/releases/1.9.0.mdx`
|
||||
- Path: `packages/twenty-website-new/src/content/releases/{VERSION}.mdx`
|
||||
- Example: `packages/twenty-website-new/src/content/releases/1.9.0.mdx`
|
||||
|
||||
**Create image folder:**
|
||||
- Path: `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
|
||||
- Example for version 1.9.0: `packages/twenty-website/public/images/releases/1.9/`
|
||||
- Example for version 2.0.0: `packages/twenty-website/public/images/releases/2.0/`
|
||||
- Path: `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
|
||||
- Example for version 1.9.0: `packages/twenty-website-new/public/images/releases/1.9/`
|
||||
- Example for version 2.0.0: `packages/twenty-website-new/public/images/releases/2.0/`
|
||||
|
||||
```bash
|
||||
# Create the image folder
|
||||
mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION}
|
||||
mkdir -p packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
|
||||
```
|
||||
|
||||
### 3. Move Illustration Files
|
||||
|
||||
**Source:** `/Users/thomascolasdesfrancs/Downloads/🆕`
|
||||
|
||||
**Destination:** `packages/twenty-website/public/images/releases/{MINOR_VERSION}/`
|
||||
**Destination:** `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
|
||||
|
||||
**Naming Convention:** `{VERSION}-descriptive-name.webp`
|
||||
|
||||
@@ -133,8 +133,8 @@ Examples:
|
||||
|
||||
```bash
|
||||
# Move and rename source files, then convert to webp if needed
|
||||
cp ~/Downloads/🆕/source-file.png packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
|
||||
cd packages/twenty-website && node scripts/convert-png-to-webp.mjs
|
||||
cp ~/Downloads/🆕/source-file.png packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
|
||||
cd packages/twenty-website-new && node scripts/convert-png-to-webp.mjs
|
||||
```
|
||||
|
||||
### 4. Research Features (if needed)
|
||||
@@ -183,7 +183,7 @@ Description of the third feature.
|
||||
- **NEVER mention the brand name "Twenty"** in changelog text - use "your workspace", "the platform", or similar neutral references instead
|
||||
|
||||
**Reference Previous Changelogs:**
|
||||
- Check `packages/twenty-website/src/content/releases/` for examples
|
||||
- Check `packages/twenty-website-new/src/content/releases/` for examples
|
||||
- Recent releases: 1.7.0.mdx, 1.6.0.mdx, 1.5.0.mdx
|
||||
|
||||
### 6. Review
|
||||
@@ -191,10 +191,10 @@ Description of the third feature.
|
||||
Open the changelog file for review:
|
||||
```bash
|
||||
# Open in Cursor
|
||||
cursor packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
cursor packages/twenty-website-new/src/content/releases/{VERSION}.mdx
|
||||
|
||||
# Open image folder to verify illustrations
|
||||
open packages/twenty-website/public/images/releases/{MINOR_VERSION}
|
||||
open packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
|
||||
```
|
||||
|
||||
Review checklist:
|
||||
@@ -222,8 +222,8 @@ I've created the changelog for version {VERSION}. Here's the content for your re
|
||||
[Show full MDX content]
|
||||
|
||||
Images moved to:
|
||||
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
|
||||
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp
|
||||
- packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
|
||||
- packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp
|
||||
|
||||
Please review the content. Once you approve, I'll commit the changes and create the pull request.
|
||||
```
|
||||
@@ -242,8 +242,8 @@ Possible user responses:
|
||||
git status
|
||||
|
||||
# Add files
|
||||
git add packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
git add packages/twenty-website/public/images/releases/{MINOR_VERSION}/
|
||||
git add packages/twenty-website-new/src/content/releases/{VERSION}.mdx
|
||||
git add packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
|
||||
|
||||
# Commit
|
||||
git commit -m "Add {VERSION} release changelog"
|
||||
@@ -266,7 +266,7 @@ This release includes:
|
||||
- Feature 2
|
||||
- Feature 3
|
||||
|
||||
Changelog file: \`packages/twenty-website/src/content/releases/{VERSION}.mdx\`
|
||||
Changelog file: \`packages/twenty-website-new/src/content/releases/{VERSION}.mdx\`
|
||||
Release date: {DATE}" \
|
||||
--base main \
|
||||
--head {VERSION}
|
||||
@@ -280,13 +280,13 @@ Or visit: `https://github.com/twentyhq/twenty/pull/new/{VERSION}`
|
||||
- **Format**: `{MAJOR}.{MINOR}.{PATCH}.mdx`
|
||||
- **Convention**: One file per complete version
|
||||
- **Examples**: `1.6.0.mdx`, `1.7.0.mdx`, `2.0.0.mdx`
|
||||
- **Location**: `packages/twenty-website/src/content/releases/`
|
||||
- **Location**: `packages/twenty-website-new/src/content/releases/`
|
||||
|
||||
### Image Folders
|
||||
- **Format**: `{MAJOR}.{MINOR}/`
|
||||
- **Convention**: One folder per minor version (shared across patches)
|
||||
- **Examples**: `1.6/`, `1.7/`, `2.0/`
|
||||
- **Location**: `packages/twenty-website/public/images/releases/`
|
||||
- **Location**: `packages/twenty-website-new/public/images/releases/`
|
||||
|
||||
### Image Files
|
||||
- **Format**: `{VERSION}-descriptive-name.webp`
|
||||
@@ -311,8 +311,8 @@ Features to document:
|
||||
3. ___________________________
|
||||
|
||||
Branch name: {VERSION}
|
||||
Changelog path: packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
Images path: packages/twenty-website/public/images/releases/{MINOR_VERSION}/
|
||||
Changelog path: packages/twenty-website-new/src/content/releases/{VERSION}.mdx
|
||||
Images path: packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# Crowdin CLI configuration for Website translations (twenty-website)
|
||||
# Crowdin CLI configuration for Website translations (twenty-website-new)
|
||||
# Project ID: 4
|
||||
# See https://crowdin.github.io/crowdin-cli/configuration for more information
|
||||
#
|
||||
@@ -16,7 +16,7 @@ files:
|
||||
#
|
||||
# Source file - PO file for Lingui
|
||||
#
|
||||
- source: packages/twenty-website/src/locales/en.po
|
||||
- source: packages/twenty-website-new/src/locales/en.po
|
||||
#
|
||||
# Translation files path
|
||||
#
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 &
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,7 +19,7 @@ jobs:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-website/**
|
||||
packages/twenty-website-new/**
|
||||
packages/twenty-shared/**
|
||||
website-task:
|
||||
needs: changed-files-check
|
||||
|
||||
@@ -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,12 +1,12 @@
|
||||
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:
|
||||
@@ -14,19 +14,39 @@ permissions:
|
||||
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({
|
||||
@@ -39,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
|
||||
@@ -51,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;
|
||||
|
||||
@@ -58,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,
|
||||
@@ -74,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;
|
||||
}
|
||||
@@ -83,65 +105,43 @@ jobs:
|
||||
core.setOutput('has_pr', 'true');
|
||||
core.info(`PR #${prNumber}`);
|
||||
|
||||
- name: Download storybook artifact from triggering run
|
||||
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: ${{ steps.project.outputs.artifact_name }}
|
||||
path: storybook-static
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- 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 .
|
||||
|
||||
- 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 }}
|
||||
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[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"
|
||||
|
||||
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 }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
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[run_id]=$RUN_ID" \
|
||||
-f "client_payload[repo]=$REPOSITORY" \
|
||||
-f "client_payload[project]=$PROJECT" \
|
||||
-f "client_payload[branch]=$BRANCH" \
|
||||
-f "client_payload[commit]=$COMMIT"
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
# Strict mode fails if there are missing website translations.
|
||||
- name: Compile website translations
|
||||
id: compile_translations_strict
|
||||
run: npx nx run twenty-website:lingui:compile --strict
|
||||
run: npx nx run twenty-website-new:lingui:compile --strict
|
||||
continue-on-error: true
|
||||
|
||||
- name: Stash any changes before pulling translations
|
||||
@@ -71,8 +71,8 @@ jobs:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
source: 'packages/twenty-website/src/locales/en.po'
|
||||
translation: 'packages/twenty-website/src/locales/%locale%.po'
|
||||
source: 'packages/twenty-website-new/src/locales/en.po'
|
||||
translation: 'packages/twenty-website-new/src/locales/%locale%.po'
|
||||
export_only_approved: false
|
||||
localization_branch_name: i18n-website
|
||||
base_url: 'https://twenty.api.crowdin.com'
|
||||
@@ -101,9 +101,9 @@ jobs:
|
||||
- name: Compile website translations
|
||||
id: compile_translations
|
||||
run: |
|
||||
npx nx run twenty-website:lingui:compile
|
||||
npx nx run twenty-website-new:lingui:compile
|
||||
git status
|
||||
git add packages/twenty-website/src/locales
|
||||
git add packages/twenty-website-new/src/locales
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: compile website translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
push:
|
||||
branches: ['main']
|
||||
paths:
|
||||
- 'packages/twenty-website/**'
|
||||
- 'packages/twenty-website-new/**'
|
||||
- '.github/crowdin-website.yml'
|
||||
- '.github/workflows/website-i18n-push.yaml'
|
||||
|
||||
@@ -41,14 +41,14 @@ jobs:
|
||||
run: npx nx build twenty-shared
|
||||
|
||||
- name: Extract website translations
|
||||
run: npx nx run twenty-website:lingui:extract
|
||||
run: npx nx run twenty-website-new:lingui:extract
|
||||
|
||||
- name: Check and commit extracted files
|
||||
id: check_extract_changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add packages/twenty-website/src/locales
|
||||
git add packages/twenty-website-new/src/locales
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: extract website translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
@@ -57,14 +57,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Compile website translations
|
||||
run: npx nx run twenty-website:lingui:compile
|
||||
run: npx nx run twenty-website-new:lingui:compile
|
||||
|
||||
- name: Check and commit compiled files
|
||||
id: check_compile_changes
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@twenty.com'
|
||||
git add packages/twenty-website/src/locales/generated
|
||||
git add packages/twenty-website-new/src/locales/generated
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: compile website translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -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"
|
||||
@@ -51,9 +51,7 @@ dump.rdb
|
||||
|
||||
mcp.json
|
||||
/.junie/
|
||||
/.agents/plugins/marketplace.json
|
||||
TRANSLATION_QA_REPORT.md
|
||||
.playwright-mcp/
|
||||
.playwright-cli/
|
||||
output/playwright/
|
||||
screenshots/
|
||||
|
||||
@@ -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/**"
|
||||
]
|
||||
}
|
||||
Vendored
+2
-2
@@ -81,8 +81,8 @@
|
||||
"path": "../packages/twenty-sdk"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-website",
|
||||
"path": "../packages/twenty-website"
|
||||
"name": "packages/twenty-website-new",
|
||||
"path": "../packages/twenty-website-new"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@@ -10,8 +10,4 @@ nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
npmPreapprovedPackages:
|
||||
- twenty-sdk
|
||||
- twenty-client-sdk
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||
|
||||
@@ -110,7 +110,7 @@ packages/
|
||||
├── twenty-ui/ # Shared UI components library
|
||||
├── twenty-shared/ # Common types and utilities
|
||||
├── twenty-emails/ # Email templates with React Email
|
||||
├── twenty-website/ # Next.js marketing website
|
||||
├── twenty-website-new/ # Next.js marketing website
|
||||
├── twenty-docs/ # Documentation website
|
||||
├── twenty-zapier/ # Zapier integration
|
||||
└── twenty-e2e-testing/ # Playwright E2E tests
|
||||
|
||||
@@ -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 14–27 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 9–12 (36–48px)
|
||||
- h2 / section heads: size 7–8 (28–32px)
|
||||
- h3 / card heads: size 5–6 (20–24px)
|
||||
- Body / prose: size 4–5 (16–20px)
|
||||
- Eyebrow / meta: size 3 (12px) with `letter-spacing: 0.06–0.08em` and `text-transform: uppercase`
|
||||
|
||||
Body line length: cap at 65–75ch (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(10–14)` — generous, editorial breathing room.
|
||||
- Inter-element gap inside a section: `theme.spacing(3–5)`.
|
||||
- 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`, 14–16px on body-level chips, 18–24px 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
@@ -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 2–5 candidates over a single browsing session. Will spend 30–90 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/`).
|
||||
@@ -1,19 +1,19 @@
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<img src="./packages/twenty-website/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
|
||||
<img src="./packages/twenty-website-new/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM</h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.webp" width="12" height="12"/> Figma</a></p>
|
||||
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website-new/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website-new/public/images/readme/figma-icon.webp" width="12" height="12"/> Figma</a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/github-cover-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/github-cover-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/github-cover-light.webp" alt="Twenty banner" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/github-cover-light.webp" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/github-cover-light.webp" alt="Twenty banner" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
@@ -24,17 +24,17 @@
|
||||
|
||||
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
|
||||
|
||||
<a href="https://twenty.com/resources/why-twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
|
||||
<a href="https://twenty.com/resources/why-twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="14" height="14"/> Learn more about why we built Twenty</a>
|
||||
|
||||
<br />
|
||||
|
||||
# Installation
|
||||
|
||||
### <img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
|
||||
### <img src="./packages/twenty-website-new/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
|
||||
|
||||
The fastest way to get started. Sign up at [twenty.com](https://twenty.com) and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
|
||||
|
||||
### <img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
|
||||
### <img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
|
||||
|
||||
Scaffold a new app with the Twenty CLI:
|
||||
|
||||
@@ -63,12 +63,12 @@ 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.
|
||||
|
||||
### <img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
|
||||
### <img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
|
||||
|
||||
Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.com/developers/self-host/capabilities/docker-compose), or contribute locally via the [local setup guide](https://docs.twenty.com/developers/contribute/capabilities/local-setup).
|
||||
|
||||
@@ -79,61 +79,61 @@ Run Twenty on your own infrastructure with [Docker Compose](https://docs.twenty.
|
||||
|
||||
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
|
||||
|
||||
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
|
||||
Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="14" height="14"/> User Guide</a> for product walkthroughs, or the <a href="https://docs.twenty.com"><img src="./packages/twenty-website-new/public/images/readme/book-icon.svg" width="14" height="14"/> Documentation</a> for developer reference.
|
||||
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-build-apps-light.webp" alt="Create your apps" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.webp" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-build-apps-light.webp" alt="Create your apps" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/getting-started"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="16" height="16"/> Learn more about apps in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-version-control-light.webp" alt="Stay on top with version control" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-version-control-light.webp" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-version-control-light.webp" alt="Stay on top with version control" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/publishing"><img src="./packages/twenty-website-new/public/images/readme/monitor-icon.svg" width="16" height="16"/> Learn more about version control in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-all-tools-light.webp" alt="All the tools you need to build anything" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.webp" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-all-tools-light.webp" alt="All the tools you need to build anything" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/developers/extend/apps/building"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="16" height="16"/> Learn more about primitives in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-tools-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-tools-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-tools-light.webp" alt="Customize your layouts" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-tools-light.webp" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-tools-light.webp" alt="Customize your layouts" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/layout/overview"><img src="./packages/twenty-website-new/public/images/readme/planner-icon.svg" width="16" height="16"/> Learn more about layouts in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-ai-agents-light.webp" alt="AI agents and chats" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.webp" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-ai-agents-light.webp" alt="AI agents and chats" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/ai/overview"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="16" height="16"/> Learn more about AI in doc</a></p>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" />
|
||||
<img src="./packages/twenty-website/public/images/readme/v2-crm-tools-light.webp" alt="Plus all the tools of a good CRM" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-dark.webp" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.webp" />
|
||||
<img src="./packages/twenty-website-new/public/images/readme/v2-crm-tools-light.webp" alt="Plus all the tools of a good CRM" />
|
||||
</picture>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
|
||||
<p align="center"><a href="https://docs.twenty.com/user-guide/introduction"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="16" height="16"/> Learn more about CRM features in doc</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -142,23 +142,28 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
|
||||
|
||||
# Stack
|
||||
|
||||
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
|
||||
- <a href="https://nx.dev/"><img src="./packages/twenty-website/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
|
||||
- <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>
|
||||
- <a href="https://www.typescriptlang.org/"><img src="./packages/twenty-website-new/public/images/readme/stack-typescript.svg" width="14" height="14"/> TypeScript</a>
|
||||
- <a href="https://nx.dev/"><img src="./packages/twenty-website-new/public/images/readme/stack-nx.svg" width="14" height="14"/> Nx</a>
|
||||
- <a href="https://nestjs.com/"><img src="./packages/twenty-website-new/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-new/public/images/readme/stack-postgresql.svg" width="14" height="14"/> PostgreSQL</a>, <a href="https://redis.io/"><img src="./packages/twenty-website-new/public/images/readme/stack-redis.svg" width="14" height="14"/> Redis</a>
|
||||
- <a href="https://reactjs.org/"><img src="./packages/twenty-website-new/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://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.webp" height="28" alt="Greptile" /></a>
|
||||
<a href="https://www.chromatic.com/"><img src="./packages/twenty-website-new/public/images/readme/chromatic.webp" height="28" alt="Chromatic" /></a>
|
||||
|
||||
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.webp" height="28" alt="Sentry" /></a>
|
||||
<a href="https://greptile.com"><img src="./packages/twenty-website-new/public/images/readme/greptile.webp" height="28" alt="Greptile" /></a>
|
||||
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.webp" height="28" alt="Crowdin" /></a>
|
||||
<a href="https://sentry.io/"><img src="./packages/twenty-website-new/public/images/readme/sentry.webp" height="28" alt="Sentry" /></a>
|
||||
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website-new/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
|
||||
|
||||
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
|
||||
<p><a href="https://github.com/twentyhq/twenty"><img src="./packages/twenty-website-new/public/images/readme/star-icon.svg" width="12" height="12"/> Star the repo</a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website-new/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://github.com/twentyhq/twenty/discussions"><img src="./packages/twenty-website-new/public/images/readme/message-icon.svg" width="12" height="12"/> Feature requests</a> · <a href="https://github.com/orgs/twentyhq/projects/1/views/35"><img src="./packages/twenty-website-new/public/images/readme/rocket-icon.svg" width="12" height="12"/> Releases</a> · <a href="https://twitter.com/twentycrm"><img src="./packages/twenty-website-new/public/images/readme/x-icon.svg" width="12" height="12"/> X</a> · <a href="https://www.linkedin.com/company/twenty/"><img src="./packages/twenty-website-new/public/images/readme/linkedin-icon.svg" width="12" height="12"/> LinkedIn</a> · <a href="https://twenty.crowdin.com/twenty"><img src="./packages/twenty-website-new/public/images/readme/language-icon.svg" width="12" height="12"/> Crowdin</a> · <a href="https://github.com/twentyhq/twenty/contribute"><img src="./packages/twenty-website-new/public/images/readme/code-icon.svg" width="12" height="12"/> Contribute</a></p>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB |
@@ -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": [
|
||||
|
||||
+2
-4
@@ -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",
|
||||
@@ -54,7 +53,7 @@
|
||||
"packages/twenty-ui",
|
||||
"packages/twenty-utils",
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-website-new",
|
||||
"packages/twenty-docs",
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
@@ -63,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"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div align="center">
|
||||
<a href="https://twenty.com">
|
||||
<picture>
|
||||
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website/public/images/core/logo.svg" height="128">
|
||||
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website-new/public/images/core/logo.svg" height="128">
|
||||
</picture>
|
||||
</a>
|
||||
<h1>Create Twenty App</h1>
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
+16
-28
@@ -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
|
||||
|
||||
-11
@@ -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',
|
||||
});
|
||||
-11
@@ -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',
|
||||
});
|
||||
+13
-1
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
+9
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
-11
@@ -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',
|
||||
});
|
||||
+9
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
-11
@@ -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',
|
||||
});
|
||||
+9
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
-11
@@ -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',
|
||||
});
|
||||
+9
@@ -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": "yarn@4.9.2",
|
||||
"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,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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],
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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>" } }
|
||||
\`\`\`
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -64,15 +64,15 @@ cd packages/twenty-apps/internal/twenty-linear
|
||||
# For day-to-day development (publish + install + watch in one):
|
||||
yarn twenty dev
|
||||
|
||||
# Manual publish flow (publish registers the app, install activates it):
|
||||
yarn twenty app:publish --private
|
||||
yarn twenty app:install
|
||||
# Manual publish flow (deploy registers the app, install activates it):
|
||||
yarn twenty deploy
|
||||
yarn twenty install
|
||||
```
|
||||
|
||||
`twenty dev` is recommended for iteration — it publishes, installs, and
|
||||
watches for changes in one command. Use `twenty app:publish --private` +
|
||||
`twenty app:install` when you want to control each step separately (e.g.
|
||||
deploying to a production server without auto-installing).
|
||||
watches for changes in one command. Use `twenty deploy` + `twenty install`
|
||||
when you want to control each step separately (e.g. deploying to a
|
||||
production server without auto-installing).
|
||||
|
||||
This serves as the reference implementation for Twenty's
|
||||
`defineConnectionProvider({ type: 'oauth' })` flow — useful as a template
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# Credentials for integration tests (vitest.config.ts) and seed scripts
|
||||
# (vitest.seed.config.ts). Copy this file to .env.local and fill in the key.
|
||||
# Get an API key from the Twenty UI: Settings -> APIs & Webhooks.
|
||||
# .env.local is gitignored; never commit a real key.
|
||||
TWENTY_API_URL=http://localhost:2020
|
||||
TWENTY_API_KEY=
|
||||
@@ -1,42 +0,0 @@
|
||||
name: CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_DEPLOY_URL: http://localhost:2020
|
||||
|
||||
concurrency:
|
||||
group: cd-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy-and-install:
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'pull_request' && github.event.label.name == 'deploy')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Deploy
|
||||
uses: twentyhq/twenty/.github/actions/deploy-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
|
||||
- name: Install
|
||||
uses: twentyhq/twenty/.github/actions/install-twenty-app@main
|
||||
with:
|
||||
api-url: ${{ env.TWENTY_DEPLOY_URL }}
|
||||
api-key: ${{ secrets.TWENTY_DEPLOY_API_KEY }}
|
||||
@@ -1,48 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_VERSION: latest
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Spawn Twenty test instance
|
||||
id: twenty
|
||||
uses: twentyhq/twenty/.github/actions/spawn-twenty-app-dev-test@main
|
||||
with:
|
||||
twenty-version: ${{ env.TWENTY_VERSION }}
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Run integration tests
|
||||
run: yarn test
|
||||
env:
|
||||
TWENTY_API_URL: ${{ steps.twenty.outputs.server-url }}
|
||||
TWENTY_API_KEY: ${{ steps.twenty.outputs.api-key }}
|
||||
@@ -1,43 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn
|
||||
|
||||
# codegen
|
||||
generated
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# dev
|
||||
/dist/
|
||||
|
||||
.twenty
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
# internal planning docs
|
||||
docs/superpowers/
|
||||
**/docs/superpowers/
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
*.d.ts
|
||||
@@ -1 +0,0 @@
|
||||
24.5.0
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"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,101 +0,0 @@
|
||||
# twenty-partners
|
||||
|
||||
A Twenty app that turns the CRM into the operating system for the Twenty partner program:
|
||||
intake partner-eligible deals, match them to vetted marketplace partners, and track the
|
||||
matching pipeline end-to-end.
|
||||
|
||||
Built on [Twenty](https://twenty.com) with [`twenty-sdk`](https://www.npmjs.com/package/twenty-sdk) v2.5.
|
||||
|
||||
## What's inside
|
||||
|
||||
- **Custom object: `Partner`** — slug, status, availability, served geos, languages spoken,
|
||||
deployment expertise, Calendly link, last-match timestamp. See `src/objects/partner.object.ts`.
|
||||
- **Opportunity extensions** — `matchStatus`, `designDocStatus`,
|
||||
`introSentAt`, `lastRelanceSentAt`, `tftId`, plus a `partner` relation.
|
||||
- **Logic functions**
|
||||
- `on-opportunity-auto-match` — fires when `matchStatus` is set to `AUTO_MATCH`. Assigns the longest-idle available partner and flips status to `MATCHED`. If no partner is available, hands off to `MANUAL_MATCH` with an audit Note explaining why.
|
||||
- `list-available-partners` — surfaces matchable partners for a given opportunity.
|
||||
- `post-install` — first-run setup.
|
||||
- **Roles** (`src/roles/`)
|
||||
- **Twenty Partner Ops** — internal team role, full CRUD on Partner/Company/Person/Opportunity.
|
||||
- **Partner** — placeholder external-partner role. *Do not assign until Twenty ships
|
||||
row-level permissions* — it currently grants access to every record.
|
||||
- **Views** (`src/views/`)
|
||||
- `Waiting for match` — opportunities awaiting human action (`matchStatus` is `TO_BE_MATCHED` or `MANUAL_MATCH`).
|
||||
- `Matches overview` — full matching funnel grouped by `matchStatus` (configure Kanban
|
||||
grouping manually in the UI).
|
||||
- `Opportunities` — replacement of the native opportunities view with the partner columns.
|
||||
- `Partners` and `All matched deals` — partner-side index and deal log.
|
||||
- **Sidebar nav** — surfaced in workflow order: `Waiting for match`, `All partner deals`,
|
||||
`Matches overview`, `Partners`, `Opportunities`.
|
||||
- **Seed scripts** (`src/scripts/`) — populate a fresh workspace with realistic demo data.
|
||||
|
||||
## Match status pipeline
|
||||
|
||||
`matchStatus` is a non-nullable SELECT field with a default of `TO_BE_MATCHED`. The 10 states follow the deal lifecycle:
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `TO_BE_MATCHED` | Default — deal entered, awaiting assignment |
|
||||
| `MANUAL_MATCH` | Needs a human to pick a partner |
|
||||
| `AUTO_MATCH` | Triggers automatic partner assignment |
|
||||
| `MATCHED` | Partner assigned |
|
||||
| `INTRODUCED_TO_A_PARTNER` | Customer intro sent |
|
||||
| `WORKING_WITH_A_PARTNER` | Engagement underway |
|
||||
| `IMPLEMENTING` | Active implementation |
|
||||
| `WON` | Deal closed won |
|
||||
| `RECONNECT_LATER` | Paused — reconnect in future |
|
||||
| `LOST` | Deal closed lost |
|
||||
|
||||
## Getting started
|
||||
|
||||
Requires a local Twenty server at `http://localhost:2020` and Node `^24.5`.
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Default dev credentials: `tim@apple.dev` / `tim@apple.dev`.
|
||||
|
||||
Run `yarn twenty help` for the full CLI reference.
|
||||
|
||||
## Common commands
|
||||
|
||||
| Command | What it does |
|
||||
| --- | --- |
|
||||
| `yarn twenty dev` | Start the dev server and sync the app on file changes |
|
||||
| `yarn twenty server status` | Check the local Twenty server |
|
||||
| `yarn lint` / `yarn lint:fix` | Run oxlint |
|
||||
| `yarn test` | Run integration tests (`vitest.config.ts`) |
|
||||
|
||||
## Seeding demo data
|
||||
|
||||
Two idempotent seed scripts. Both run via the `vitest.seed.config.ts` config that skips
|
||||
the global app uninstall/reinstall.
|
||||
|
||||
```bash
|
||||
# 1. Marketplace partners (run first — pipeline seed wires opportunities to these by slug)
|
||||
yarn vitest run --config vitest.seed.config.ts src/scripts/seed-marketplace-partners.ts
|
||||
|
||||
# 2. Pipeline demo: 3 companies, 3 people, 15 opportunities spread across matchStatus values
|
||||
yarn vitest run --config vitest.seed.config.ts src/scripts/seed-pipeline-demo.ts
|
||||
```
|
||||
|
||||
Both scripts skip records that already exist (by `slug`, `name`, or `firstName+lastName`),
|
||||
so they are safe to re-run.
|
||||
|
||||
## Known limitations
|
||||
|
||||
Current SDK gaps blocking further polish:
|
||||
|
||||
- Custom Partner record page layout (RECORD_TABLE has no relation scoping).
|
||||
- Native Opportunities view column-order override.
|
||||
- Kanban view configuration from app code (`ViewType.KANBAN` is currently ignored).
|
||||
- App and field descriptions.
|
||||
|
||||
## Learn more
|
||||
|
||||
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)
|
||||
- [`twenty-sdk` on npm](https://www.npmjs.com/package/twenty-sdk)
|
||||
- [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "twenty-partners",
|
||||
"version": "0.3.4",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"keywords": [],
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run --config vitest.unit.config.ts",
|
||||
"test:watch": "vitest",
|
||||
"seed": "tsx src/scripts/seed.ts",
|
||||
"seed:prod": "ENV_FILE=.env.prod tsx src/scripts/seed.ts",
|
||||
"purge": "tsx src/scripts/purge-soft-deleted.ts",
|
||||
"purge:prod": "ENV_FILE=.env.prod tsx src/scripts/purge-soft-deleted.ts",
|
||||
"import:dryrun": "tsx src/scripts/import-from-tft.ts",
|
||||
"import:dryrun:prod": "ENV_FILE=.env.prod tsx src/scripts/import-from-tft.ts",
|
||||
"import:apply": "IMPORT_APPLY=1 tsx src/scripts/import-from-tft.ts",
|
||||
"import:apply:prod": "ENV_FILE=.env.prod IMPORT_APPLY=1 tsx src/scripts/import-from-tft.ts",
|
||||
"migrate:partner-scope": "tsx src/scripts/migrate-partner-scope.ts",
|
||||
"migrate:partner-scope:prod": "ENV_FILE=.env.prod tsx src/scripts/migrate-partner-scope.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "2.4.0",
|
||||
"twenty-sdk": "2.4.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^19.0.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tsx": "^4.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
// Seed file: ensures at least one ACTIVE + AVAILABLE partner exists before
|
||||
// the matching integration tests run. Idempotent — skips if already seeded.
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { beforeAll, describe, it } from 'vitest';
|
||||
|
||||
describe('seed: marketplace partners', () => {
|
||||
let client: CoreApiClient;
|
||||
|
||||
beforeAll(() => {
|
||||
client = new CoreApiClient();
|
||||
});
|
||||
|
||||
it('ensures at least one ACTIVE AVAILABLE partner exists', async () => {
|
||||
const existingResult = await client.query({
|
||||
partners: {
|
||||
__args: {
|
||||
filter: { validationStage: { eq: 'VALIDATED' }, availability: { eq: 'AVAILABLE' } },
|
||||
first: 1,
|
||||
},
|
||||
edges: { node: { id: true } },
|
||||
},
|
||||
} as any);
|
||||
|
||||
const existing = (existingResult as any).partners.edges;
|
||||
if (existing.length > 0) {
|
||||
console.log('[seed] partner already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const PARTNERS = [
|
||||
{
|
||||
slug: 'nine-dots-ventures',
|
||||
name: 'Nine Dots Ventures',
|
||||
introduction: 'Boutique CRM implementer specialising in real-estate workflows.',
|
||||
calendlyLink: 'https://calendly.com/placeholder',
|
||||
deploymentExpertise: ['CLOUD', 'SELF_HOST'],
|
||||
servedGeos: ['EUROPE', 'MENA'],
|
||||
languagesSpoken: ['ENGLISH', 'FRENCH'],
|
||||
},
|
||||
{
|
||||
slug: 'elevate-consulting',
|
||||
name: 'Elevate Consulting',
|
||||
introduction: 'Revenue-operations partner for B2B SaaS teams.',
|
||||
calendlyLink: 'https://calendly.com/placeholder',
|
||||
deploymentExpertise: ['CLOUD'],
|
||||
servedGeos: ['US', 'LATAM'],
|
||||
languagesSpoken: ['ENGLISH', 'SPANISH'],
|
||||
},
|
||||
];
|
||||
|
||||
for (const p of PARTNERS) {
|
||||
const r = await client.mutation({
|
||||
createPartner: {
|
||||
__args: {
|
||||
data: {
|
||||
name: p.name,
|
||||
slug: p.slug,
|
||||
introduction: p.introduction,
|
||||
calendarLink: { primaryLinkUrl: p.calendlyLink },
|
||||
deploymentExpertise: p.deploymentExpertise,
|
||||
region: p.servedGeos,
|
||||
languagesSpoken: p.languagesSpoken,
|
||||
validationStage: 'VALIDATED',
|
||||
availability: 'AVAILABLE',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
} as any);
|
||||
console.log('[seed] created', (r as any).createPartner.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
|
||||
function validateEnv(): { apiUrl: string; apiKey: string } {
|
||||
const apiUrl = process.env.TWENTY_API_URL;
|
||||
const apiKey = process.env.TWENTY_API_KEY;
|
||||
|
||||
if (!apiUrl || !apiKey) {
|
||||
throw new Error(
|
||||
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
||||
'Start a local server: yarn twenty server start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
return { apiUrl, apiKey };
|
||||
}
|
||||
|
||||
async function checkServer(apiUrl: string) {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${apiUrl}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${apiUrl}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfig(apiUrl: string, apiKey: string) {
|
||||
const payload = JSON.stringify(
|
||||
{
|
||||
remotes: {
|
||||
local: { apiUrl, apiKey, accessToken: apiKey },
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload);
|
||||
}
|
||||
|
||||
export async function setup() {
|
||||
const { apiUrl, apiKey } = validateEnv();
|
||||
|
||||
await checkServer(apiUrl);
|
||||
|
||||
writeConfig(apiUrl, apiKey);
|
||||
|
||||
await appUninstall({ appPath: APP_PATH }).catch(() => {});
|
||||
|
||||
const result = await appDevOnce({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(`[dev] ${message}`),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Dev sync failed: ${result.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('App installation', () => {
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const client = new MetadataApiClient();
|
||||
|
||||
const result = await client.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const app = result.findManyApplications.find(
|
||||
(a: { universalIdentifier: string }) =>
|
||||
a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CoreApiClient', () => {
|
||||
it('should support CRUD on standard objects', async () => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const created = await client.mutation({
|
||||
createNote: {
|
||||
__args: { data: { title: 'Integration test note' } },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
expect(created.createNote.id).toBeDefined();
|
||||
|
||||
await client.mutation({
|
||||
destroyNote: {
|
||||
__args: { id: created.createNote.id },
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
APP_DESCRIPTION,
|
||||
APP_DISPLAY_NAME,
|
||||
APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: APP_DISPLAY_NAME,
|
||||
description: APP_DESCRIPTION,
|
||||
applicationVariables: {
|
||||
PARTNER_APPLICATION_SECRET: {
|
||||
universalIdentifier: '2026a052-9f01-4d18-b6a7-31c3a5b1c7d2',
|
||||
description:
|
||||
'Shared secret required in the X-Application-Secret header on POST /partner-applications. Must match the website route\'s PARTNER_APPLICATION_SECRET env var. Set per-workspace in Settings → Apps → Twenty Partners → Variables.',
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
export const APP_DISPLAY_NAME = 'Twenty partners';
|
||||
export const APP_DESCRIPTION = '';
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER = 'e662fc1f-02c1-41ff-b8ba-c95a447b3965';
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = 'ee18c3f3-ebe7-4c56-ad6d-aad555cc32db';
|
||||
export const PARTNER_OBJECT_UNIVERSAL_IDENTIFIER = '39101b39-1c16-4148-9e82-45dc271bb90d';
|
||||
export const PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER = '65172140-d377-41c1-a2ae-190e96fb79dd';
|
||||
export const ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER = '379b11d5-44d5-476b-ba7d-31d5f515c9b4';
|
||||
export const ALL_MATCHED_DEALS_VIEW_UNIVERSAL_IDENTIFIER = '7a34da39-a8e1-44c7-88b4-91ceaa064920';
|
||||
export const PARTNERS_NAV_UNIVERSAL_IDENTIFIER = '3fe15ab5-e38b-4914-af17-2270b210aeb2';
|
||||
export const PARTNER_DEALS_NAV_UNIVERSAL_IDENTIFIER = 'c5e4ac36-bede-4f4b-bfe8-bbd09518abae';
|
||||
export const ON_OPP_AUTO_MATCH_FN_UNIVERSAL_IDENTIFIER = 'eb8d4d26-8103-4b66-9026-6a86556f7ca5';
|
||||
export const POST_INSTALL_FN_UNIVERSAL_IDENTIFIER = 'f92bad2e-5905-4757-96ee-af9869d4ca0c';
|
||||
export const MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER = 'd8dd0623-3a4c-4ab3-a1e0-4ece7df24fb2';
|
||||
export const INTRO_SENT_AT_FIELD_UNIVERSAL_IDENTIFIER = 'fcf39b0c-0547-415e-806d-b238131ad7cc';
|
||||
|
||||
// Roles (Task 2)
|
||||
export const TWENTY_PARTNER_OPS_ROLE_UNIVERSAL_IDENTIFIER = '3340ca65-863d-4cdc-95c9-8abdec13d0f6';
|
||||
export const PARTNER_ROLE_UNIVERSAL_IDENTIFIER = 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343';
|
||||
|
||||
// Views (Task 3)
|
||||
export const WAITING_FOR_MATCH_VIEW_UNIVERSAL_IDENTIFIER = 'fe11e738-6bf3-4714-929c-51c76a3fd050';
|
||||
export const MATCHES_OVERVIEW_VIEW_UNIVERSAL_IDENTIFIER = '5a8fd51a-cf9e-4a6a-b1b4-b833b215fc1c';
|
||||
|
||||
// Nav items (Task 5)
|
||||
export const WAITING_FOR_MATCH_NAV_UNIVERSAL_IDENTIFIER = '00be7449-8927-47c8-a6a1-212d9106587f';
|
||||
export const MATCHES_OVERVIEW_NAV_UNIVERSAL_IDENTIFIER = '0cf349c9-fcbf-40f8-8e91-142c02bbde9c';
|
||||
|
||||
// Page layout (Task 6)
|
||||
export const PARTNER_RECORD_PAGE_UNIVERSAL_IDENTIFIER = 'a888b39e-d64a-48ba-a044-d8cb685fad74';
|
||||
|
||||
// All Opportunities view + nav
|
||||
export const ALL_OPPORTUNITIES_VIEW_UNIVERSAL_IDENTIFIER = '6ce1300b-6e91-4c28-83bb-6f692dbc7a98';
|
||||
export const ALL_OPPORTUNITIES_NAV_UNIVERSAL_IDENTIFIER = '37944f52-cbe5-4814-a1e6-be5b21425870';
|
||||
|
||||
// Partner views + nav (harmonization)
|
||||
export const PARTNER_APPLICATIONS_VIEW_UNIVERSAL_IDENTIFIER = 'b57a84ed-d7c1-420d-b0eb-348db0dac612';
|
||||
export const VALIDATED_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER = '13cca6a7-b9f1-4103-b011-ea2e39430899';
|
||||
export const PARTNER_CONTENT_VIEW_UNIVERSAL_IDENTIFIER = 'd9db705c-795a-4a14-b891-6201149510b3';
|
||||
export const PARTNER_APPLICATIONS_NAV_UNIVERSAL_IDENTIFIER = '13e2334a-6b1e-4080-8c74-d11109990cc1';
|
||||
export const VALIDATED_PARTNERS_NAV_UNIVERSAL_IDENTIFIER = '6aed30c6-d80f-4ac6-aab0-db5bc59e5c4b';
|
||||
export const PARTNER_CONTENT_NAV_UNIVERSAL_IDENTIFIER = '3543723d-80c1-466a-ac35-86f7b284917b';
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineApplicationRole } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
APP_DISPLAY_NAME,
|
||||
DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: `${APP_DISPLAY_NAME} default function role`,
|
||||
description: `${APP_DISPLAY_NAME} default function role`,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: true,
|
||||
canSoftDeleteAllObjectRecords: true,
|
||||
canDestroyAllObjectRecords: false,
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { OPPORTUNITIES_ON_PARTNER_FIELD_ID, PARTNER_ON_OPPORTUNITY_FIELD_ID } from './partner-on-opportunity.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: OPPORTUNITIES_ON_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'opportunities',
|
||||
label: 'Opportunities',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_ON_OPPORTUNITY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'cc6b8a59-f860-493f-8b9a-f138c078fbf1',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.SELECT,
|
||||
name: 'designDocStatus',
|
||||
label: 'Design Doc Status',
|
||||
defaultValue: "'DRAFT'",
|
||||
options: [
|
||||
{ id: '1901c790-22af-4149-a792-09374d67acfd', value: 'DRAFT', label: 'Draft', position: 0, color: 'gray' },
|
||||
{ id: '02cbe191-cc96-4b42-9d8e-f85cd47bed24', value: 'DONE', label: 'Done', position: 1, color: 'green' },
|
||||
{ id: '943e1389-12cd-4605-8066-db27ba68a50a', value: 'SHARED_WITH_PARTNER', label: 'Shared with Partner', position: 2, color: 'blue' },
|
||||
],
|
||||
});
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '37e5428c-6c8c-4616-b626-f0ea1caa443d',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.LINKS,
|
||||
name: 'designDocUrl',
|
||||
label: 'Design Doc URL',
|
||||
isNullable: true,
|
||||
});
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '7ac7517f-bbca-4b4c-8996-6f864f71219b',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.SELECT,
|
||||
name: 'hostingType',
|
||||
label: 'Hosting Type',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '42c108d7-a874-4d1f-be4c-e87edd08f3c7', value: 'CLOUD', label: 'Cloud', position: 0, color: 'sky' },
|
||||
{ id: '0fe995f4-42de-4160-96af-b3e7d542dfdd', value: 'SELF_HOSTING', label: 'Self-hosting', position: 1, color: 'purple' },
|
||||
],
|
||||
});
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { INTRO_SENT_AT_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: INTRO_SENT_AT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.DATE_TIME,
|
||||
name: 'introSentAt',
|
||||
label: 'Intro Sent At',
|
||||
isNullable: true,
|
||||
});
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '834e233d-b171-409e-825f-77ac49b0f19d',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.TEXT,
|
||||
name: 'lostReason',
|
||||
label: 'Lost Reason',
|
||||
isNullable: true,
|
||||
});
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.SELECT,
|
||||
name: 'matchStatus',
|
||||
label: 'Match Status',
|
||||
isNullable: false,
|
||||
defaultValue: "'TO_BE_MATCHED'",
|
||||
options: [
|
||||
// Pre-match (new). NEW UUIDs generated with `uuidgen` (v4).
|
||||
{ id: '8b3a1c0e-2f64-4a87-9d2b-1e3c4f5a6b78', value: 'TO_BE_MATCHED', label: 'To Be Matched', position: 0, color: 'grey' },
|
||||
{ id: '4c5d6e7f-8a9b-4c0d-9e1f-2a3b4c5d6e7f', value: 'MANUAL_MATCH', label: 'Manual Match', position: 1, color: 'grey' },
|
||||
{ id: '7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b', value: 'AUTO_MATCH', label: 'Auto Match', position: 2, color: 'yellow' },
|
||||
// Post-match. Reuse DELIVERED's UUID for MATCHED so existing rows auto-relabel.
|
||||
{ id: '095428d8-4680-4a2c-af83-7809dcb3f194', value: 'MATCHED', label: 'Matched', position: 3, color: 'blue' },
|
||||
{ id: '2f1c79a1-ca91-4937-a4c0-6422f6534d34', value: 'INTRODUCED_TO_A_PARTNER', label: 'Introduced to a partner', position: 4, color: 'sky' },
|
||||
{ id: '45cdf6ef-8672-40d5-b71f-1e5687ba5776', value: 'WORKING_WITH_A_PARTNER', label: 'Working with a partner', position: 5, color: 'turquoise' },
|
||||
{ id: '7189b18d-b0f7-435a-9272-f812cba5d13d', value: 'IMPLEMENTING', label: 'Implementing', position: 6, color: 'green' },
|
||||
{ id: '54cd33bc-11ea-42f1-87c8-cd9d32d2c266', value: 'WON', label: 'Won', position: 7, color: 'purple' },
|
||||
{ id: '505433e8-5367-4dfb-a89a-708d5182165b', value: 'RECONNECT_LATER', label: 'Reconnect later', position: 8, color: 'orange' },
|
||||
{ id: '572a9ad2-e0a6-49f2-b1f5-e36c75cc5176', value: 'LOST', label: 'Lost', position: 9, color: 'red' },
|
||||
],
|
||||
});
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '90c683ec-2365-4533-a187-7b9ae162b753',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.NUMBER,
|
||||
name: 'numberOfSeats',
|
||||
label: 'Number of Seats',
|
||||
isNullable: true,
|
||||
});
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '59d5de53-202f-4913-a417-8a08970d87cc',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.SELECT,
|
||||
name: 'subscriptionFrequency',
|
||||
label: 'Subscription Frequency',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: 'e53a9ebb-11d6-40de-93f2-6bcb6ab7141c', value: 'MONTHLY', label: 'Monthly', position: 0, color: 'blue' },
|
||||
{ id: '6eca9f01-f891-4c9d-bef6-9238c8e67392', value: 'ANNUAL', label: 'Annual', position: 1, color: 'green' },
|
||||
],
|
||||
});
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'a58214e9-38f9-4faf-8927-09b3980fd8c3',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.SELECT,
|
||||
name: 'subscriptionType',
|
||||
label: 'Subscription Type',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '6d91b477-5bf1-4f5c-8aef-577b6c21fe45', value: 'PRO', label: 'Pro', position: 0, color: 'blue' },
|
||||
{ id: '7b64f281-3445-4429-a5f0-af9484dff8b4', value: 'ORG', label: 'Org', position: 1, color: 'green' },
|
||||
{ id: 'bf202f8f-caf9-45bf-a80c-65b344b2798d', value: 'ENT', label: 'Enterprise', position: 2, color: 'purple' },
|
||||
],
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '2e3e1d04-2719-4e0d-9a6b-ec73acf896c5',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.TEXT,
|
||||
name: 'tftOpportunityId',
|
||||
label: 'TfT Opportunity ID',
|
||||
isNullable: true,
|
||||
});
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '1bc57f52-a621-4243-ae3e-05c3f504b90c',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.TEXT,
|
||||
name: 'useCase',
|
||||
label: 'Use Case',
|
||||
isNullable: true,
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_COMPANY_FIELD_ID = '2779015b-28fa-4117-8ce1-b0c98cf16de2';
|
||||
export const PARTNERS_ON_COMPANY_FIELD_ID = '2896d888-a4ab-4c29-bf63-e8bfdbd1924f';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_COMPANY_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNERS_ON_COMPANY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'companyId',
|
||||
},
|
||||
});
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_CONTENT_CUSTOMER_COMPANY_FIELD_ID = 'd448f6be-533f-4c43-a70f-55dc361dcdd7';
|
||||
export const PARTNER_CONTENTS_ON_COMPANY_FIELD_ID = '941e7707-1b3d-47d4-b13a-45c6628c1b3d';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENT_CUSTOMER_COMPANY_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'customerCompany',
|
||||
label: 'Customer Company',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENTS_ON_COMPANY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'customerCompanyId',
|
||||
},
|
||||
});
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_CONTENT_CUSTOMER_PERSON_FIELD_ID = 'f17547be-76aa-4231-bd1a-3f57bb1ae323';
|
||||
export const PARTNER_CONTENTS_ON_PERSON_FIELD_ID = 'fae42f4e-b054-4267-a761-81a6792f7c12';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENT_CUSTOMER_PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'customerPerson',
|
||||
label: 'Customer Person',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENTS_ON_PERSON_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'customerPersonId',
|
||||
},
|
||||
});
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_CONTENT_PARTNER_FIELD_ID = '1774738e-96a1-43e2-aca4-d3c7cc794c50';
|
||||
export const PARTNER_CONTENTS_ON_PARTNER_FIELD_ID = 'ac7995f1-7ccf-4607-827b-0788eeacaee0';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENT_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partner',
|
||||
label: 'Partner',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENTS_ON_PARTNER_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'partnerId',
|
||||
},
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_CONTENTS_ON_COMPANY_FIELD_ID, PARTNER_CONTENT_CUSTOMER_COMPANY_FIELD_ID } from './partner-content-customer-company.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENTS_ON_COMPANY_FIELD_ID,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerContents',
|
||||
label: 'Partner Content',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENT_CUSTOMER_COMPANY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { FieldType, RelationType, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_CONTENTS_ON_PARTNER_FIELD_ID, PARTNER_CONTENT_PARTNER_FIELD_ID } from './partner-content-partner.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENTS_ON_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerContents',
|
||||
label: 'Partner Content',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENT_PARTNER_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_CONTENTS_ON_PERSON_FIELD_ID, PARTNER_CONTENT_CUSTOMER_PERSON_FIELD_ID } from './partner-content-customer-person.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_CONTENTS_ON_PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerContents',
|
||||
label: 'Partner Content',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_CONTENT_CUSTOMER_PERSON_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user