Compare commits
147
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01b70f0c13 | ||
|
|
ae7db23bdd | ||
|
|
2f358a1775 | ||
|
|
fc3004b2f6 | ||
|
|
086b79e5b9 | ||
|
|
8cb88cabee | ||
|
|
ac89d2ff56 | ||
|
|
423faa6153 | ||
|
|
a051490ec9 | ||
|
|
34db7ac8b4 | ||
|
|
4a82cddad6 | ||
|
|
9d6c5b7d58 | ||
|
|
a2db0b6932 | ||
|
|
5eb79e7797 | ||
|
|
f19647617a | ||
|
|
424c6737a1 | ||
|
|
be8c25dfc2 | ||
|
|
b8de469f37 | ||
|
|
c74337978b | ||
|
|
059e75e532 | ||
|
|
bb5c2bd00c | ||
|
|
a2673da164 | ||
|
|
08682fb3f5 | ||
|
|
53d4e92dda | ||
|
|
53392f9a16 | ||
|
|
7e7bb81586 | ||
|
|
3703b3e2f6 | ||
|
|
e72d10f550 | ||
|
|
7ca9081efa | ||
|
|
015dca95fc | ||
|
|
076c05cbd0 | ||
|
|
82c565f7dc | ||
|
|
44bae39538 | ||
|
|
1188ea9cd5 | ||
|
|
f721420705 | ||
|
|
96d247e94d | ||
|
|
90f711361c | ||
|
|
98d47d0dd0 | ||
|
|
f613886511 | ||
|
|
a3e41ec267 | ||
|
|
d602f35cbd | ||
|
|
69d89f8cfc | ||
|
|
be39702fd2 | ||
|
|
50c8834e21 | ||
|
|
d54caf10c1 | ||
|
|
0b38b4ffc4 | ||
|
|
b8b115f4e3 | ||
|
|
85d649e831 | ||
|
|
2f4ebf8160 | ||
|
|
77603a4102 | ||
|
|
91ce59d8e2 | ||
|
|
056e3a4cd8 | ||
|
|
9876af5587 | ||
|
|
563acc3f57 | ||
|
|
452433a9ae | ||
|
|
3bda05ea57 | ||
|
|
eda41b4eba | ||
|
|
de044f4b45 | ||
|
|
e3c79c803c | ||
|
|
59d69e2f5c | ||
|
|
4554dbe3c9 | ||
|
|
efba71a938 | ||
|
|
e6cc783a23 | ||
|
|
76e144e85a | ||
|
|
8f24cda586 | ||
|
|
c2a48a1cc2 | ||
|
|
37a1d3980f | ||
|
|
05f58b2dba | ||
|
|
4e0b69eb8d | ||
|
|
99799073e9 | ||
|
|
084fa8eaba | ||
|
|
788d120b71 | ||
|
|
3128157988 | ||
|
|
068d365731 | ||
|
|
9b05e7fff4 | ||
|
|
323e66433e | ||
|
|
e2ee4ffdff | ||
|
|
9a7f50fcb7 | ||
|
|
c721fa8502 | ||
|
|
b0a1002838 | ||
|
|
1e2ae5342b | ||
|
|
11b9f708d6 | ||
|
|
07a20cba5e | ||
|
|
138eb5a74a | ||
|
|
a66fae79ee | ||
|
|
edf2bfbf76 | ||
|
|
64241ed8ae | ||
|
|
0edd8d400c | ||
|
|
3c91f3f276 | ||
|
|
0bfcd9a701 | ||
|
|
869f8af4f0 | ||
|
|
2fcf3e3c2b | ||
|
|
8826d12a18 | ||
|
|
abf5902ab5 | ||
|
|
a78c4c9fe8 | ||
|
|
a9ff1a9d3c | ||
|
|
570c57563a | ||
|
|
4b8c722b41 | ||
|
|
d13cc7c349 | ||
|
|
d192d2d492 | ||
|
|
7ea652ddf8 | ||
|
|
9b9c97a049 | ||
|
|
f1b2be041a | ||
|
|
2963fa9324 | ||
|
|
a6a08439f7 | ||
|
|
383edd5871 | ||
|
|
16ad50ea46 | ||
|
|
74fa26f9e5 | ||
|
|
7ca2efeb96 | ||
|
|
224376233b | ||
|
|
a3c92311e3 | ||
|
|
1ed347bc17 | ||
|
|
b792f7654b | ||
|
|
42ad3cbd1a | ||
|
|
e1378dd4cf | ||
|
|
5e1b23a28b | ||
|
|
7aef406696 | ||
|
|
923c3beead | ||
|
|
31842f7714 | ||
|
|
9988f98577 | ||
|
|
3c458ce4ca | ||
|
|
b869107a22 | ||
|
|
4c4dc4cb21 | ||
|
|
237a943947 | ||
|
|
6e5e7963b5 | ||
|
|
a2acf88a57 | ||
|
|
c002bc52bd | ||
|
|
127fb2a470 | ||
|
|
3d49c17e34 | ||
|
|
b454ad2aea | ||
|
|
fb47e4497a | ||
|
|
00fad657f4 | ||
|
|
c800eccc65 | ||
|
|
a26fe3bb65 | ||
|
|
8d81496625 | ||
|
|
f1b7c21b6c | ||
|
|
f630ce34fe | ||
|
|
7ebcbe8801 | ||
|
|
b821061526 | ||
|
|
f3aadbbb66 | ||
|
|
e494bc7006 | ||
|
|
91e0e07af4 | ||
|
|
7ab6f5719f | ||
|
|
d17393e91d | ||
|
|
658bdf3e57 | ||
|
|
24a836cc7d | ||
|
|
1a9f786e42 |
@@ -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-new/src/content/releases/{VERSION}.mdx`
|
||||
- Example: `packages/twenty-website-new/src/content/releases/1.9.0.mdx`
|
||||
- Path: `packages/twenty-website/src/content/releases/{VERSION}.mdx`
|
||||
- Example: `packages/twenty-website/src/content/releases/1.9.0.mdx`
|
||||
|
||||
**Create image folder:**
|
||||
- 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/`
|
||||
- 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/`
|
||||
|
||||
```bash
|
||||
# Create the image folder
|
||||
mkdir -p packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
|
||||
mkdir -p packages/twenty-website/public/images/releases/{MINOR_VERSION}
|
||||
```
|
||||
|
||||
### 3. Move Illustration Files
|
||||
|
||||
**Source:** `/Users/thomascolasdesfrancs/Downloads/🆕`
|
||||
|
||||
**Destination:** `packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/`
|
||||
**Destination:** `packages/twenty-website/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-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-name.png
|
||||
cd packages/twenty-website-new && node scripts/convert-png-to-webp.mjs
|
||||
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
|
||||
```
|
||||
|
||||
### 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-new/src/content/releases/` for examples
|
||||
- Check `packages/twenty-website/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-new/src/content/releases/{VERSION}.mdx
|
||||
cursor packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
|
||||
# Open image folder to verify illustrations
|
||||
open packages/twenty-website-new/public/images/releases/{MINOR_VERSION}
|
||||
open packages/twenty-website/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-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
|
||||
- packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-2.webp
|
||||
- packages/twenty-website/public/images/releases/{MINOR_VERSION}/{VERSION}-feature-1.webp
|
||||
- packages/twenty-website/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-new/src/content/releases/{VERSION}.mdx
|
||||
git add packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
|
||||
git add packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
git add packages/twenty-website/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-new/src/content/releases/{VERSION}.mdx\`
|
||||
Changelog file: \`packages/twenty-website/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-new/src/content/releases/`
|
||||
- **Location**: `packages/twenty-website/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-new/public/images/releases/`
|
||||
- **Location**: `packages/twenty-website/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-new/src/content/releases/{VERSION}.mdx
|
||||
Images path: packages/twenty-website-new/public/images/releases/{MINOR_VERSION}/
|
||||
Changelog path: packages/twenty-website/src/content/releases/{VERSION}.mdx
|
||||
Images path: packages/twenty-website/public/images/releases/{MINOR_VERSION}/
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
@@ -50,4 +50,4 @@ runs:
|
||||
- name: Deploy
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn twenty deploy --remote target
|
||||
run: yarn twenty app:publish --private --remote target
|
||||
|
||||
@@ -50,4 +50,4 @@ runs:
|
||||
- name: Install
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.app-path }}
|
||||
run: yarn twenty install --remote target
|
||||
run: yarn twenty app:install --remote target
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# Crowdin CLI configuration for Website translations (twenty-website-new)
|
||||
# Crowdin CLI configuration for Website translations (twenty-website)
|
||||
# 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-new/src/locales/en.po
|
||||
- source: packages/twenty-website/src/locales/en.po
|
||||
#
|
||||
# Translation files path
|
||||
#
|
||||
|
||||
@@ -144,6 +144,9 @@ 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
|
||||
@@ -160,7 +163,7 @@ jobs:
|
||||
- name: Wait for current branch server to be ready
|
||||
run: |
|
||||
echo "Waiting for current branch server to start..."
|
||||
timeout=300
|
||||
timeout=60
|
||||
interval=5
|
||||
elapsed=0
|
||||
|
||||
@@ -185,9 +188,10 @@ jobs:
|
||||
done
|
||||
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "::warning::Timed out waiting for current branch server to serve a valid schema. Validation will skip the API diff."
|
||||
echo "❌ Timed out waiting for current branch server to serve a valid schema."
|
||||
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
|
||||
@@ -311,6 +315,9 @@ 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
|
||||
@@ -352,9 +359,10 @@ jobs:
|
||||
done
|
||||
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "::warning::Timed out waiting for main branch server to serve a valid schema. Validation will skip the API diff."
|
||||
echo "❌ Timed out waiting for main branch server to serve a valid schema."
|
||||
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
|
||||
@@ -450,6 +458,7 @@ 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 ==="
|
||||
@@ -463,6 +472,7 @@ 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 || {
|
||||
@@ -480,6 +490,7 @@ 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 || {
|
||||
@@ -496,6 +507,7 @@ 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 ==="
|
||||
@@ -518,6 +530,7 @@ 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
|
||||
@@ -565,6 +578,7 @@ 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 ==="
|
||||
@@ -587,6 +601,7 @@ 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
|
||||
@@ -632,6 +647,79 @@ 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
|
||||
@@ -652,5 +740,3 @@ jobs:
|
||||
if [ -f /tmp/main-server.pid ]; then
|
||||
kill $(cat /tmp/main-server.pid) || true
|
||||
fi
|
||||
|
||||
|
||||
|
||||
@@ -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" --workspace-url http://localhost:3000
|
||||
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --url http://localhost:3000
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
@@ -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 }} --api-url ${{ env.TWENTY_API_URL }}
|
||||
npx --no-install twenty remote:add --api-key ${{ env.TWENTY_API_KEY }} --url ${{ env.TWENTY_API_URL }}
|
||||
|
||||
- name: Run scaffolded app integration test (deploys, installs, and verifies the app)
|
||||
run: |
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-website-new/**
|
||||
packages/twenty-website/**
|
||||
packages/twenty-shared/**
|
||||
website-task:
|
||||
needs: changed-files-check
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name: 'Preview Environment Dispatch'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -11,7 +10,6 @@ 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 }}
|
||||
@@ -35,28 +33,15 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger preview environment workflow
|
||||
- name: Dispatch preview-env to ci-privileged
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
gh api repos/"$REPOSITORY"/dispatches \
|
||||
gh api repos/twentyhq/ci-privileged/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"
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
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: ./
|
||||
@@ -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-new:lingui:compile --strict
|
||||
run: npx nx run twenty-website: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-new/src/locales/en.po'
|
||||
translation: 'packages/twenty-website-new/src/locales/%locale%.po'
|
||||
source: 'packages/twenty-website/src/locales/en.po'
|
||||
translation: 'packages/twenty-website/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-new:lingui:compile
|
||||
npx nx run twenty-website:lingui:compile
|
||||
git status
|
||||
git add packages/twenty-website-new/src/locales
|
||||
git add packages/twenty-website/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-new/**'
|
||||
- 'packages/twenty-website/**'
|
||||
- '.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-new:lingui:extract
|
||||
run: npx nx run twenty-website: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-new/src/locales
|
||||
git add packages/twenty-website/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-new:lingui:compile
|
||||
run: npx nx run twenty-website: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-new/src/locales/generated
|
||||
git add packages/twenty-website/src/locales/generated
|
||||
if ! git diff --staged --quiet --exit-code; then
|
||||
git commit -m "chore: compile website translations"
|
||||
echo "changes_detected=true" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
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"
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"$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-new",
|
||||
"path": "../packages/twenty-website-new"
|
||||
"name": "packages/twenty-website",
|
||||
"path": "../packages/twenty-website"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@@ -10,4 +10,8 @@ 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-new/ # Next.js marketing website
|
||||
├── twenty-website/ # Next.js marketing website
|
||||
├── twenty-docs/ # Documentation website
|
||||
├── twenty-zapier/ # Zapier integration
|
||||
└── twenty-e2e-testing/ # Playwright E2E tests
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<p align="center">
|
||||
<a href="https://www.twenty.com">
|
||||
<img src="./packages/twenty-website-new/public/images/core/logo.svg" width="100px" alt="Twenty logo" />
|
||||
<img src="./packages/twenty-website/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-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://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://www.twenty.com">
|
||||
<picture>
|
||||
<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" />
|
||||
<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" />
|
||||
</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-new/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/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-new/public/images/readme/globe-icon.svg" width="14" height="14"/> Cloud
|
||||
### <img src="./packages/twenty-website/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-new/public/images/readme/book-icon.svg" width="14" height="14"/> Build an app
|
||||
### <img src="./packages/twenty-website/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 deploy
|
||||
npx twenty app:publish --private
|
||||
```
|
||||
|
||||
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-new/public/images/readme/rocket-icon.svg" width="14" height="14"/> Self-hosting
|
||||
### <img src="./packages/twenty-website/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-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.
|
||||
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.
|
||||
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<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" />
|
||||
<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" />
|
||||
</picture>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<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" />
|
||||
<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" />
|
||||
</picture>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<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" />
|
||||
<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" />
|
||||
</picture>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<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" />
|
||||
<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" />
|
||||
</picture>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<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" />
|
||||
<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" />
|
||||
</picture>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<picture>
|
||||
<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" />
|
||||
<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" />
|
||||
</picture>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -142,23 +142,23 @@ 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-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>
|
||||
- <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>
|
||||
|
||||
|
||||
|
||||
# Thanks
|
||||
|
||||
<p align="center">
|
||||
<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://www.chromatic.com/"><img src="./packages/twenty-website/public/images/readme/chromatic.webp" height="28" alt="Chromatic" /></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://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.webp" height="28" alt="Greptile" /></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://sentry.io/"><img src="./packages/twenty-website/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>
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.webp" height="28" alt="Crowdin" /></a>
|
||||
</p>
|
||||
|
||||
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
|
||||
@@ -166,4 +166,4 @@ Want to go deeper? Read the <a href="https://docs.twenty.com/user-guide/introduc
|
||||
|
||||
# Join the Community
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"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))"
|
||||
"command": "npx oxlint -c .oxlintrc.json . && (npx oxfmt --check . || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint --configuration=fix' && false))"
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {},
|
||||
"fix": {
|
||||
"command": "npx oxlint --fix -c .oxlintrc.json . && prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
|
||||
"command": "npx oxlint --fix -c .oxlintrc.json . && npx oxfmt ."
|
||||
}
|
||||
},
|
||||
"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 && (prettier --check $FILES || (echo 'ERROR: Prettier 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 && (npx oxfmt --check $FILES || (echo 'ERROR: oxfmt 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 && prettier --write $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 && npx oxfmt $FILES)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -73,18 +73,14 @@
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"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
|
||||
"command": "npx oxfmt --check {args.files} {args.write}",
|
||||
"files": ".",
|
||||
"write": ""
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"cacheStrategy": "content"
|
||||
},
|
||||
"ci": {},
|
||||
"fix": {
|
||||
"write": true
|
||||
"command": "npx oxfmt {args.files}"
|
||||
}
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -53,7 +54,7 @@
|
||||
"packages/twenty-ui",
|
||||
"packages/twenty-utils",
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website-new",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-docs",
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
|
||||
@@ -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-new/public/images/core/logo.svg" height="128">
|
||||
<img alt="Twenty logo" src="https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-website/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 |
|
||||
| `--workspace-url <url>` | Twenty workspace URL (default: `http://localhost:2020`) |
|
||||
| `--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 server 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 docker: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.5.1",
|
||||
"version": "2.9.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -18,11 +18,8 @@ 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(
|
||||
'--workspace-url <workspaceUrl>',
|
||||
'Twenty workspace URL (default: http://localhost:2020)',
|
||||
)
|
||||
.option('--api-url <apiUrl>', '[deprecated: use --workspace-url]')
|
||||
.option('--url <url>', 'Twenty server URL (default: http://localhost:2020)')
|
||||
.option('--api-url <apiUrl>', '[deprecated: use --url]')
|
||||
.option(
|
||||
'--authentication-method <method>',
|
||||
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)',
|
||||
@@ -35,7 +32,7 @@ const program = new Command(packageJson.name)
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
workspaceUrl?: string;
|
||||
url?: string;
|
||||
apiUrl?: string;
|
||||
authenticationMethod?: AuthenticationMethod;
|
||||
},
|
||||
@@ -68,18 +65,18 @@ const program = new Command(packageJson.name)
|
||||
|
||||
if (options?.apiUrl) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
'Warning: --api-url is deprecated. Use --workspace-url instead.',
|
||||
),
|
||||
chalk.yellow('Warning: --api-url is deprecated. Use --url instead.'),
|
||||
);
|
||||
}
|
||||
|
||||
const serverUrl = (options?.url ?? options?.apiUrl)?.replace(/\/+$/, '');
|
||||
|
||||
await new CreateAppCommand().execute({
|
||||
directory,
|
||||
name: options?.name,
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
workspaceUrl: options?.workspaceUrl ?? options?.apiUrl,
|
||||
serverUrl,
|
||||
authenticationMethod: options?.authenticationMethod,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -49,19 +49,19 @@
|
||||
|
||||
## Best practice
|
||||
|
||||
It's highly recommended to create new app entities using `yarn twenty add`. These are the options:
|
||||
It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options:
|
||||
|
||||
| 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` |
|
||||
| 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` |
|
||||
|
||||
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 server status` - Check the local Twenty server status
|
||||
- `yarn twenty server start` - Start the local Twenty server
|
||||
- `yarn twenty docker:status` - Check the local Twenty server status
|
||||
- `yarn twenty docker:start` - Start the local Twenty server
|
||||
- `yarn test` - Run integration tests
|
||||
|
||||
## Learn More
|
||||
|
||||
@@ -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 server start\n' +
|
||||
'Start a local server: yarn twenty docker:start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
+28
-16
@@ -1,4 +1,11 @@
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
Avatar,
|
||||
IconBox,
|
||||
IconHierarchy,
|
||||
IconLayout,
|
||||
IconSettingsAutomation,
|
||||
} from 'twenty-sdk/ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
@@ -30,7 +37,7 @@ const CATEGORIES = [
|
||||
href: `${DOCS_BASE_URL}/logic/logic-functions`,
|
||||
},
|
||||
{
|
||||
label: 'SERVERLESS FUNCT.',
|
||||
label: 'LOGIC FUNCTION',
|
||||
href: `${DOCS_BASE_URL}/logic/logic-functions`,
|
||||
},
|
||||
{
|
||||
@@ -59,15 +66,6 @@ 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
|
||||
@@ -93,6 +91,18 @@ 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={{
|
||||
@@ -111,8 +121,12 @@ const CategoryCard = ({
|
||||
style={{
|
||||
padding: '16px 20px',
|
||||
background: `${color}22`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<CategoryIcon />
|
||||
<span
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
@@ -154,7 +168,7 @@ const CategoryCard = ({
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
>
|
||||
<ItemIcon color={color} />
|
||||
<IconBox color={color} size={'20px'} />
|
||||
<span
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
@@ -190,13 +204,11 @@ const MainPage = () => {
|
||||
padding: '40px',
|
||||
}}
|
||||
>
|
||||
{/*<Avatar
|
||||
avatarUrl={getPublicAssetUrl('logo.svg')}
|
||||
<Avatar
|
||||
placeholder={APP_DISPLAY_NAME}
|
||||
placeholderColorSeed={APP_DISPLAY_NAME}
|
||||
type="squared"
|
||||
size="xl"
|
||||
/>*/}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
@@ -220,7 +232,7 @@ const MainPage = () => {
|
||||
You can now add content to your app.
|
||||
</span>
|
||||
<a
|
||||
href="/settings/applications"
|
||||
href="/settings/applications#installed"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
DEV_API_URL,
|
||||
serverStart,
|
||||
} from 'twenty-sdk/cli';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined, normalizeUrl } from 'twenty-shared/utils';
|
||||
import {
|
||||
getDockerInstallInstructions,
|
||||
isDockerInstalled,
|
||||
@@ -33,7 +33,7 @@ type CreateAppOptions = {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
workspaceUrl?: string;
|
||||
serverUrl?: string;
|
||||
authenticationMethod?: AuthenticationMethod;
|
||||
};
|
||||
|
||||
@@ -45,9 +45,9 @@ export class CreateAppCommand {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
this.getAppInfos(options);
|
||||
|
||||
const workspaceUrl = options.workspaceUrl ?? DEV_API_URL;
|
||||
const serverUrl = options.serverUrl ?? DEV_API_URL;
|
||||
|
||||
const skipLocalInstance = workspaceUrl !== DEV_API_URL;
|
||||
const skipLocalInstance = serverUrl !== 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 resolvedWorkspaceUrl = workspaceUrl;
|
||||
let resolvedServerUrl = serverUrl;
|
||||
let serverReady = skipLocalInstance;
|
||||
|
||||
if (!skipLocalInstance) {
|
||||
@@ -126,7 +126,7 @@ export class CreateAppCommand {
|
||||
const serverResult = await this.ensureDockerServer(dockerPullPromise);
|
||||
|
||||
if (isDefined(serverResult.url)) {
|
||||
resolvedWorkspaceUrl = serverResult.url;
|
||||
resolvedServerUrl = serverResult.url;
|
||||
serverReady = true;
|
||||
}
|
||||
}
|
||||
@@ -134,18 +134,16 @@ export class CreateAppCommand {
|
||||
if (serverReady) {
|
||||
this.logNextStep('Authenticating');
|
||||
|
||||
authSucceeded = await this.tryExistingAuth(resolvedWorkspaceUrl);
|
||||
authSucceeded = await this.tryExistingAuth(resolvedServerUrl);
|
||||
|
||||
if (authSucceeded) {
|
||||
this.logDetail('Reusing existing credentials');
|
||||
} else if (authenticationMethod === 'oauth') {
|
||||
this.logDetail('Starting OAuth flow');
|
||||
authSucceeded =
|
||||
await this.authenticateWithOAuth(resolvedWorkspaceUrl);
|
||||
authSucceeded = await this.authenticateWithOAuth(resolvedServerUrl);
|
||||
} else {
|
||||
this.logDetail('Using development API key');
|
||||
authSucceeded =
|
||||
await this.authenticateWithDevKey(resolvedWorkspaceUrl);
|
||||
authSucceeded = await this.authenticateWithDevKey(resolvedServerUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,10 +163,10 @@ export class CreateAppCommand {
|
||||
}
|
||||
|
||||
if (syncSucceeded) {
|
||||
await this.openMainPage(appDirectory, resolvedWorkspaceUrl);
|
||||
await this.openMainPage(appDirectory, resolvedServerUrl);
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, resolvedWorkspaceUrl, authSucceeded);
|
||||
this.logSuccess(appDirectory, resolvedServerUrl, authSucceeded);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('\nCreate application failed:'),
|
||||
@@ -315,7 +313,7 @@ export class CreateAppCommand {
|
||||
|
||||
private async openMainPage(
|
||||
appDirectory: string,
|
||||
workspaceUrl: string,
|
||||
serverUrl: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const configService = new ConfigService();
|
||||
@@ -328,7 +326,7 @@ export class CreateAppCommand {
|
||||
|
||||
const [universalIdentifier, frontUrl] = await Promise.all([
|
||||
this.readMainPageLayoutUniversalIdentifier(appDirectory),
|
||||
this.resolveWorkspaceFrontUrl(workspaceUrl, token),
|
||||
this.resolveWorkspaceFrontUrl(serverUrl, token),
|
||||
]);
|
||||
|
||||
if (!universalIdentifier || !frontUrl) {
|
||||
@@ -336,7 +334,7 @@ export class CreateAppCommand {
|
||||
}
|
||||
|
||||
const pageLayoutId = await this.resolvePageLayoutId(
|
||||
workspaceUrl,
|
||||
serverUrl,
|
||||
universalIdentifier,
|
||||
token,
|
||||
);
|
||||
@@ -355,12 +353,12 @@ export class CreateAppCommand {
|
||||
}
|
||||
|
||||
private async resolveWorkspaceFrontUrl(
|
||||
workspaceUrl: string,
|
||||
serverUrl: string,
|
||||
token: string,
|
||||
): Promise<string | null> {
|
||||
const query = `{ currentWorkspace { workspaceUrls { subdomainUrl customUrl } } }`;
|
||||
|
||||
const response = await fetch(`${workspaceUrl}/metadata`, {
|
||||
const response = await fetch(`${serverUrl}/metadata`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -389,7 +387,7 @@ export class CreateAppCommand {
|
||||
|
||||
const frontUrl = urls.customUrl ?? urls.subdomainUrl;
|
||||
|
||||
return frontUrl?.replace(/\/+$/, '') ?? null;
|
||||
return frontUrl ? normalizeUrl(frontUrl) : null;
|
||||
}
|
||||
|
||||
private async readMainPageLayoutUniversalIdentifier(
|
||||
@@ -410,13 +408,13 @@ export class CreateAppCommand {
|
||||
}
|
||||
|
||||
private async resolvePageLayoutId(
|
||||
workspaceUrl: string,
|
||||
serverUrl: string,
|
||||
universalIdentifier: string,
|
||||
token: string,
|
||||
): Promise<string | null> {
|
||||
const query = `{ getPageLayouts { id universalIdentifier } }`;
|
||||
|
||||
const response = await fetch(`${workspaceUrl}/metadata`, {
|
||||
const response = await fetch(`${serverUrl}/metadata`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -503,7 +501,7 @@ export class CreateAppCommand {
|
||||
});
|
||||
}
|
||||
|
||||
private async tryExistingAuth(workspaceUrl: string): Promise<boolean> {
|
||||
private async tryExistingAuth(serverUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const configService = new ConfigService();
|
||||
const remoteNames = await configService.getRemotes();
|
||||
@@ -511,7 +509,7 @@ export class CreateAppCommand {
|
||||
for (const remoteName of remoteNames) {
|
||||
const remoteConfig = await configService.getConfigForRemote(remoteName);
|
||||
|
||||
if (remoteConfig.apiUrl !== workspaceUrl) {
|
||||
if (remoteConfig.apiUrl !== serverUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -521,7 +519,7 @@ export class CreateAppCommand {
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await fetch(`${workspaceUrl}/metadata`, {
|
||||
const response = await fetch(`${serverUrl}/metadata`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -555,11 +553,11 @@ export class CreateAppCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async authenticateWithDevKey(workspaceUrl: string): Promise<boolean> {
|
||||
private async authenticateWithDevKey(serverUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await authLogin({
|
||||
apiKey: DEV_API_KEY,
|
||||
apiUrl: workspaceUrl,
|
||||
apiUrl: serverUrl,
|
||||
remote: 'local',
|
||||
});
|
||||
|
||||
@@ -574,7 +572,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.',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -582,7 +580,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.',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -598,21 +596,21 @@ export class CreateAppCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async authenticateWithOAuth(workspaceUrl: string): Promise<boolean> {
|
||||
private async authenticateWithOAuth(serverUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const remoteName = this.deriveRemoteName(workspaceUrl);
|
||||
const remoteName = this.deriveRemoteName(serverUrl);
|
||||
|
||||
ConfigService.setActiveRemote(remoteName);
|
||||
|
||||
this.logDetail('Opening browser for OAuth...');
|
||||
|
||||
const result = await authLoginOAuth({ apiUrl: workspaceUrl });
|
||||
const result = await authLoginOAuth({ apiUrl: serverUrl });
|
||||
|
||||
if (result.success) {
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.setDefaultRemote(remoteName);
|
||||
this.logDetail(`Authenticated via OAuth to ${workspaceUrl}`);
|
||||
this.logDetail(`Authenticated via OAuth to ${serverUrl}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -620,7 +618,7 @@ export class CreateAppCommand {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` OAuth failed: ${result.error.message}\n` +
|
||||
` Run \`yarn twenty remote add --api-url ${workspaceUrl}\` manually.`,
|
||||
` Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -628,7 +626,7 @@ export class CreateAppCommand {
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` Authentication failed. Run \`yarn twenty remote add --api-url ${workspaceUrl}\` manually.`,
|
||||
` Authentication failed. Run \`yarn twenty remote:add --url ${serverUrl}\` manually.`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -638,7 +636,7 @@ export class CreateAppCommand {
|
||||
|
||||
private logSuccess(
|
||||
appDirectory: string,
|
||||
workspaceUrl: string,
|
||||
serverUrl: string,
|
||||
authSucceeded: boolean,
|
||||
): void {
|
||||
const dirName = basename(appDirectory);
|
||||
@@ -656,9 +654,7 @@ export class CreateAppCommand {
|
||||
if (!authSucceeded) {
|
||||
console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
' yarn twenty remote add --api-url <your-instance-url>\n',
|
||||
),
|
||||
chalk.cyan(' yarn twenty remote:add --url <your-instance-url>\n'),
|
||||
);
|
||||
stepNumber++;
|
||||
}
|
||||
@@ -668,7 +664,7 @@ export class CreateAppCommand {
|
||||
stepNumber++;
|
||||
|
||||
console.log(chalk.white(` ${stepNumber}. Open your twenty instance`));
|
||||
console.log(chalk.cyan(` ${workspaceUrl}\n`));
|
||||
console.log(chalk.cyan(` ${serverUrl}\n`));
|
||||
|
||||
console.log(
|
||||
chalk.gray(
|
||||
|
||||
@@ -17,6 +17,7 @@ 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';
|
||||
@@ -173,6 +174,27 @@ 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)');
|
||||
onProgress?.('Configuring dotfiles (.gitignore, .github, .yarnrc.yml)');
|
||||
await renameDotfiles({ appDirectory });
|
||||
|
||||
onProgress?.('Mirroring AGENTS.md to CLAUDE.md');
|
||||
@@ -47,6 +47,7 @@ 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) {
|
||||
|
||||
@@ -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 --workspace-url <your-instance-url>',
|
||||
' npx create-twenty-app@latest my-twenty-app --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 install # builds and installs once
|
||||
yarn twenty remote:add # same prompts as above
|
||||
yarn twenty app:install # builds and installs once
|
||||
```
|
||||
|
||||
## Configure authentication
|
||||
|
||||
@@ -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 switch # Switch 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:use # Set 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 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
|
||||
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
|
||||
```
|
||||
|
||||
## 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 server start\n' +
|
||||
'Start a local server: yarn twenty docker: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 server start\n' +
|
||||
'Start a local server: yarn twenty docker: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) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
throw new Error(
|
||||
`App uninstall failed during teardown: ${JSON.stringify(uninstallResult.error, null, 2)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import {
|
||||
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
@@ -36,5 +36,5 @@ export default defineRole({
|
||||
canUpdateFieldValue: true,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
});
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"dev": "twenty dev",
|
||||
"exec": "twenty exec",
|
||||
"uninstall": "twenty uninstall",
|
||||
"exec": "twenty dev:fn-exec",
|
||||
"uninstall": "twenty app:uninstall",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json ."
|
||||
},
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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 { PermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import { SystemPermissionFlag, defineRole } from 'twenty-sdk/define';
|
||||
import {
|
||||
CONTENT_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
@@ -36,5 +36,5 @@ export default defineRole({
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.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 switch # Switch 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:use # Set 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 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
|
||||
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
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
@@ -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 server start\n' +
|
||||
'Start a local server: yarn twenty docker: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 server start\n' +
|
||||
'Start a local server: yarn twenty docker: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 (deploy registers the app, install activates it):
|
||||
yarn twenty deploy
|
||||
yarn twenty install
|
||||
# Manual publish flow (publish registers the app, install activates it):
|
||||
yarn twenty app:publish --private
|
||||
yarn twenty app:install
|
||||
```
|
||||
|
||||
`twenty dev` is recommended for iteration — it publishes, installs, and
|
||||
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).
|
||||
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).
|
||||
|
||||
This serves as the reference implementation for Twenty's
|
||||
`defineConnectionProvider({ type: 'oauth' })` flow — useful as a template
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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=
|
||||
@@ -0,0 +1,42 @@
|
||||
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 }}
|
||||
@@ -0,0 +1,48 @@
|
||||
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 }}
|
||||
@@ -0,0 +1,43 @@
|
||||
# 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
|
||||
@@ -0,0 +1 @@
|
||||
24.5.0
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$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."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
@@ -0,0 +1,101 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "twenty-partners",
|
||||
"version": "0.3.1",
|
||||
"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: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"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-client-sdk": "2.4.0",
|
||||
"twenty-sdk": "2.4.0"
|
||||
},
|
||||
"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
@@ -0,0 +1,74 @@
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
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
@@ -0,0 +1,46 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
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,
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
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';
|
||||
@@ -0,0 +1,16 @@
|
||||
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
@@ -0,0 +1,18 @@
|
||||
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
@@ -0,0 +1,15 @@
|
||||
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
@@ -0,0 +1,10 @@
|
||||
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
@@ -0,0 +1,14 @@
|
||||
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
@@ -0,0 +1,11 @@
|
||||
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
@@ -0,0 +1,10 @@
|
||||
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
@@ -0,0 +1,26 @@
|
||||
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
@@ -0,0 +1,10 @@
|
||||
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
@@ -0,0 +1,14 @@
|
||||
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
@@ -0,0 +1,15 @@
|
||||
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' },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
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
@@ -0,0 +1,10 @@
|
||||
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,
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
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
@@ -0,0 +1,22 @@
|
||||
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
@@ -0,0 +1,22 @@
|
||||
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
@@ -0,0 +1,22 @@
|
||||
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
@@ -0,0 +1,18 @@
|
||||
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
@@ -0,0 +1,18 @@
|
||||
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
@@ -0,0 +1,18 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
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_ON_OPPORTUNITY_FIELD_ID = 'd9eeacaa-2f9e-44cc-b5f6-5e1526256d49';
|
||||
export const OPPORTUNITIES_ON_PARTNER_FIELD_ID = '8c04a5d4-c423-487e-bd78-7142a75b2896';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_ON_OPPORTUNITY_FIELD_ID,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partner',
|
||||
label: 'Partner',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: OPPORTUNITIES_ON_PARTNER_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'partnerId',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
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_ON_PERSON_FIELD_ID = 'b49eeaa3-c7ef-4a5c-8c47-d2c234b5122f';
|
||||
export const PERSONS_ON_PARTNER_FIELD_ID = '2c0e2035-db52-434b-9706-cd2210009a86';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_ON_PERSON_FIELD_ID,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partner',
|
||||
label: 'Partner',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PERSONS_ON_PARTNER_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'partnerId',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNERS_ON_COMPANY_FIELD_ID, PARTNER_COMPANY_FIELD_ID } from './partner-company.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNERS_ON_COMPANY_FIELD_ID,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partners',
|
||||
label: 'Partners',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_COMPANY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'c6862035-bf2e-42ea-86c4-bf46636f7859',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
type: FieldType.TEXT,
|
||||
name: 'discord',
|
||||
label: 'Discord',
|
||||
isNullable: true,
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { FieldType, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_ON_PERSON_FIELD_ID, PERSONS_ON_PARTNER_FIELD_ID } from './partner-on-person.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PERSONS_ON_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'persons',
|
||||
label: 'Persons',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_ON_PERSON_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
// Helpers
|
||||
async function createOpp(client: CoreApiClient, name: string) {
|
||||
const r = await client.mutation({
|
||||
createOpportunity: {
|
||||
__args: { data: { name } },
|
||||
id: true,
|
||||
matchStatus: true,
|
||||
},
|
||||
} as any);
|
||||
return (r as any).createOpportunity as { id: string; matchStatus: string };
|
||||
}
|
||||
|
||||
async function destroyOpp(client: CoreApiClient, id: string) {
|
||||
await client
|
||||
.mutation({ destroyOpportunity: { __args: { id }, id: true } } as any)
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async function getOpp(client: CoreApiClient, id: string) {
|
||||
const r = await client.query({
|
||||
opportunity: {
|
||||
__args: { filter: { id: { eq: id } } },
|
||||
id: true,
|
||||
matchStatus: true,
|
||||
partner: { id: true },
|
||||
},
|
||||
} as any);
|
||||
return (r as any).opportunity as {
|
||||
id: string;
|
||||
matchStatus: string;
|
||||
partner: { id: string } | null;
|
||||
};
|
||||
}
|
||||
|
||||
async function sleep(ms: number) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
describe('on-opportunity-auto-match (success path)', () => {
|
||||
let client: CoreApiClient;
|
||||
const created: string[] = [];
|
||||
let seededPartnerId: string | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
client = new CoreApiClient();
|
||||
// Ensure at least one ACTIVE+AVAILABLE partner exists for the function to pick.
|
||||
const existing = await client.query({
|
||||
partners: {
|
||||
__args: {
|
||||
filter: { validationStage: { eq: 'VALIDATED' }, availability: { eq: 'AVAILABLE' } },
|
||||
first: 1,
|
||||
},
|
||||
edges: { node: { id: true } },
|
||||
},
|
||||
} as any);
|
||||
if ((existing as any).partners.edges.length === 0) {
|
||||
const r = await client.mutation({
|
||||
createPartner: {
|
||||
__args: {
|
||||
data: {
|
||||
name: '[test] auto-match seed partner',
|
||||
slug: 'test-auto-match-seed',
|
||||
validationStage: 'VALIDATED',
|
||||
availability: 'AVAILABLE',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
seededPartnerId = (r as any).createPartner.id;
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (seededPartnerId) {
|
||||
await client
|
||||
.mutation({ destroyPartner: { __args: { id: seededPartnerId }, id: true } } as any)
|
||||
.catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
client = new CoreApiClient();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
for (const id of created) await destroyOpp(client, id);
|
||||
created.length = 0;
|
||||
});
|
||||
|
||||
it('defaults a new opportunity to TO_BE_MATCHED', async () => {
|
||||
const opp = await createOpp(client, `[test] default status ${Date.now()}`);
|
||||
created.push(opp.id);
|
||||
expect(opp.matchStatus).toBe('TO_BE_MATCHED');
|
||||
});
|
||||
|
||||
it('assigns a partner and flips to MATCHED when set to AUTO_MATCH', async () => {
|
||||
const opp = await createOpp(client, `[test] auto match ${Date.now()}`);
|
||||
created.push(opp.id);
|
||||
|
||||
await client.mutation({
|
||||
updateOpportunity: {
|
||||
__args: { id: opp.id, data: { matchStatus: 'AUTO_MATCH' } },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
// Logic function runs async (~2s per spec). Poll up to 10s.
|
||||
let final = await getOpp(client, opp.id);
|
||||
for (let i = 0; i < 20 && final.matchStatus === 'AUTO_MATCH'; i++) {
|
||||
await sleep(500);
|
||||
final = await getOpp(client, opp.id);
|
||||
}
|
||||
|
||||
expect(final.matchStatus).toBe('MATCHED');
|
||||
expect(final.partner?.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('on-opportunity-auto-match (failure path)', () => {
|
||||
let client: CoreApiClient;
|
||||
const createdOpps: string[] = [];
|
||||
const flippedPartners: Array<{ id: string; prevAvailability: string }> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
client = new CoreApiClient();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Restore partner availabilities first so other tests find a partner.
|
||||
for (const p of flippedPartners) {
|
||||
await client
|
||||
.mutation({
|
||||
updatePartner: {
|
||||
__args: { id: p.id, data: { availability: p.prevAvailability } },
|
||||
id: true,
|
||||
},
|
||||
} as any)
|
||||
.catch(() => {});
|
||||
}
|
||||
flippedPartners.length = 0;
|
||||
|
||||
for (const id of createdOpps) await destroyOpp(client, id);
|
||||
createdOpps.length = 0;
|
||||
});
|
||||
|
||||
it('hands off to MANUAL_MATCH with a Note when no partner is available', async () => {
|
||||
// Make every AVAILABLE partner UNAVAILABLE for the duration of this test.
|
||||
const all = await client.query({
|
||||
partners: {
|
||||
__args: {
|
||||
filter: { availability: { eq: 'AVAILABLE' } },
|
||||
first: 100,
|
||||
},
|
||||
edges: { node: { id: true, availability: true } },
|
||||
},
|
||||
} as any);
|
||||
|
||||
const edges = ((all as any)?.partners?.edges ?? []) as Array<{
|
||||
node: { id: string; availability: string };
|
||||
}>;
|
||||
|
||||
for (const e of edges) {
|
||||
flippedPartners.push({ id: e.node.id, prevAvailability: e.node.availability });
|
||||
await client.mutation({
|
||||
updatePartner: {
|
||||
__args: { id: e.node.id, data: { availability: 'UNAVAILABLE' } },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
|
||||
const opp = await createOpp(client, `[test] no-partner ${Date.now()}`);
|
||||
createdOpps.push(opp.id);
|
||||
|
||||
await client.mutation({
|
||||
updateOpportunity: {
|
||||
__args: { id: opp.id, data: { matchStatus: 'AUTO_MATCH' } },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
let final = await getOpp(client, opp.id);
|
||||
for (let i = 0; i < 20 && final.matchStatus === 'AUTO_MATCH'; i++) {
|
||||
await sleep(500);
|
||||
final = await getOpp(client, opp.id);
|
||||
}
|
||||
|
||||
expect(final.matchStatus).toBe('MANUAL_MATCH');
|
||||
expect(final.partner).toBeNull();
|
||||
|
||||
// Confirm a Note was attached.
|
||||
const notes = await client.query({
|
||||
noteTargets: {
|
||||
__args: {
|
||||
filter: { targetOpportunityId: { eq: opp.id } },
|
||||
first: 10,
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
note: { id: true, title: true, bodyV2: { markdown: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
const noteEdges = ((notes as any)?.noteTargets?.edges ?? []) as Array<{
|
||||
node: { note: { title: string; bodyV2: { markdown: string } } };
|
||||
}>;
|
||||
|
||||
const autoMatchNote = noteEdges.find((e) =>
|
||||
e.node.note.title.toLowerCase().includes('auto-match'),
|
||||
);
|
||||
expect(autoMatchNote).toBeDefined();
|
||||
expect(autoMatchNote!.node.note.bodyV2.markdown).toContain('No partners');
|
||||
});
|
||||
});
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
export const LIST_AVAILABLE_PARTNERS_LOGIC_FUNCTION_ID =
|
||||
'0f91164f-f492-41e8-9bb0-481be5a3d5b9';
|
||||
|
||||
type Partner = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
slug: string | null;
|
||||
introduction: string | null;
|
||||
languagesSpoken: string[] | null;
|
||||
deploymentExpertise: string[] | null;
|
||||
region: string[] | null;
|
||||
calendarLink: { primaryLinkUrl: string | null } | null;
|
||||
};
|
||||
|
||||
type ListAvailablePartnersResult =
|
||||
| { ok: true; count: number; partners: Partner[] }
|
||||
| { ok: false; reason: string };
|
||||
|
||||
const handler = async (): Promise<ListAvailablePartnersResult> => {
|
||||
try {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const result = await client.query({
|
||||
partners: {
|
||||
__args: {
|
||||
filter: {
|
||||
validationStage: { eq: 'VALIDATED' },
|
||||
availability: { eq: 'AVAILABLE' },
|
||||
},
|
||||
orderBy: [{ name: 'AscNullsLast' }],
|
||||
first: 100,
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
introduction: true,
|
||||
languagesSpoken: true,
|
||||
deploymentExpertise: true,
|
||||
region: true,
|
||||
calendarLink: { primaryLinkUrl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const partners = (
|
||||
(result?.partners?.edges ?? []) as Array<{ node: Partner }>
|
||||
).map((edge) => edge.node);
|
||||
|
||||
return { ok: true, count: partners.length, partners };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: LIST_AVAILABLE_PARTNERS_LOGIC_FUNCTION_ID,
|
||||
name: 'list-available-partners',
|
||||
description: 'Returns all partners with validationStage=VALIDATED and availability=AVAILABLE.',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/partners',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { DatabaseEventPayload, defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
import { ON_OPP_AUTO_MATCH_FN_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
// Fires when an opportunity's matchStatus is set to 'AUTO_MATCH'.
|
||||
// Happy path: picks the longest-idle ACTIVE+AVAILABLE partner, assigns it, flips to 'MATCHED'.
|
||||
// Failure path: no partner available → flips to 'MANUAL_MATCH' and creates an audit Note.
|
||||
const handler = async (payload: DatabaseEventPayload) => {
|
||||
const props = payload.properties as {
|
||||
after?: { id: string; matchStatus?: string; partnerId?: string | null };
|
||||
before?: { matchStatus?: string };
|
||||
updatedFields?: string[];
|
||||
};
|
||||
|
||||
if (!props.updatedFields?.includes('matchStatus')) return {};
|
||||
if (props.after?.matchStatus !== 'AUTO_MATCH') return {};
|
||||
if (props.before?.matchStatus === 'AUTO_MATCH') return {};
|
||||
if (props.after.partnerId) return {};
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const partnersResult = await client.query({
|
||||
partners: {
|
||||
__args: {
|
||||
filter: {
|
||||
validationStage: { eq: 'VALIDATED' },
|
||||
availability: { eq: 'AVAILABLE' },
|
||||
},
|
||||
orderBy: [{ lastMatchAt: 'AscNullsFirst' }],
|
||||
first: 1,
|
||||
},
|
||||
edges: { node: { id: true, lastMatchAt: true } },
|
||||
},
|
||||
} as any);
|
||||
|
||||
const topPartner = (partnersResult.partners as any).edges[0]?.node;
|
||||
if (!topPartner) {
|
||||
const noteBody =
|
||||
`Auto-match attempted ${new Date().toISOString()}.\n` +
|
||||
`No partners matched (status=ACTIVE, availability=AVAILABLE).\n` +
|
||||
`Status moved to Manual Match — pick a partner manually or ` +
|
||||
`update partner availability and retry by setting status back to Auto Match.`;
|
||||
|
||||
try {
|
||||
const noteResult = await client.mutation({
|
||||
createNote: {
|
||||
__args: { data: { title: 'Auto-match failed', bodyV2: { markdown: noteBody } } },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
const noteId = (noteResult as any).createNote.id as string;
|
||||
|
||||
await client.mutation({
|
||||
createNoteTarget: {
|
||||
__args: { data: { noteId, targetOpportunityId: props.after.id } },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
} catch {
|
||||
// Note creation is best-effort; status flip is the critical action.
|
||||
}
|
||||
|
||||
await client.mutation({
|
||||
updateOpportunity: {
|
||||
__args: { id: props.after.id, data: { matchStatus: 'MANUAL_MATCH' } },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
return { matched: false, reason: 'no_partner_available' };
|
||||
}
|
||||
|
||||
await client.mutation({
|
||||
updateOpportunity: {
|
||||
__args: {
|
||||
id: props.after.id,
|
||||
data: { partnerId: topPartner.id, matchStatus: 'MATCHED' },
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
await client.mutation({
|
||||
updatePartner: {
|
||||
__args: {
|
||||
id: topPartner.id,
|
||||
data: { lastMatchAt: new Date().toISOString() },
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
return { matched: true, partnerId: topPartner.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: ON_OPP_AUTO_MATCH_FN_UNIVERSAL_IDENTIFIER,
|
||||
name: 'on-opportunity-auto-match',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'opportunity.updated',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { InstallPayload, definePostInstallLogicFunction } from 'twenty-sdk/define';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const handler = async (_payload: InstallPayload) => {
|
||||
const client = new CoreApiClient();
|
||||
|
||||
const partnerResult = await client.mutation({
|
||||
createPartner: {
|
||||
__args: {
|
||||
data: {
|
||||
name: 'Test Partner Alpha',
|
||||
validationStage: 'VALIDATED',
|
||||
availability: 'AVAILABLE',
|
||||
languagesSpoken: ['ENGLISH', 'FRENCH'],
|
||||
deploymentExpertise: ['CLOUD', 'SELF_HOST'],
|
||||
region: 'EUROPE',
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const partnerId = (partnerResult.createPartner as any).id;
|
||||
|
||||
await client.mutation({
|
||||
createPerson: {
|
||||
__args: {
|
||||
data: {
|
||||
name: {
|
||||
firstName: 'Test Partner',
|
||||
lastName: 'Contact',
|
||||
},
|
||||
partnerId,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
return { seeded: true, partnerId };
|
||||
};
|
||||
|
||||
export default definePostInstallLogicFunction({
|
||||
universalIdentifier: 'f92bad2e-5905-4757-96ee-af9869d4ca0c',
|
||||
name: 'post-install',
|
||||
handler,
|
||||
shouldRunSynchronously: true,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
ALL_OPPORTUNITIES_NAV_UNIVERSAL_IDENTIFIER,
|
||||
ALL_OPPORTUNITIES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: ALL_OPPORTUNITIES_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconTargetArrow',
|
||||
position: 3,
|
||||
folderUniversalIdentifier: '0b2e499a-ae74-45e0-af08-243e19fc56aa',
|
||||
viewUniversalIdentifier: ALL_OPPORTUNITIES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
MATCHES_OVERVIEW_NAV_UNIVERSAL_IDENTIFIER,
|
||||
MATCHES_OVERVIEW_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: MATCHES_OVERVIEW_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconLayoutKanban',
|
||||
position: 1,
|
||||
folderUniversalIdentifier: '0b2e499a-ae74-45e0-af08-243e19fc56aa',
|
||||
viewUniversalIdentifier: MATCHES_OVERVIEW_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_APPLICATIONS_NAV_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_APPLICATIONS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: PARTNER_APPLICATIONS_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconUserPlus',
|
||||
position: 1,
|
||||
folderUniversalIdentifier: '857be3b5-82c6-45f7-b546-e20a8a97be8d',
|
||||
viewUniversalIdentifier: PARTNER_APPLICATIONS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_CONTENT_NAV_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_CONTENT_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: PARTNER_CONTENT_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconQuote',
|
||||
position: 3,
|
||||
folderUniversalIdentifier: '857be3b5-82c6-45f7-b546-e20a8a97be8d',
|
||||
viewUniversalIdentifier: PARTNER_CONTENT_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
ALL_MATCHED_DEALS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_DEALS_NAV_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: PARTNER_DEALS_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconHandshake',
|
||||
position: 2,
|
||||
folderUniversalIdentifier: '0b2e499a-ae74-45e0-af08-243e19fc56aa',
|
||||
viewUniversalIdentifier: ALL_MATCHED_DEALS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '857be3b5-82c6-45f7-b546-e20a8a97be8d',
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
name: 'Partners',
|
||||
icon: 'IconBuildingStore',
|
||||
color: 'purple',
|
||||
position: -1,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
PARTNERS_NAV_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: PARTNERS_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconBuildingStore',
|
||||
position: 0,
|
||||
folderUniversalIdentifier: '857be3b5-82c6-45f7-b546-e20a8a97be8d',
|
||||
viewUniversalIdentifier: ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '0b2e499a-ae74-45e0-af08-243e19fc56aa',
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
name: 'Pipeline',
|
||||
icon: 'IconTargetArrow',
|
||||
position: -2,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
VALIDATED_PARTNERS_NAV_UNIVERSAL_IDENTIFIER,
|
||||
VALIDATED_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: VALIDATED_PARTNERS_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconCircleCheck',
|
||||
position: 2,
|
||||
folderUniversalIdentifier: '857be3b5-82c6-45f7-b546-e20a8a97be8d',
|
||||
viewUniversalIdentifier: VALIDATED_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
WAITING_FOR_MATCH_NAV_UNIVERSAL_IDENTIFIER,
|
||||
WAITING_FOR_MATCH_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: WAITING_FOR_MATCH_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconClockHour4',
|
||||
position: 0,
|
||||
folderUniversalIdentifier: '0b2e499a-ae74-45e0-af08-243e19fc56aa',
|
||||
viewUniversalIdentifier: WAITING_FOR_MATCH_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { FieldType, defineObject } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: PARTNER_CONTENT_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'partnerContent',
|
||||
namePlural: 'partnerContents',
|
||||
labelSingular: 'Partner Content',
|
||||
labelPlural: 'Partner Content',
|
||||
description: 'Marketing content involving a partner: quotes, case studies, logos',
|
||||
icon: 'IconQuote',
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: '9e688624-83d2-4715-8b18-80492a6de2b6',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '9e688624-83d2-4715-8b18-80492a6de2b6',
|
||||
type: FieldType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
icon: 'IconTag',
|
||||
defaultValue: "''",
|
||||
},
|
||||
{
|
||||
universalIdentifier: '1d926e6e-6ac1-4d60-ab3d-a73114005692',
|
||||
type: FieldType.MULTI_SELECT,
|
||||
name: 'contentType',
|
||||
label: 'Content Type',
|
||||
icon: 'IconCategory',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '07f85e5b-0d70-416b-9884-256e469ed532', value: 'CUSTOMER_QUOTE', label: 'Customer quote', position: 0, color: 'blue' },
|
||||
{ id: '108b2358-d04d-4fdc-83df-a5978d39f66f', value: 'CASE_STUDY', label: 'Case study', position: 1, color: 'green' },
|
||||
{ id: 'eb45f371-f93c-4c45-9c8a-f29e1a58b7e4', value: 'PARTNER_QUOTE', label: 'Partner quote', position: 2, color: 'orange' },
|
||||
{ id: '3356c8a0-41cd-47c3-a293-5862138abc1a', value: 'LOGO', label: 'Logo', position: 3, color: 'purple' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0fe09c4-c1f4-4b96-93c6-d7ec38f1166a',
|
||||
type: FieldType.SELECT,
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
icon: 'IconProgressCheck',
|
||||
defaultValue: "'WIP'",
|
||||
options: [
|
||||
{ id: '5d41450b-8efb-41e8-81b7-b534429ec1b4', value: 'WIP', label: 'WIP', position: 0, color: 'gray' },
|
||||
{ id: '6611ae9a-4253-4638-9c47-d8bf2ec54368', value: 'INTERVIEW_SCHEDULED', label: 'Interview scheduled', position: 1, color: 'blue' },
|
||||
{ id: 'ae3d91e4-96ea-4687-b71f-d2d80088f28a', value: 'UNDER_CUSTOMER_PARTNER_REVIEW', label: 'Under review', position: 2, color: 'orange' },
|
||||
{ id: '91fe48a3-7950-4cdd-8c52-8d9b2cc03f0b', value: 'APPROVED', label: 'Approved', position: 3, color: 'green' },
|
||||
{ id: 'fa640b71-862e-4c64-80fa-37ab8b50d254', value: 'REJECTED', label: 'Rejected', position: 4, color: 'red' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'b52d263e-423e-40b0-b82c-29214597c005',
|
||||
type: FieldType.DATE_TIME,
|
||||
name: 'approvalDate',
|
||||
label: 'Approval Date',
|
||||
icon: 'IconCalendarCheck',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'da7e9094-e2c3-47d3-924f-a1d4d3c717ed',
|
||||
type: FieldType.LINKS,
|
||||
name: 'interview',
|
||||
label: 'Interview',
|
||||
icon: 'IconMicrophone',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'f303369e-288c-4a48-9920-c1de0ad9a159',
|
||||
type: FieldType.FILES,
|
||||
name: 'documents',
|
||||
label: 'Documents',
|
||||
icon: 'IconPaperclip',
|
||||
isNullable: true,
|
||||
universalSettings: { maxNumberOfValues: 10 },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,483 @@
|
||||
import { FieldType, defineObject } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'partner',
|
||||
namePlural: 'partners',
|
||||
labelSingular: 'Partner',
|
||||
labelPlural: 'Partners',
|
||||
description: 'A certified implementation partner',
|
||||
icon: 'IconBuildingStore',
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: 'a0000001-0000-4000-8000-000000000001',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'a0000001-0000-4000-8000-000000000001',
|
||||
type: FieldType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
icon: 'IconTag',
|
||||
defaultValue: "''",
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0000002-0000-4000-8000-000000000002',
|
||||
type: FieldType.TEXT,
|
||||
name: 'slug',
|
||||
label: 'Slug',
|
||||
icon: 'IconLink',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '2ca9856f-f54a-4326-9ff3-668fd7da0b50',
|
||||
type: FieldType.SELECT,
|
||||
name: 'validationStage',
|
||||
label: 'Validation Stage',
|
||||
icon: 'IconProgressCheck',
|
||||
defaultValue: "'APPLICATION'",
|
||||
options: [
|
||||
{ id: '1b4f8b0b-68ec-4c02-a7e6-fbd0cda2f3b9', value: 'APPLICATION', label: 'Application', position: 0, color: 'gray' },
|
||||
{ id: '901181af-b2cb-4052-9ac8-3c84281671f8', value: 'POTENTIAL', label: 'Potential partner', position: 1, color: 'blue' },
|
||||
{ id: '3ebbc425-e5e3-412d-93df-5f70fefa1798', value: 'VALIDATED', label: 'Validated partner', position: 2, color: 'green' },
|
||||
{ id: '3d0ec027-0d51-4df7-932e-13c60d4b4382', value: 'FORMER', label: 'Former partner', position: 3, color: 'orange' },
|
||||
{ id: '19dc4929-735a-4b64-8f57-f34f3eaa3a73', value: 'REJECTED', label: 'Rejected', position: 4, color: 'red' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5af4e57e-7fa7-4c4f-b40f-37549361459a',
|
||||
type: FieldType.BOOLEAN,
|
||||
name: 'reviewed',
|
||||
label: 'Reviewed',
|
||||
icon: 'IconChecks',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5412e4ca-cc96-4be8-8652-b73dace7673b',
|
||||
type: FieldType.RATING,
|
||||
name: 'ranking',
|
||||
label: 'Ranking',
|
||||
icon: 'IconStar',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: 'e4c784f0-7e2b-4eca-94dc-fc447266f252', value: 'RATING_1', label: '1', position: 0 },
|
||||
{ id: '79d033bd-4189-4f69-a9d1-a54d7cbbc5bc', value: 'RATING_2', label: '2', position: 1 },
|
||||
{ id: '1206bdcc-a804-4649-a3ce-bd001a3abc2c', value: 'RATING_3', label: '3', position: 2 },
|
||||
{ id: '1c195738-d823-415e-b4db-44a6cdc09ec5', value: 'RATING_4', label: '4', position: 3 },
|
||||
{ id: '92cd3a72-8d5b-4077-a369-ddfe429aa2aa', value: 'RATING_5', label: '5', position: 4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'd4fa6461-37b6-49ee-9181-dd482e74a70b',
|
||||
type: FieldType.SELECT,
|
||||
name: 'partnerTier',
|
||||
label: 'Partner Tier',
|
||||
icon: 'IconMedal',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '63806cc0-02a8-4fe9-b2ba-0efe43a33109', value: 'NEW', label: 'New', position: 0, color: 'gray' },
|
||||
{ id: '1ea6f5ba-6a4a-4e64-9b56-11e9ae56c97e', value: 'INTERMEDIATE', label: 'Intermediate', position: 1, color: 'blue' },
|
||||
{ id: 'b44de8b4-caea-4ca6-94cd-fb67221b5fdb', value: 'ADVANCED', label: 'Advanced', position: 2, color: 'green' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: '500021ad-ca42-4fd3-8727-392dd26b722a',
|
||||
type: FieldType.MULTI_SELECT,
|
||||
name: 'partnerScope',
|
||||
label: 'Partner Scope',
|
||||
icon: 'IconListCheck',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '00f05e1c-0fd9-4214-a461-554b7c9e7eb5', value: 'APPS', label: 'Apps', position: 0, color: 'blue' },
|
||||
{ id: 'e0f789d5-a3bc-4ecb-8bc2-5aa018468d83', value: 'DATA_MODEL', label: 'Data model', position: 1, color: 'green' },
|
||||
{ id: 'c88bf189-8be4-4431-aa03-85928f8b2a52', value: 'DATA_MIGRATION', label: 'Data migration', position: 2, color: 'turquoise' },
|
||||
{ id: 'a7fd9429-c26f-49ab-bf52-4d591f5ca7a0', value: 'HOSTING_ENVIRONMENT', label: 'Hosting environment', position: 3, color: 'purple' },
|
||||
{ id: '9e416a39-05c6-4c80-9f36-99b5b60c26ec', value: 'WORKFLOWS', label: 'Workflows', position: 4, color: 'orange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a451e557-a488-470a-8b35-6f9b8cfb1a10',
|
||||
type: FieldType.SELECT,
|
||||
name: 'typeOfTeam',
|
||||
label: 'Type of Team',
|
||||
icon: 'IconUsersGroup',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '0f59b781-5a81-4025-92ad-4b93b2c62df0', value: 'SOLO', label: 'Solo', position: 0, color: 'blue' },
|
||||
{ id: 'db47bc44-93db-4cbf-8d0f-3057cf01d11e', value: 'AGENCY', label: 'Agency', position: 1, color: 'green' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: '6f260cc0-a860-41e6-ad40-a2ef32ecffbe',
|
||||
type: FieldType.ARRAY,
|
||||
name: 'skills',
|
||||
label: 'Skills',
|
||||
icon: 'IconTags',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0000004-0000-4000-8000-000000000004',
|
||||
type: FieldType.SELECT,
|
||||
name: 'availability',
|
||||
label: 'Availability',
|
||||
icon: 'IconCalendarCheck',
|
||||
defaultValue: "'AVAILABLE'",
|
||||
options: [
|
||||
{ id: '639d74c3-5d38-407b-b0e4-81f3930db451', value: 'AVAILABLE', label: 'Available', position: 0, color: 'green' },
|
||||
{ id: '6a921479-559a-4446-892a-d4a1b5a3abb4', value: 'UNAVAILABLE', label: 'Unavailable', position: 1, color: 'gray' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0000005-0000-4000-8000-000000000005',
|
||||
type: FieldType.MULTI_SELECT,
|
||||
name: 'deploymentExpertise',
|
||||
label: 'Deployment Expertise',
|
||||
icon: 'IconCloud',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: 'f2f53365-d909-4a97-bb81-efab43f0a17e', value: 'CLOUD', label: 'Cloud', position: 0, color: 'sky' },
|
||||
{ id: '5e47226b-4e60-413f-80ee-b8f1d9ee6ad6', value: 'SELF_HOST', label: 'Self-host', position: 1, color: 'purple' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0000007-0000-4000-8000-000000000007',
|
||||
type: FieldType.MULTI_SELECT,
|
||||
name: 'languagesSpoken',
|
||||
label: 'Languages Spoken',
|
||||
icon: 'IconLanguage',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '3fe94a92-5f64-46f4-8697-1a44e06e939f', value: 'ENGLISH', label: 'English', position: 0, color: 'blue' },
|
||||
{ id: 'fdfe8adf-b2f1-4ef9-97c1-449302932698', value: 'FRENCH', label: 'French', position: 1, color: 'red' },
|
||||
{ id: '820c120a-17ba-4e9a-8dd7-9724ac41d7ca', value: 'GERMAN', label: 'German', position: 2, color: 'yellow' },
|
||||
{ id: '28171507-753d-4a8e-b9c2-44c698c2a26b', value: 'CHINESE', label: 'Chinese', position: 3, color: 'orange' },
|
||||
{ id: 'd9111720-26c1-47c3-85bb-a30699737cce', value: 'SPANISH', label: 'Spanish', position: 4, color: 'green' },
|
||||
{ id: '7dbfcb38-4255-4fe8-91dc-ce0686191521', value: 'ARABIC', label: 'Arabic', position: 5, color: 'sky' },
|
||||
{ id: 'd4e0e54b-2c57-498a-9ccc-9cfe64f09a36', value: 'BENGALI', label: 'Bengali', position: 6, color: 'purple' },
|
||||
{ id: '34e765d2-5b03-4a63-a6e6-6c3c75fd8d75', value: 'CATALAN', label: 'Catalan', position: 7, color: 'pink' },
|
||||
{ id: 'b042b1d3-19d6-4094-b53e-85f9727360b1', value: 'CZECH', label: 'Czech', position: 8, color: 'turquoise' },
|
||||
{ id: '0a8ebe74-dcfa-44f5-abde-1af3a8dd8879', value: 'DANISH', label: 'Danish', position: 9, color: 'gray' },
|
||||
{ id: '385e0db5-ab16-4a59-94fd-98921fa367d5', value: 'DUTCH', label: 'Dutch', position: 10, color: 'blue' },
|
||||
{ id: 'f8ec118f-6652-4fe4-a7dc-a77c079f473e', value: 'FARSI', label: 'Farsi', position: 11, color: 'red' },
|
||||
{ id: 'cd58db19-6be6-4deb-8f1b-4cd9bdbcacdf', value: 'FINNISH', label: 'Finnish', position: 12, color: 'yellow' },
|
||||
{ id: 'c20be129-8a93-479c-99f1-89dad2177c0a', value: 'GREEK', label: 'Greek', position: 13, color: 'orange' },
|
||||
{ id: '7faaec05-22d1-4b37-a700-9dc4733effb6', value: 'HINDI', label: 'Hindi', position: 14, color: 'green' },
|
||||
{ id: '3400d7db-312f-4d54-ac19-04a2f6fed6e0', value: 'INDONESIAN', label: 'Indonesian', position: 15, color: 'sky' },
|
||||
{ id: 'd1eb3870-2938-4c87-9596-3980b31df293', value: 'ITALIAN', label: 'Italian', position: 16, color: 'purple' },
|
||||
{ id: '7a854dd9-6c81-4d16-8b76-79b3571638ec', value: 'JAPANESE', label: 'Japanese', position: 17, color: 'pink' },
|
||||
{ id: '51be3b14-195c-45e2-815e-a6591d0946b1', value: 'KOREAN', label: 'Korean', position: 18, color: 'turquoise' },
|
||||
{ id: 'c31168a0-276c-45dc-bd39-a498d39a44d8', value: 'MALAY', label: 'Malay', position: 19, color: 'gray' },
|
||||
{ id: '7719d197-9e91-41d3-a66f-728cfce32c33', value: 'NORWEGIAN', label: 'Norwegian', position: 20, color: 'blue' },
|
||||
{ id: '115c5e11-2462-4371-92ca-c4835649de15', value: 'POLISH', label: 'Polish', position: 21, color: 'red' },
|
||||
{ id: '018e3663-3de8-4110-aceb-48c8083ebb9a', value: 'PORTUGUESE', label: 'Portuguese', position: 22, color: 'yellow' },
|
||||
{ id: 'ca97987f-4f43-43d0-b7a0-490bc566b42f', value: 'PUNJABI', label: 'Punjabi', position: 23, color: 'orange' },
|
||||
{ id: '05215653-4d32-49e2-9ff3-73e66e14d97d', value: 'ROMANIAN', label: 'Romanian', position: 24, color: 'green' },
|
||||
{ id: 'a14e341e-a266-407f-b4c7-936cfa608500', value: 'RUSSIAN', label: 'Russian', position: 25, color: 'sky' },
|
||||
{ id: '1190a9eb-bd06-4d77-ab59-c619f4e2faa3', value: 'SWAHILI', label: 'Swahili', position: 26, color: 'purple' },
|
||||
{ id: '0fedffb0-c0d7-47b7-ac5f-0b166d24d670', value: 'SWEDISH', label: 'Swedish', position: 27, color: 'pink' },
|
||||
{ id: '8205bc44-b2a0-4211-9fe5-1bc64efaa844', value: 'TAGALOG', label: 'Tagalog', position: 28, color: 'turquoise' },
|
||||
{ id: '866b6438-c172-4c72-b69e-b1ca89342526', value: 'TAMIL', label: 'Tamil', position: 29, color: 'gray' },
|
||||
{ id: 'a2c4851b-302b-4420-aff2-6d7dc614fa6c', value: 'THAI', label: 'Thai', position: 30, color: 'blue' },
|
||||
{ id: '0a5672dd-ebe5-48bc-b8ab-7d2bf6a605e2', value: 'TURKISH', label: 'Turkish', position: 31, color: 'red' },
|
||||
{ id: '36324fb0-5ea2-4e9f-845c-ac65b977ab20', value: 'UKRAINIAN', label: 'Ukrainian', position: 32, color: 'yellow' },
|
||||
{ id: 'b1aad243-fc85-4df3-a83e-07796ae01d6c', value: 'URDU', label: 'Urdu', position: 33, color: 'orange' },
|
||||
{ id: '922895f8-3a8a-45b8-a70e-0ae7b0c8ff32', value: 'VIETNAMESE', label: 'Vietnamese', position: 34, color: 'green' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: '0a1ce916-4df5-469a-b793-30f19e45b38d',
|
||||
type: FieldType.TEXT,
|
||||
name: 'city',
|
||||
label: 'City',
|
||||
icon: 'IconMapPin',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a77d7fa6-c398-47db-af0f-036a5c719f20',
|
||||
type: FieldType.SELECT,
|
||||
name: 'country',
|
||||
label: 'Country',
|
||||
icon: 'IconFlag',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '03332728-50b1-49ec-bdec-a938cb97ca74', value: 'AFGHANISTAN', label: 'Afghanistan 🇦🇫', position: 0, color: 'blue' },
|
||||
{ id: 'c39c7baa-6f52-4349-bc28-97b59f2a9a3a', value: 'ALBANIA', label: 'Albania 🇦🇱', position: 1, color: 'turquoise' },
|
||||
{ id: '1bce0427-d693-451a-9a6b-a50c0c502c85', value: 'ALGERIA', label: 'Algeria 🇩🇿', position: 2, color: 'green' },
|
||||
{ id: '634fb580-4f12-4a5b-b43c-5787330d4b60', value: 'ANDORRA', label: 'Andorra 🇦🇩', position: 3, color: 'sky' },
|
||||
{ id: '545797b9-16df-40d5-ab9b-037d3d875263', value: 'ANGOLA', label: 'Angola 🇦🇴', position: 4, color: 'purple' },
|
||||
{ id: '83b58c61-8b0d-42e1-9d4f-9a530ace7899', value: 'ANTIGUA_AND_BARBUDA', label: 'Antigua & Barbuda 🇦🇬', position: 5, color: 'orange' },
|
||||
{ id: '58fd4e0d-b48d-4b22-81d8-de6db6bd22ed', value: 'ARGENTINA', label: 'Argentina 🇦🇷', position: 6, color: 'yellow' },
|
||||
{ id: '7091a2e3-2095-4693-bd53-366df4ba5c75', value: 'ARMENIA', label: 'Armenia 🇦🇲', position: 7, color: 'red' },
|
||||
{ id: '479725e7-f185-46c6-9e8b-605fbbb392f8', value: 'AUSTRALIA', label: 'Australia 🇦🇺', position: 8, color: 'pink' },
|
||||
{ id: 'b7993e1b-5f7d-47b9-a84d-41795160393a', value: 'AUSTRIA', label: 'Austria 🇦🇹', position: 9, color: 'gray' },
|
||||
{ id: 'e2d324df-2baa-49a2-a016-2c7cab0d0e95', value: 'AZERBAIJAN', label: 'Azerbaijan 🇦🇿', position: 10, color: 'blue' },
|
||||
{ id: '87dc4b54-c4f7-4813-a0e4-d2214e659a77', value: 'BAHAMAS', label: 'Bahamas 🇧🇸', position: 11, color: 'turquoise' },
|
||||
{ id: '32149c4f-3319-42a6-a0ca-6afb42bdf3b9', value: 'BAHRAIN', label: 'Bahrain 🇧🇭', position: 12, color: 'green' },
|
||||
{ id: 'c74d1f90-e17f-4e9a-9341-5c6ffda2d3ac', value: 'BANGLADESH', label: 'Bangladesh 🇧🇩', position: 13, color: 'sky' },
|
||||
{ id: '61c2e8c4-06af-4daa-ac52-34f4b47e2aad', value: 'BARBADOS', label: 'Barbados 🇧🇧', position: 14, color: 'purple' },
|
||||
{ id: '1c7dfa46-72e0-4ba3-8af5-3b2d1d46fd4b', value: 'BELARUS', label: 'Belarus 🇧🇾', position: 15, color: 'orange' },
|
||||
{ id: '771258e5-b16f-47d4-aaed-47a4c931b72d', value: 'BELGIUM', label: 'Belgium 🇧🇪', position: 16, color: 'yellow' },
|
||||
{ id: 'f3910b68-3f3f-4afb-9f90-5561376e5098', value: 'BELIZE', label: 'Belize 🇧🇿', position: 17, color: 'red' },
|
||||
{ id: 'b06f1bc7-2058-44fc-b868-631559de7d11', value: 'BENIN', label: 'Benin 🇧🇯', position: 18, color: 'pink' },
|
||||
{ id: 'acff20b8-b865-47de-9049-c660866f5218', value: 'BHUTAN', label: 'Bhutan 🇧🇹', position: 19, color: 'gray' },
|
||||
{ id: 'bee20647-c283-40af-9733-37753e42b464', value: 'BOLIVIA', label: 'Bolivia 🇧🇴', position: 20, color: 'blue' },
|
||||
{ id: '4dd91769-c713-47af-bf48-aed0251115b1', value: 'BOSNIA_AND_HERZEGOVINA', label: 'Bosnia & Herzegovina 🇧🇦', position: 21, color: 'turquoise' },
|
||||
{ id: 'a6997427-007f-42b4-b6ea-892f7ce58dff', value: 'BOTSWANA', label: 'Botswana 🇧🇼', position: 22, color: 'green' },
|
||||
{ id: '3ff1f170-d3f0-4ce6-9490-bfc20aadf470', value: 'BRAZIL', label: 'Brazil 🇧🇷', position: 23, color: 'sky' },
|
||||
{ id: '7b2d6911-4676-433d-93f1-6eca2530d89d', value: 'BRUNEI', label: 'Brunei 🇧🇳', position: 24, color: 'purple' },
|
||||
{ id: '767c72f3-f951-4b00-958d-856d2a8bda7a', value: 'BULGARIA', label: 'Bulgaria 🇧🇬', position: 25, color: 'orange' },
|
||||
{ id: 'e16ce6af-86b0-4448-8008-c5efa2ce3fe9', value: 'BURKINA_FASO', label: 'Burkina Faso 🇧🇫', position: 26, color: 'yellow' },
|
||||
{ id: 'dd7e7b2a-8367-482d-99d4-7877d883c2e3', value: 'BURUNDI', label: 'Burundi 🇧🇮', position: 27, color: 'red' },
|
||||
{ id: 'd9d6eea9-7724-4c65-be48-e861075121f1', value: 'CAMBODIA', label: 'Cambodia 🇰🇭', position: 28, color: 'pink' },
|
||||
{ id: '49178472-0bdd-4797-be6c-e0a1ac6e4147', value: 'CAMEROON', label: 'Cameroon 🇨🇲', position: 29, color: 'gray' },
|
||||
{ id: '849b6361-a83b-48fb-a3e5-2ac6e9089c7f', value: 'CANADA', label: 'Canada 🇨🇦', position: 30, color: 'blue' },
|
||||
{ id: 'ca22a230-01b1-4099-b7c2-29fd3b8d09de', value: 'CAPE_VERDE', label: 'Cape Verde 🇨🇻', position: 31, color: 'turquoise' },
|
||||
{ id: '360bef1e-34a9-4467-9ffe-5ebeb6427711', value: 'CENTRAL_AFRICAN_REPUBLIC', label: 'Central African Republic 🇨🇫', position: 32, color: 'green' },
|
||||
{ id: 'deba0ee2-3027-4646-afc6-24410bf68198', value: 'CHAD', label: 'Chad 🇹🇩', position: 33, color: 'sky' },
|
||||
{ id: 'd0bf04d5-a40c-45f0-8e82-4d5905c00131', value: 'CHILE', label: 'Chile 🇨🇱', position: 34, color: 'purple' },
|
||||
{ id: '60522b37-dded-4f18-a25b-05b51a8c9aa7', value: 'CHINA', label: 'China 🇨🇳', position: 35, color: 'orange' },
|
||||
{ id: '3b2c58f1-4efc-4c0f-9d43-f3a5b5b577cf', value: 'COLOMBIA', label: 'Colombia 🇨🇴', position: 36, color: 'yellow' },
|
||||
{ id: 'cbea73ba-8a9d-47ee-a38b-b7c3d89d3922', value: 'COMOROS', label: 'Comoros 🇰🇲', position: 37, color: 'red' },
|
||||
{ id: '895ed0d4-6a00-482a-840e-44edef034d2b', value: 'CONGO', label: 'Congo 🇨🇬', position: 38, color: 'pink' },
|
||||
{ id: '13db3209-398c-474c-b5f9-269ccf27adc5', value: 'DR_CONGO', label: 'DR Congo 🇨🇩', position: 39, color: 'gray' },
|
||||
{ id: '150c4227-ce67-420b-bb6c-82d5a4fb9c70', value: 'COSTA_RICA', label: 'Costa Rica 🇨🇷', position: 40, color: 'blue' },
|
||||
{ id: 'ad1853e2-753d-4245-b9a7-88a29281ca0a', value: 'CROATIA', label: 'Croatia 🇭🇷', position: 41, color: 'turquoise' },
|
||||
{ id: 'e42cad17-f7ea-450b-8c7f-a0ac0dc31dac', value: 'CUBA', label: 'Cuba 🇨🇺', position: 42, color: 'green' },
|
||||
{ id: '9035a680-5962-48c3-8d79-ee805a2c8b1c', value: 'CYPRUS', label: 'Cyprus 🇨🇾', position: 43, color: 'sky' },
|
||||
{ id: 'facb14c9-40e9-47f6-bc4b-b29178761210', value: 'CZECH_REPUBLIC', label: 'Czech Republic 🇨🇿', position: 44, color: 'purple' },
|
||||
{ id: '47026695-ee4b-47f2-97c1-9d1ea9854cd5', value: 'DENMARK', label: 'Denmark 🇩🇰', position: 45, color: 'orange' },
|
||||
{ id: '335e3877-1f8b-4f2b-a409-ec74596dd779', value: 'DJIBOUTI', label: 'Djibouti 🇩🇯', position: 46, color: 'yellow' },
|
||||
{ id: 'ee17957f-f692-495b-a664-61284fa723f2', value: 'DOMINICA', label: 'Dominica 🇩🇲', position: 47, color: 'red' },
|
||||
{ id: '4114c313-27d7-4f5a-a381-38daca143642', value: 'DOMINICAN_REPUBLIC', label: 'Dominican Republic 🇩🇴', position: 48, color: 'pink' },
|
||||
{ id: 'ffcfae44-e1b7-4739-87dd-0d957062646d', value: 'ECUADOR', label: 'Ecuador 🇪🇨', position: 49, color: 'gray' },
|
||||
{ id: '1f4e8ee4-3be1-42cd-96d4-3e5686dee153', value: 'EGYPT', label: 'Egypt 🇪🇬', position: 50, color: 'blue' },
|
||||
{ id: 'cf98813f-2a7e-4426-b97f-d3f1ac044bb8', value: 'EL_SALVADOR', label: 'El Salvador 🇸🇻', position: 51, color: 'turquoise' },
|
||||
{ id: '0e787d6e-e864-4ee3-b4c7-20fa873e8649', value: 'EQUATORIAL_GUINEA', label: 'Equatorial Guinea 🇬🇶', position: 52, color: 'green' },
|
||||
{ id: 'f68b6554-4ed5-4161-933f-a321b41391ea', value: 'ERITREA', label: 'Eritrea 🇪🇷', position: 53, color: 'sky' },
|
||||
{ id: '7a4387dc-198c-46db-9536-7bc73de4e1b0', value: 'ESTONIA', label: 'Estonia 🇪🇪', position: 54, color: 'purple' },
|
||||
{ id: '7e0fec86-e04f-499c-a814-72ac9b4cc5ce', value: 'ESWATINI', label: 'Eswatini 🇸🇿', position: 55, color: 'orange' },
|
||||
{ id: 'a7768d57-5bf2-401e-a82d-ec36e3f3a419', value: 'ETHIOPIA', label: 'Ethiopia 🇪🇹', position: 56, color: 'yellow' },
|
||||
{ id: '1b9e15c3-4fa5-4cd8-9bfb-8262518665fa', value: 'FIJI', label: 'Fiji 🇫🇯', position: 57, color: 'red' },
|
||||
{ id: '828a6fe2-3c50-4da3-97c9-11f3b80fbb0d', value: 'FINLAND', label: 'Finland 🇫🇮', position: 58, color: 'pink' },
|
||||
{ id: '03a1c597-65cf-440f-a20d-9d6bef0540c2', value: 'FRANCE', label: 'France 🇫🇷', position: 59, color: 'gray' },
|
||||
{ id: 'ab2edd1f-6353-4de8-863f-4b28e774be8e', value: 'GABON', label: 'Gabon 🇬🇦', position: 60, color: 'blue' },
|
||||
{ id: '12a50b82-c2f8-4d0b-b5f4-e52dfe01ba13', value: 'GAMBIA', label: 'Gambia 🇬🇲', position: 61, color: 'turquoise' },
|
||||
{ id: '2adf673e-8367-4eaf-83f0-41199718f0ee', value: 'GEORGIA', label: 'Georgia 🇬🇪', position: 62, color: 'green' },
|
||||
{ id: 'bd61021e-c25b-453e-b0cc-afb3942b063f', value: 'GERMANY', label: 'Germany 🇩🇪', position: 63, color: 'sky' },
|
||||
{ id: '1c45b7ae-9489-4714-b21e-8c717e6599ab', value: 'GHANA', label: 'Ghana 🇬🇭', position: 64, color: 'purple' },
|
||||
{ id: 'cc47d864-cd78-4cdd-a711-d679f0cbc133', value: 'GREECE', label: 'Greece 🇬🇷', position: 65, color: 'orange' },
|
||||
{ id: '2a2c3ca6-611e-49a4-9639-e8f60172c1b3', value: 'GRENADA', label: 'Grenada 🇬🇩', position: 66, color: 'yellow' },
|
||||
{ id: 'da2024e5-a1a0-49ab-8cf0-0d9e3f10ff9d', value: 'GUATEMALA', label: 'Guatemala 🇬🇹', position: 67, color: 'red' },
|
||||
{ id: '086c9969-1911-4aa4-8d67-3abdc12b2ce7', value: 'GUINEA', label: 'Guinea 🇬🇳', position: 68, color: 'pink' },
|
||||
{ id: 'a648d1d3-4d3f-4fc6-a1ea-40f98283df75', value: 'GUINEA_BISSAU', label: 'Guinea-Bissau 🇬🇼', position: 69, color: 'gray' },
|
||||
{ id: 'b567e119-4dcb-4f8a-b012-febe72262d85', value: 'GUYANA', label: 'Guyana 🇬🇾', position: 70, color: 'blue' },
|
||||
{ id: '26801d38-c0e6-468a-8c78-a0aed06c16fd', value: 'HAITI', label: 'Haiti 🇭🇹', position: 71, color: 'turquoise' },
|
||||
{ id: '2e31c5ff-da0a-4c82-b7bc-7a8c18b54f1e', value: 'HONDURAS', label: 'Honduras 🇭🇳', position: 72, color: 'green' },
|
||||
{ id: 'a78bd322-ac40-4ab3-a46f-6204e42b0712', value: 'HUNGARY', label: 'Hungary 🇭🇺', position: 73, color: 'sky' },
|
||||
{ id: '0026e951-225d-4e8c-8e64-289ead339bd3', value: 'ICELAND', label: 'Iceland 🇮🇸', position: 74, color: 'purple' },
|
||||
{ id: 'f5101d7d-9b81-4d2e-a9aa-e20dfab52c58', value: 'INDIA', label: 'India 🇮🇳', position: 75, color: 'orange' },
|
||||
{ id: '11b30046-58f3-42e5-a3e2-5d4761675056', value: 'INDONESIA', label: 'Indonesia 🇮🇩', position: 76, color: 'yellow' },
|
||||
{ id: '7345db23-db33-45cb-b8cc-e4d4a1f1b90e', value: 'IRAN', label: 'Iran 🇮🇷', position: 77, color: 'red' },
|
||||
{ id: '899d0eb1-9f04-4a88-b7f3-351e998b5979', value: 'IRAQ', label: 'Iraq 🇮🇶', position: 78, color: 'pink' },
|
||||
{ id: 'e45e8143-14c1-4184-84db-c11212cb8e25', value: 'IRELAND', label: 'Ireland 🇮🇪', position: 79, color: 'gray' },
|
||||
{ id: 'a208dac3-455e-41ed-9db0-aa7352d420ab', value: 'ISRAEL', label: 'Israel 🇮🇱', position: 80, color: 'blue' },
|
||||
{ id: '4537862d-0565-47a0-8759-ef522996881f', value: 'ITALY', label: 'Italy 🇮🇹', position: 81, color: 'turquoise' },
|
||||
{ id: '988630a1-dd19-476b-a0dc-cf8904ec5f68', value: 'IVORY_COAST', label: 'Ivory Coast 🇨🇮', position: 82, color: 'green' },
|
||||
{ id: '30c755e0-61f2-4590-9969-f3150c3b5aff', value: 'JAMAICA', label: 'Jamaica 🇯🇲', position: 83, color: 'sky' },
|
||||
{ id: '9ca39ab6-e0fb-4bbb-80bc-cbffa48682da', value: 'JAPAN', label: 'Japan 🇯🇵', position: 84, color: 'purple' },
|
||||
{ id: 'f71aefd9-1dc0-4370-8a31-2a329d8ab15d', value: 'JORDAN', label: 'Jordan 🇯🇴', position: 85, color: 'orange' },
|
||||
{ id: '7f230c15-2745-4e78-ab35-8ae5bec7bcc6', value: 'KAZAKHSTAN', label: 'Kazakhstan 🇰🇿', position: 86, color: 'yellow' },
|
||||
{ id: '9e745903-4ac7-432f-bfd2-d0d78e76f3e9', value: 'KENYA', label: 'Kenya 🇰🇪', position: 87, color: 'red' },
|
||||
{ id: 'ee448c0f-8b3b-4666-bbbb-19e8b62ecbcc', value: 'KIRIBATI', label: 'Kiribati 🇰🇮', position: 88, color: 'pink' },
|
||||
{ id: '6de78d0a-7a2b-48bb-97b5-2a86c8bab3b6', value: 'KOSOVO', label: 'Kosovo 🇽🇰', position: 89, color: 'gray' },
|
||||
{ id: '48e160eb-1fdf-4456-99f7-4e97e7d16cfa', value: 'KUWAIT', label: 'Kuwait 🇰🇼', position: 90, color: 'blue' },
|
||||
{ id: '7d318293-6378-4a02-af49-23d48d887a8f', value: 'KYRGYZSTAN', label: 'Kyrgyzstan 🇰🇬', position: 91, color: 'turquoise' },
|
||||
{ id: 'dc540210-83bb-40c5-ab0d-585f4648df1b', value: 'LAOS', label: 'Laos 🇱🇦', position: 92, color: 'green' },
|
||||
{ id: 'fe2c5828-330f-45b8-991c-180723d5bed6', value: 'LATVIA', label: 'Latvia 🇱🇻', position: 93, color: 'sky' },
|
||||
{ id: 'cff1390a-7548-401b-bfa1-5e062e1e2cd3', value: 'LEBANON', label: 'Lebanon 🇱🇧', position: 94, color: 'purple' },
|
||||
{ id: '2a1d40fd-a633-4bb0-890a-1009a2126ff9', value: 'LESOTHO', label: 'Lesotho 🇱🇸', position: 95, color: 'orange' },
|
||||
{ id: 'dee0303d-11c9-4bf9-913b-c5fedfc93766', value: 'LIBERIA', label: 'Liberia 🇱🇷', position: 96, color: 'yellow' },
|
||||
{ id: '57713732-bd4e-4888-a4e3-ad7761e76dd9', value: 'LIBYA', label: 'Libya 🇱🇾', position: 97, color: 'red' },
|
||||
{ id: '5ed4e5fd-31dc-4adc-a875-b5036ef5eac6', value: 'LIECHTENSTEIN', label: 'Liechtenstein 🇱🇮', position: 98, color: 'pink' },
|
||||
{ id: '8facad69-2b22-47a4-a324-75773bd7d4f8', value: 'LITHUANIA', label: 'Lithuania 🇱🇹', position: 99, color: 'gray' },
|
||||
{ id: 'fb1b4543-7f22-4c44-a953-7c68e2790e27', value: 'LUXEMBOURG', label: 'Luxembourg 🇱🇺', position: 100, color: 'blue' },
|
||||
{ id: '80009115-d98c-4e63-8401-285f82d94921', value: 'MADAGASCAR', label: 'Madagascar 🇲🇬', position: 101, color: 'turquoise' },
|
||||
{ id: 'b32ed1f2-3d12-4255-a39a-3a5ab64b425c', value: 'MALAWI', label: 'Malawi 🇲🇼', position: 102, color: 'green' },
|
||||
{ id: 'd7c744b0-23d2-4dd4-a9f9-3298cfce0d1b', value: 'MALAYSIA', label: 'Malaysia 🇲🇾', position: 103, color: 'sky' },
|
||||
{ id: 'a878865c-a73f-47d7-bf52-29c1b7b947d7', value: 'MALDIVES', label: 'Maldives 🇲🇻', position: 104, color: 'purple' },
|
||||
{ id: '2d5e9644-11b6-4b55-8f9a-ed33da2de6ac', value: 'MALI', label: 'Mali 🇲🇱', position: 105, color: 'orange' },
|
||||
{ id: 'e2fa091a-0e78-41da-937b-42b55e501226', value: 'MALTA', label: 'Malta 🇲🇹', position: 106, color: 'yellow' },
|
||||
{ id: 'e36301e0-1d79-4540-87c7-4de2bfeface6', value: 'MARSHALL_ISLANDS', label: 'Marshall Islands 🇲🇭', position: 107, color: 'red' },
|
||||
{ id: '8dc782e6-3578-4d7a-a6ba-a29e48f3cc11', value: 'MAURITANIA', label: 'Mauritania 🇲🇷', position: 108, color: 'pink' },
|
||||
{ id: 'edf63c87-d621-40fe-b653-a0f7764c8961', value: 'MAURITIUS', label: 'Mauritius 🇲🇺', position: 109, color: 'gray' },
|
||||
{ id: '86233df1-6c71-4ffb-9298-c61c65dcc79c', value: 'MEXICO', label: 'Mexico 🇲🇽', position: 110, color: 'blue' },
|
||||
{ id: 'c41c35ff-118b-4119-b12f-93be635fbae3', value: 'MICRONESIA', label: 'Micronesia 🇫🇲', position: 111, color: 'turquoise' },
|
||||
{ id: '8466a06e-a34f-4dd4-b660-8e9ad2aa5998', value: 'MOLDOVA', label: 'Moldova 🇲🇩', position: 112, color: 'green' },
|
||||
{ id: '948e9117-486d-40c1-847c-de6cc64b9730', value: 'MONACO', label: 'Monaco 🇲🇨', position: 113, color: 'sky' },
|
||||
{ id: 'ee33ddb8-3219-4fdb-8a71-8a6f8fdf93d3', value: 'MONGOLIA', label: 'Mongolia 🇲🇳', position: 114, color: 'purple' },
|
||||
{ id: 'b5c1fa55-0b1a-43d8-a871-ebd004952d0b', value: 'MONTENEGRO', label: 'Montenegro 🇲🇪', position: 115, color: 'orange' },
|
||||
{ id: '9e98cb68-666e-4116-b35f-bb9711fd7546', value: 'MOROCCO', label: 'Morocco 🇲🇦', position: 116, color: 'yellow' },
|
||||
{ id: '86c4b5f8-d4bc-4639-a5bf-00a84601b911', value: 'MOZAMBIQUE', label: 'Mozambique 🇲🇿', position: 117, color: 'red' },
|
||||
{ id: '8b3bcfa4-1797-492e-83d7-7d939e479b1d', value: 'MYANMAR', label: 'Myanmar 🇲🇲', position: 118, color: 'pink' },
|
||||
{ id: 'b8d01fef-4061-4e8c-bc38-f8226b4be6be', value: 'NAMIBIA', label: 'Namibia 🇳🇦', position: 119, color: 'gray' },
|
||||
{ id: '71dd1ad1-af94-4cfb-973d-33f87b4ef0cd', value: 'NAURU', label: 'Nauru 🇳🇷', position: 120, color: 'blue' },
|
||||
{ id: 'dc692171-3adc-4a73-8b45-a25cf659c546', value: 'NEPAL', label: 'Nepal 🇳🇵', position: 121, color: 'turquoise' },
|
||||
{ id: '3e161b4b-be95-4a6a-910d-628ad32691e1', value: 'NETHERLANDS', label: 'Netherlands 🇳🇱', position: 122, color: 'green' },
|
||||
{ id: 'b86be59e-a1ae-4df4-937f-ad97c641e0df', value: 'NEW_ZEALAND', label: 'New Zealand 🇳🇿', position: 123, color: 'sky' },
|
||||
{ id: '4674abdd-0548-4317-8af5-865e4d11327d', value: 'NICARAGUA', label: 'Nicaragua 🇳🇮', position: 124, color: 'purple' },
|
||||
{ id: '85ff2ce7-ddd8-4be6-b740-d79e70a3d103', value: 'NIGER', label: 'Niger 🇳🇪', position: 125, color: 'orange' },
|
||||
{ id: 'a69ab8cf-5a59-4879-92da-7cd2dea6b9e6', value: 'NIGERIA', label: 'Nigeria 🇳🇬', position: 126, color: 'yellow' },
|
||||
{ id: '28736977-b21f-4678-8e22-363be28a8e94', value: 'NORTH_KOREA', label: 'North Korea 🇰🇵', position: 127, color: 'red' },
|
||||
{ id: '11f87414-633d-48f8-a99d-0b407c281dbd', value: 'NORTH_MACEDONIA', label: 'North Macedonia 🇲🇰', position: 128, color: 'pink' },
|
||||
{ id: '9637c5aa-d405-4414-a56b-b983ed0bfa51', value: 'NORWAY', label: 'Norway 🇳🇴', position: 129, color: 'gray' },
|
||||
{ id: '70f79652-7f95-4836-ab2d-4373631910ec', value: 'OMAN', label: 'Oman 🇴🇲', position: 130, color: 'blue' },
|
||||
{ id: '50e4ae80-79d8-4cdf-aa0a-a2675e6e26fb', value: 'PAKISTAN', label: 'Pakistan 🇵🇰', position: 131, color: 'turquoise' },
|
||||
{ id: '08d27180-4861-4937-b04e-6cbcd33f507d', value: 'PALAU', label: 'Palau 🇵🇼', position: 132, color: 'green' },
|
||||
{ id: '6aa50196-ea4b-42cd-b890-8d7d56cc9731', value: 'PALESTINE', label: 'Palestine 🇵🇸', position: 133, color: 'sky' },
|
||||
{ id: '5663124d-f2fc-4374-8734-f3c53f9ea4f5', value: 'PANAMA', label: 'Panama 🇵🇦', position: 134, color: 'purple' },
|
||||
{ id: '8bde7cc7-fa6c-4f52-b9df-16c99928f50f', value: 'PAPUA_NEW_GUINEA', label: 'Papua New Guinea 🇵🇬', position: 135, color: 'orange' },
|
||||
{ id: '3eed4b21-1009-4dd2-bdac-53b11df8400c', value: 'PARAGUAY', label: 'Paraguay 🇵🇾', position: 136, color: 'yellow' },
|
||||
{ id: 'fbc734c9-63ae-4d2e-af0f-8e2abd874dda', value: 'PERU', label: 'Peru 🇵🇪', position: 137, color: 'red' },
|
||||
{ id: '211555bc-8f26-4d4d-94f1-c83884c32e20', value: 'PHILIPPINES', label: 'Philippines 🇵🇭', position: 138, color: 'pink' },
|
||||
{ id: '6d69e1a3-270e-4e36-9a55-40daed5b10ef', value: 'POLAND', label: 'Poland 🇵🇱', position: 139, color: 'gray' },
|
||||
{ id: '23b1ae16-f00f-4844-8871-ed2b46c1cb51', value: 'PORTUGAL', label: 'Portugal 🇵🇹', position: 140, color: 'blue' },
|
||||
{ id: '4dc69f6b-b0ee-48ea-95d3-517dc4ec7b91', value: 'QATAR', label: 'Qatar 🇶🇦', position: 141, color: 'turquoise' },
|
||||
{ id: '8dcd82ce-98b2-45cb-a76d-f1503509035f', value: 'ROMANIA', label: 'Romania 🇷🇴', position: 142, color: 'green' },
|
||||
{ id: '7e18e241-4fd6-4303-b390-0a9dea9c16bd', value: 'RUSSIA', label: 'Russia 🇷🇺', position: 143, color: 'sky' },
|
||||
{ id: '966e45fb-7ed8-478e-9c26-25b7d59ae4f4', value: 'RWANDA', label: 'Rwanda 🇷🇼', position: 144, color: 'purple' },
|
||||
{ id: '22ddf2d7-f1f8-46e9-8eb9-248c02652ad0', value: 'SAINT_KITTS_AND_NEVIS', label: 'Saint Kitts & Nevis 🇰🇳', position: 145, color: 'orange' },
|
||||
{ id: '3312359a-b9fd-4a95-9db2-ad3e1f67f2b2', value: 'SAINT_LUCIA', label: 'Saint Lucia 🇱🇨', position: 146, color: 'yellow' },
|
||||
{ id: '159c06c1-14a5-407f-af16-b84ba14f16be', value: 'SAINT_VINCENT', label: 'Saint Vincent 🇻🇨', position: 147, color: 'red' },
|
||||
{ id: '0dd1cf79-89e2-4442-bd83-c65b43a3a544', value: 'SAMOA', label: 'Samoa 🇼🇸', position: 148, color: 'pink' },
|
||||
{ id: 'e16851a7-97de-4e17-aeac-0cb828655406', value: 'SAN_MARINO', label: 'San Marino 🇸🇲', position: 149, color: 'gray' },
|
||||
{ id: '6415861d-5fa9-46b5-a2cd-d58587c9dd03', value: 'SAO_TOME_AND_PRINCIPE', label: 'São Tomé & Príncipe 🇸🇹', position: 150, color: 'blue' },
|
||||
{ id: '5831720a-3ce0-409c-9694-1b1d12394599', value: 'SAUDI_ARABIA', label: 'Saudi Arabia 🇸🇦', position: 151, color: 'turquoise' },
|
||||
{ id: '89155d75-e4d0-4115-916c-8b77b9ddc5f3', value: 'SENEGAL', label: 'Senegal 🇸🇳', position: 152, color: 'green' },
|
||||
{ id: 'f0c3e0e0-8106-4921-a02e-0497c1318efa', value: 'SERBIA', label: 'Serbia 🇷🇸', position: 153, color: 'sky' },
|
||||
{ id: 'd8633c5d-1c65-44c5-bc71-7eb65cb988c3', value: 'SEYCHELLES', label: 'Seychelles 🇸🇨', position: 154, color: 'purple' },
|
||||
{ id: '64b9fd47-a748-4add-bacf-a7dc1f1aa0c7', value: 'SIERRA_LEONE', label: 'Sierra Leone 🇸🇱', position: 155, color: 'orange' },
|
||||
{ id: '5bd24cd5-a233-4d8f-930d-27f15d493c92', value: 'SINGAPORE', label: 'Singapore 🇸🇬', position: 156, color: 'yellow' },
|
||||
{ id: '63c5df12-b203-4627-91b2-0526a56452d4', value: 'SLOVAKIA', label: 'Slovakia 🇸🇰', position: 157, color: 'red' },
|
||||
{ id: 'b0deeef1-767a-4730-8743-588bfeb856e6', value: 'SLOVENIA', label: 'Slovenia 🇸🇮', position: 158, color: 'pink' },
|
||||
{ id: '9274cd87-3400-4ccd-9192-3239b5126691', value: 'SOLOMON_ISLANDS', label: 'Solomon Islands 🇸🇧', position: 159, color: 'gray' },
|
||||
{ id: '67dc7930-eaa4-4ef0-9fc4-af996eaaae09', value: 'SOMALIA', label: 'Somalia 🇸🇴', position: 160, color: 'blue' },
|
||||
{ id: '19d96578-107c-4286-9504-5d59077b8290', value: 'SOUTH_AFRICA', label: 'South Africa 🇿🇦', position: 161, color: 'turquoise' },
|
||||
{ id: '11cb5cb6-d67d-47ae-adf1-ecba3635fe20', value: 'SOUTH_KOREA', label: 'South Korea 🇰🇷', position: 162, color: 'green' },
|
||||
{ id: '8293e893-0d7d-4002-9929-fea536a9a73c', value: 'SOUTH_SUDAN', label: 'South Sudan 🇸🇸', position: 163, color: 'sky' },
|
||||
{ id: 'e0b9ba99-d891-4f6c-95df-1f92a0a7e12e', value: 'SPAIN', label: 'Spain 🇪🇸', position: 164, color: 'purple' },
|
||||
{ id: '18378ce8-e480-4fd9-b90d-379d5daf7ecb', value: 'SRI_LANKA', label: 'Sri Lanka 🇱🇰', position: 165, color: 'orange' },
|
||||
{ id: '10943324-5ae5-47db-bb2f-5d4bdeafd0b0', value: 'SUDAN', label: 'Sudan 🇸🇩', position: 166, color: 'yellow' },
|
||||
{ id: 'bfd244e4-87c0-4dca-89b0-a1b581f1aa65', value: 'SURINAME', label: 'Suriname 🇸🇷', position: 167, color: 'red' },
|
||||
{ id: '6a8f9119-c413-4ece-b432-2ce2e80580a1', value: 'SWEDEN', label: 'Sweden 🇸🇪', position: 168, color: 'pink' },
|
||||
{ id: '866a8e2d-60a7-40a0-8541-35a9cfa20c09', value: 'SWITZERLAND', label: 'Switzerland 🇨🇭', position: 169, color: 'gray' },
|
||||
{ id: '93758334-0ebc-4884-bfac-c0dbeec23a35', value: 'SYRIA', label: 'Syria 🇸🇾', position: 170, color: 'blue' },
|
||||
{ id: 'c785e424-7a6f-4f27-91bc-aadb617525b0', value: 'TAIWAN', label: 'Taiwan 🇹🇼', position: 171, color: 'turquoise' },
|
||||
{ id: '15ab8a5b-5989-4e3b-92cb-00b88cf7b7f6', value: 'TAJIKISTAN', label: 'Tajikistan 🇹🇯', position: 172, color: 'green' },
|
||||
{ id: '476a55dd-8c69-4711-8e40-54cbe41d5c3a', value: 'TANZANIA', label: 'Tanzania 🇹🇿', position: 173, color: 'sky' },
|
||||
{ id: 'bfcbef16-36c4-4350-a614-7ad065276104', value: 'THAILAND', label: 'Thailand 🇹🇭', position: 174, color: 'purple' },
|
||||
{ id: '6872c804-9baa-498f-9dc6-b5894b64dece', value: 'TIMOR_LESTE', label: 'Timor-Leste 🇹🇱', position: 175, color: 'orange' },
|
||||
{ id: 'b6429782-b6e1-4404-a5c4-8a44f3818147', value: 'TOGO', label: 'Togo 🇹🇬', position: 176, color: 'yellow' },
|
||||
{ id: 'f4a7b7f9-aa58-4d23-adac-a96ed2c61ba1', value: 'TONGA', label: 'Tonga 🇹🇴', position: 177, color: 'red' },
|
||||
{ id: 'a1377467-7a21-40af-861f-4b362b30fcb4', value: 'TRINIDAD_AND_TOBAGO', label: 'Trinidad & Tobago 🇹🇹', position: 178, color: 'pink' },
|
||||
{ id: '7779c7d6-c7e1-4d6c-8282-31f659988c22', value: 'TUNISIA', label: 'Tunisia 🇹🇳', position: 179, color: 'gray' },
|
||||
{ id: '5858f81e-629a-4b04-98e8-6441f3984044', value: 'TURKEY', label: 'Turkey 🇹🇷', position: 180, color: 'blue' },
|
||||
{ id: 'cfdae353-6782-4dde-b229-d4e2c577fbed', value: 'TURKMENISTAN', label: 'Turkmenistan 🇹🇲', position: 181, color: 'turquoise' },
|
||||
{ id: '755d859a-a8d4-45e0-aa6e-77afcedb29d0', value: 'TUVALU', label: 'Tuvalu 🇹🇻', position: 182, color: 'green' },
|
||||
{ id: '3b325d46-b8e8-4cb9-9318-53f20baf50bd', value: 'UGANDA', label: 'Uganda 🇺🇬', position: 183, color: 'sky' },
|
||||
{ id: '10f32cd9-0a6b-4829-913d-50f519dcfc3a', value: 'UKRAINE', label: 'Ukraine 🇺🇦', position: 184, color: 'purple' },
|
||||
{ id: '0776427b-0dc1-49ef-b1c8-bb7dbba5a1e9', value: 'UNITED_ARAB_EMIRATES', label: 'UAE 🇦🇪', position: 185, color: 'orange' },
|
||||
{ id: 'edaf21dc-60d4-4550-8b58-2f7a19eedc92', value: 'UNITED_KINGDOM', label: 'UK 🇬🇧', position: 186, color: 'yellow' },
|
||||
{ id: '553004cd-aeed-4025-aa71-574efcade200', value: 'UNITED_STATES', label: 'USA 🇺🇸', position: 187, color: 'red' },
|
||||
{ id: 'f925d857-5cb7-467d-bc65-e5c04ae5a238', value: 'URUGUAY', label: 'Uruguay 🇺🇾', position: 188, color: 'pink' },
|
||||
{ id: '9b50ea41-2b8f-44d1-beeb-f4f4428289c4', value: 'UZBEKISTAN', label: 'Uzbekistan 🇺🇿', position: 189, color: 'gray' },
|
||||
{ id: 'a7a7cbfd-6999-42e8-874e-c2bfbe6ca567', value: 'VANUATU', label: 'Vanuatu 🇻🇺', position: 190, color: 'blue' },
|
||||
{ id: '7de0a670-8f04-444c-86dc-d7be00cc8b4e', value: 'VATICAN', label: 'Vatican 🇻🇦', position: 191, color: 'turquoise' },
|
||||
{ id: '9acc531c-7f17-4b67-804a-035c3c873cce', value: 'VENEZUELA', label: 'Venezuela 🇻🇪', position: 192, color: 'green' },
|
||||
{ id: '540b908e-f6b6-4f70-bb9c-c86dc5becbe7', value: 'VIETNAM', label: 'Vietnam 🇻🇳', position: 193, color: 'sky' },
|
||||
{ id: 'a70a3610-9a9b-4fe0-9e97-10d3e60a3d92', value: 'YEMEN', label: 'Yemen 🇾🇪', position: 194, color: 'purple' },
|
||||
{ id: 'fde30bd3-b2f1-4331-a255-9601259e807e', value: 'ZAMBIA', label: 'Zambia 🇿🇲', position: 195, color: 'orange' },
|
||||
{ id: 'a83060bf-f55b-4efe-9e0c-b856ad5446c4', value: 'ZIMBABWE', label: 'Zimbabwe 🇿🇼', position: 196, color: 'yellow' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: '560503de-6330-4c1d-af97-a8dee125f2ad',
|
||||
type: FieldType.MULTI_SELECT,
|
||||
name: 'region',
|
||||
label: 'Region',
|
||||
icon: 'IconWorld',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{ id: '735d7a75-c032-48b3-8480-c46e15b7e511', value: 'EUROPE', label: 'Europe', position: 0, color: 'blue' },
|
||||
{ id: '78eb8d0c-841e-48dd-8425-d8546cbcfe25', value: 'US', label: 'US', position: 1, color: 'red' },
|
||||
{ id: 'b0339081-52dd-4101-b7e7-83ed6ad6ca4f', value: 'LATAM', label: 'LATAM', position: 2, color: 'green' },
|
||||
{ id: 'f02bd583-87b8-48d1-97db-9395056c96e2', value: 'MENA', label: 'MENA', position: 3, color: 'orange' },
|
||||
{ id: '5205c25f-a300-4967-b5f3-033c0515a28a', value: 'APAC', label: 'APAC', position: 4, color: 'yellow' },
|
||||
{ id: '7f55cf70-8c81-4f68-83bc-270d215f0a4e', value: 'AFRICA', label: 'Africa', position: 5, color: 'pink' },
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: '243e9808-c070-4163-8603-ded12b03923c',
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'projectBudgetMin',
|
||||
label: 'Project Budget Min',
|
||||
icon: 'IconCoin',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ced87a97-cb2a-43cb-a6fc-4a1eff2892ba',
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'projectBudgetTypical',
|
||||
label: 'Project Budget Typical',
|
||||
icon: 'IconCoins',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '6a095709-7620-495f-b6e0-790743e412d5',
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'hourlyRate',
|
||||
label: 'Hourly Rate',
|
||||
icon: 'IconCurrencyDollar',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '640bbf33-45d7-4174-a862-dbe611ab8d1a',
|
||||
type: FieldType.LINKS,
|
||||
name: 'linkedin',
|
||||
label: 'LinkedIn',
|
||||
icon: 'IconBrandLinkedin',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '40d730e3-2785-45c8-aa5f-cc724b1b08e0',
|
||||
type: FieldType.LINKS,
|
||||
name: 'profilePicture',
|
||||
label: 'Profile Picture',
|
||||
icon: 'IconPhoto',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0000008-0000-4000-8000-000000000008',
|
||||
type: FieldType.LINKS,
|
||||
name: 'calendarLink',
|
||||
label: 'Calendar Link',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0000009-0000-4000-8000-000000000009',
|
||||
type: FieldType.TEXT,
|
||||
name: 'introduction',
|
||||
label: 'Introduction',
|
||||
icon: 'IconFileText',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0000010-0000-4000-8000-000000000010',
|
||||
type: FieldType.DATE_TIME,
|
||||
name: 'lastMatchAt',
|
||||
label: 'Last Match At',
|
||||
icon: 'IconClock',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineRole } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// PLACEHOLDER role for external partners. Twenty has no row-level filtering yet,
|
||||
// so anyone assigned this role currently sees every record — do not hand out
|
||||
// until RLP ships and we can scope to records owned by the assigned user.
|
||||
export default defineRole({
|
||||
universalIdentifier: PARTNER_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Partner',
|
||||
description:
|
||||
'PLACEHOLDER. External partner self-service role. Sees ALL Partner/Opportunity records today because Twenty does not yet support row-level record filtering. When RLP ships, scope these permissions to records owned by the assigned user. DO NOT assign to real external partners until then.',
|
||||
icon: 'IconBuildingStore',
|
||||
canBeAssignedToUsers: true,
|
||||
canUpdateAllSettings: false,
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user