Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eb741e90c | ||
|
|
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 |
@@ -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/**"
|
||||
]
|
||||
}
|
||||
@@ -10,4 +10,8 @@ nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
npmPreapprovedPackages:
|
||||
- twenty-sdk
|
||||
- twenty-client-sdk
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,38 @@
|
||||
{
|
||||
"name": "twenty-partners",
|
||||
"version": "0.1.0",
|
||||
"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",
|
||||
"import:dryrun": "tsx src/scripts/import-from-tft.ts",
|
||||
"import:apply": "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_QUOTE_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_QUOTES_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_QUOTES_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_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',
|
||||
},
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_QUOTE_OPPORTUNITY_FIELD_ID = '36684f3e-f01a-4270-8f34-78966747dd64';
|
||||
export const PARTNER_QUOTES_ON_OPPORTUNITY_FIELD_ID = '4857f339-a051-4cdb-bd8a-6219b933d1ce';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_QUOTE_OPPORTUNITY_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'opportunity',
|
||||
label: 'Opportunity',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_QUOTES_ON_OPPORTUNITY_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: OnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'opportunityId',
|
||||
},
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { FieldType, OnDeleteAction, RelationType, defineField } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER, PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export const PARTNER_QUOTE_PARTNER_FIELD_ID = '1774738e-96a1-43e2-aca4-d3c7cc794c50';
|
||||
export const PARTNER_QUOTES_ON_PARTNER_FIELD_ID = 'ac7995f1-7ccf-4607-827b-0788eeacaee0';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_QUOTE_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partner',
|
||||
label: 'Partner',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_QUOTES_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_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_QUOTES_ON_OPPORTUNITY_FIELD_ID, PARTNER_QUOTE_OPPORTUNITY_FIELD_ID } from './partner-quote-opportunity.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_QUOTES_ON_OPPORTUNITY_FIELD_ID,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerQuotes',
|
||||
label: 'Partner Quotes',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_QUOTE_OPPORTUNITY_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_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { PARTNER_QUOTES_ON_PARTNER_FIELD_ID, PARTNER_QUOTE_PARTNER_FIELD_ID } from './partner-quote-partner.field';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: PARTNER_QUOTES_ON_PARTNER_FIELD_ID,
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.RELATION,
|
||||
name: 'partnerQuotes',
|
||||
label: 'Partner Quotes',
|
||||
isNullable: true,
|
||||
relationTargetObjectMetadataUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetFieldMetadataUniversalIdentifier: PARTNER_QUOTE_PARTNER_FIELD_ID,
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
});
|
||||
@@ -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 {
|
||||
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,
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NavigationMenuItemType, defineNavigationMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_QUOTES_NAV_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_QUOTES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: PARTNER_QUOTES_NAV_UNIVERSAL_IDENTIFIER,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
icon: 'IconFileDollar',
|
||||
position: 3,
|
||||
folderUniversalIdentifier: '857be3b5-82c6-45f7-b546-e20a8a97be8d',
|
||||
viewUniversalIdentifier: PARTNER_QUOTES_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,65 @@
|
||||
import { FieldType, defineObject } from 'twenty-sdk/define';
|
||||
|
||||
import { PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'partnerQuote',
|
||||
namePlural: 'partnerQuotes',
|
||||
labelSingular: 'Partner Quote',
|
||||
labelPlural: 'Partner Quotes',
|
||||
description: 'A quote a partner submitted for a customer deal',
|
||||
icon: 'IconFileDollar',
|
||||
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: '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: '2cbe67e3-24ec-421b-bab5-50f3306c2391',
|
||||
type: FieldType.CURRENCY,
|
||||
name: 'amount',
|
||||
label: 'Amount',
|
||||
icon: 'IconCoin',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'f303369e-288c-4a48-9920-c1de0ad9a159',
|
||||
type: FieldType.FILES,
|
||||
name: 'quoteFile',
|
||||
label: 'Quote File',
|
||||
icon: 'IconPaperclip',
|
||||
isNullable: true,
|
||||
universalSettings: { maxNumberOfValues: 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, defineRole } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
TWENTY_PARTNER_OPS_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// Internal role for the Twenty partner-ops team. Scoped strictly to the CRM
|
||||
// objects needed to run the matching workflow — no settings, no destroy.
|
||||
export default defineRole({
|
||||
universalIdentifier: TWENTY_PARTNER_OPS_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Twenty Partner Ops',
|
||||
description:
|
||||
'Internal Twenty teammate role for managing partners and matched deals. Full read/write on Partner, Company, Person, Opportunity. No access to Tasks/Notes/Workflows. No settings access.',
|
||||
icon: 'IconUsersGroup',
|
||||
canBeAssignedToUsers: true,
|
||||
canUpdateAllSettings: false,
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,420 @@
|
||||
// Import partners, opportunities and partner quotes FROM the TFT workspace INTO
|
||||
// this (local) workspace. Idempotent: re-running upserts by natural key
|
||||
// (partner=slug, opportunity=tftOpportunityId, quote=name).
|
||||
//
|
||||
// TFT (source) is read over RAW GraphQL fetch: its custom fields/filters are not
|
||||
// in the SDK's generated (local) genql schema, so CoreApiClient cannot build
|
||||
// queries for them. The local (target) workspace is written via CoreApiClient,
|
||||
// exactly like seed.ts. Two separate credential sets, no collision:
|
||||
// TFT -> TFT_API_URL / TFT_API_KEY (raw fetch)
|
||||
// local-> TWENTY_PARTNERS_API_URL / TWENTY_PARTNERS_API_KEY (CoreApiClient)
|
||||
//
|
||||
// The local server rate-limits API calls (~100 / 60s), so existence checks are
|
||||
// batched into one `in` query per object (like seed.ts) and writes are paced.
|
||||
//
|
||||
// DRY-RUN BY DEFAULT: reads everything, writes nothing, and reports both what it
|
||||
// WOULD upsert and the distinct TFT SELECT values it saw (flagging any value not
|
||||
// covered by a local field option, which would otherwise fail a real write).
|
||||
// Set IMPORT_APPLY=1 to actually write to the local workspace.
|
||||
//
|
||||
// TFT_API_URL=https://twentyfortwenty.twenty.com TFT_API_KEY=<tft key> \
|
||||
// TWENTY_PARTNERS_API_URL=http://localhost:2020 TWENTY_PARTNERS_API_KEY=<local key> \
|
||||
// [IMPORT_APPLY=1] \
|
||||
// tsx src/scripts/import-from-tft.ts
|
||||
|
||||
import { config } from 'dotenv';
|
||||
config({ path: '.env.local' });
|
||||
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const requireEnv = (name: string): string => {
|
||||
const value = process.env[name];
|
||||
if (!value) throw new Error(`Missing ${name} env var`);
|
||||
return value;
|
||||
};
|
||||
|
||||
const APPLY = process.env.IMPORT_APPLY === '1';
|
||||
|
||||
// Raw GraphQL against TFT. The generated CoreApiClient is bound to the LOCAL
|
||||
// schema and cannot serialise TFT's custom fields/filters, so read TFT untyped.
|
||||
const tftQuery = async (query: string): Promise<any> => {
|
||||
const url = `${requireEnv('TFT_API_URL').replace(/\/$/, '')}/graphql`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${requireEnv('TFT_API_KEY')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
const json: any = await response.json();
|
||||
if (json.errors?.length) {
|
||||
throw new Error(`TFT query failed: ${JSON.stringify(json.errors)}`);
|
||||
}
|
||||
return json.data;
|
||||
};
|
||||
|
||||
// TFT opportunity.stage -> our matchStatus (the 4 early stages collapse to TO_BE_MATCHED)
|
||||
const STAGE_TO_MATCH_STATUS: Record<string, string> = {
|
||||
INTRODUCED_TO_A_PARTNER: 'INTRODUCED_TO_A_PARTNER',
|
||||
WORKING_WITH_A_PARTNER: 'WORKING_WITH_A_PARTNER',
|
||||
WON: 'WON',
|
||||
LOST: 'LOST',
|
||||
RECONNECT_LATER: 'RECONNECT_LATER',
|
||||
IDENTIFIED: 'TO_BE_MATCHED',
|
||||
MET: 'TO_BE_MATCHED',
|
||||
SOLUTIONING: 'TO_BE_MATCHED',
|
||||
ADVANCED: 'TO_BE_MATCHED',
|
||||
};
|
||||
|
||||
// TFT person.partnerStage -> our Partner.validationStage
|
||||
const PARTNER_STAGE_TO_VALIDATION: Record<string, string> = {
|
||||
APPLICATION: 'APPLICATION',
|
||||
POTENTIAL_PARTNER: 'POTENTIAL',
|
||||
PARTNER: 'VALIDATED',
|
||||
FORMER_PARTNER: 'FORMER',
|
||||
REJECTED: 'REJECTED',
|
||||
};
|
||||
|
||||
// Local SELECT option sets, for preflight coverage checks (a TFT value not in
|
||||
// these would fail a real write). Keep in sync with src/objects + src/fields.
|
||||
const LOCAL_OPTIONS: Record<string, Set<string>> = {
|
||||
partnerTier: new Set(['NEW', 'INTERMEDIATE', 'ADVANCED']),
|
||||
partnerScope: new Set(['APPS', 'DATA_MODEL', 'DATA_MIGRATION', 'HOSTING_ENVIRONMENT', 'WORKFLOWS']),
|
||||
typeOfTeam: new Set(['SOLO', 'AGENCY']),
|
||||
hostingType: new Set(['CLOUD', 'SELF_HOSTING']),
|
||||
subscriptionType: new Set(['PRO', 'ORG', 'ENT']),
|
||||
subscriptionFrequency: new Set(['MONTHLY', 'ANNUAL']),
|
||||
quoteStatus: new Set(['WIP', 'INTERVIEW_SCHEDULED', 'UNDER_CUSTOMER_PARTNER_REVIEW', 'APPROVED', 'REJECTED']),
|
||||
};
|
||||
|
||||
const slugify = (s: string): string =>
|
||||
s
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
const edges = (result: any, key: string): any[] =>
|
||||
(result?.[key]?.edges ?? []).map((e: any) => e.node);
|
||||
|
||||
const uniq = (values: (string | undefined | null)[]): string[] =>
|
||||
[...new Set(values.filter((v): v is string => !!v))];
|
||||
|
||||
async function main() {
|
||||
console.log(`[import] mode: ${APPLY ? 'APPLY (writing to local)' : 'DRY-RUN (no writes)'}`);
|
||||
const local = new CoreApiClient({
|
||||
url: `${requireEnv('TWENTY_PARTNERS_API_URL').replace(/\/$/, '')}/graphql`,
|
||||
headers: { Authorization: `Bearer ${requireEnv('TWENTY_PARTNERS_API_KEY')}` },
|
||||
});
|
||||
|
||||
// Pace writes to stay under the local server's ~100 req/60s limit.
|
||||
let writes = 0;
|
||||
const pace = async () => {
|
||||
if (APPLY && writes++ > 0) await new Promise((r) => setTimeout(r, 750));
|
||||
};
|
||||
|
||||
// Distinct TFT values seen, for the preflight coverage report.
|
||||
const seen: Record<string, Set<string>> = {
|
||||
partnerStage: new Set(),
|
||||
partnerTier: new Set(),
|
||||
partnerScope: new Set(),
|
||||
typeOfTeam: new Set(),
|
||||
oppStage: new Set(),
|
||||
hostingType: new Set(),
|
||||
subscriptionType: new Set(),
|
||||
subscriptionFrequency: new Set(),
|
||||
quoteStatus: new Set(),
|
||||
typeCustom: new Set(),
|
||||
};
|
||||
const note = (bucket: string, value?: string | null) => {
|
||||
if (value) seen[bucket].add(value);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 1. Read all three TFT datasets (raw fetch; not rate-limited by local).
|
||||
// ---------------------------------------------------------------------
|
||||
const tftPeople = edges(
|
||||
await tftQuery(`query {
|
||||
people(first: 500, filter: { partnerStage: { in: ["APPLICATION","POTENTIAL_PARTNER","PARTNER","FORMER_PARTNER","REJECTED"] } }) {
|
||||
edges { node {
|
||||
id
|
||||
name { firstName lastName }
|
||||
emails { primaryEmail }
|
||||
city jobTitle
|
||||
linkedinLink { primaryLinkUrl }
|
||||
partnerStage partnerTier partnerScope partnerTypeOfTeam partnerIsAvailable partnerSkills
|
||||
partnerBudgetMinimum { amountMicros currencyCode }
|
||||
partnerBudgetAverage { amountMicros currencyCode }
|
||||
company { id name domainName { primaryLinkUrl } }
|
||||
} }
|
||||
}
|
||||
}`),
|
||||
'people',
|
||||
);
|
||||
const tftOpps = edges(
|
||||
await tftQuery(`query {
|
||||
opportunities(first: 500) {
|
||||
edges { node {
|
||||
id name numberOfSeats useCase hostingType subscriptionType subscriptionFrequence lostReason stage
|
||||
amount { amountMicros currencyCode }
|
||||
closeDate
|
||||
company { id name domainName { primaryLinkUrl } }
|
||||
partner { id }
|
||||
} }
|
||||
}
|
||||
}`),
|
||||
'opportunities',
|
||||
);
|
||||
const tftContent = edges(
|
||||
await tftQuery(`query {
|
||||
customerContents(first: 500) {
|
||||
edges { node { id name status approvalDate typeCustom partnerPerson { id } } }
|
||||
}
|
||||
}`),
|
||||
'customerContents',
|
||||
);
|
||||
const quotes = tftContent.filter((c: any) => Array.isArray(c.typeCustom) && c.typeCustom.includes('PARTNER_QUOTE'));
|
||||
console.log(`[import] TFT: ${tftPeople.length} partner-people, ${tftOpps.length} opportunities, ${quotes.length} partner quotes`);
|
||||
|
||||
const personSlug = (p: any): string =>
|
||||
slugify([p.name?.firstName, p.name?.lastName].filter(Boolean).join(' ').trim() || 'Unknown partner') || p.id;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 2. Batched existence lookups against local (one `in` query per object).
|
||||
// ---------------------------------------------------------------------
|
||||
const partnerSlugs = uniq(tftPeople.map(personSlug));
|
||||
const partnerIdBySlug = new Map<string, string>(
|
||||
partnerSlugs.length
|
||||
? edges(
|
||||
await local.query({
|
||||
partners: { __args: { filter: { slug: { in: partnerSlugs } }, first: 500 }, edges: { node: { id: true, slug: true } } },
|
||||
} as any),
|
||||
'partners',
|
||||
).map((n: any) => [n.slug, n.id])
|
||||
: [],
|
||||
);
|
||||
|
||||
const companyNames = uniq([...tftPeople.map((p: any) => p.company?.name), ...tftOpps.map((o: any) => o.company?.name)]);
|
||||
const companyIdByName = new Map<string, string>(
|
||||
companyNames.length
|
||||
? edges(
|
||||
await local.query({
|
||||
companies: { __args: { filter: { name: { in: companyNames } }, first: 500 }, edges: { node: { id: true, name: true } } },
|
||||
} as any),
|
||||
'companies',
|
||||
).map((n: any) => [n.name, n.id])
|
||||
: [],
|
||||
);
|
||||
|
||||
const oppTftIds = uniq(tftOpps.filter((o: any) => o.name).map((o: any) => o.id));
|
||||
const oppIdByTftId = new Map<string, string>(
|
||||
oppTftIds.length
|
||||
? edges(
|
||||
await local.query({
|
||||
opportunities: { __args: { filter: { tftOpportunityId: { in: oppTftIds } }, first: 500 }, edges: { node: { id: true, tftOpportunityId: true } } },
|
||||
} as any),
|
||||
'opportunities',
|
||||
).map((n: any) => [n.tftOpportunityId, n.id])
|
||||
: [],
|
||||
);
|
||||
|
||||
const quoteNames = uniq(quotes.map((q: any) => q.name || 'Partner quote'));
|
||||
const quoteIdByName = new Map<string, string>(
|
||||
quoteNames.length
|
||||
? edges(
|
||||
await local.query({
|
||||
partnerQuotes: { __args: { filter: { name: { in: quoteNames } }, first: 500 }, edges: { node: { id: true, name: true } } },
|
||||
} as any),
|
||||
'partnerQuotes',
|
||||
).map((n: any) => [n.name, n.id])
|
||||
: [],
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 3. Upsert. Writes are APPLY-gated and paced; in dry-run we resolve new
|
||||
// ids to synthetic placeholders so relation mapping still exercises.
|
||||
// ---------------------------------------------------------------------
|
||||
const upsertCompany = async (name?: string, domain?: string): Promise<string | undefined> => {
|
||||
if (!name) return undefined;
|
||||
if (companyIdByName.has(name)) return companyIdByName.get(name);
|
||||
let id = `dry:company:${name}`;
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
const created: any = await local.mutation({
|
||||
createCompany: {
|
||||
__args: { data: { name, ...(domain ? { domainName: { primaryLinkUrl: domain } } : {}) } },
|
||||
id: true,
|
||||
},
|
||||
} as any);
|
||||
id = created.createCompany.id;
|
||||
}
|
||||
companyIdByName.set(name, id);
|
||||
return id;
|
||||
};
|
||||
|
||||
const budgetCurrency = (amount: any) =>
|
||||
amount?.amountMicros != null ? { amountMicros: amount.amountMicros, currencyCode: amount.currencyCode ?? 'USD' } : undefined;
|
||||
|
||||
// -- Partners (upsert by slug) --
|
||||
const localPartnerIdByTftPersonId = new Map<string, string>();
|
||||
let partnersCreated = 0;
|
||||
let partnersUpdated = 0;
|
||||
for (const p of tftPeople) {
|
||||
note('partnerStage', p.partnerStage);
|
||||
note('partnerTier', p.partnerTier);
|
||||
(Array.isArray(p.partnerScope) ? p.partnerScope : []).forEach((s: string) => note('partnerScope', s));
|
||||
note('typeOfTeam', p.partnerTypeOfTeam);
|
||||
|
||||
const slug = personSlug(p);
|
||||
const companyId = await upsertCompany(p.company?.name, p.company?.domainName?.primaryLinkUrl);
|
||||
const data: Record<string, unknown> = {
|
||||
name: [p.name?.firstName, p.name?.lastName].filter(Boolean).join(' ').trim() || 'Unknown partner',
|
||||
slug,
|
||||
validationStage: PARTNER_STAGE_TO_VALIDATION[p.partnerStage] ?? 'APPLICATION',
|
||||
availability: p.partnerIsAvailable ? 'AVAILABLE' : 'UNAVAILABLE',
|
||||
...(p.partnerTier ? { partnerTier: p.partnerTier } : {}),
|
||||
...(Array.isArray(p.partnerScope) && p.partnerScope.length ? { partnerScope: p.partnerScope } : {}),
|
||||
...(p.partnerTypeOfTeam ? { typeOfTeam: p.partnerTypeOfTeam } : {}),
|
||||
...(Array.isArray(p.partnerSkills) && p.partnerSkills.length ? { skills: p.partnerSkills } : {}),
|
||||
...(p.city ? { city: p.city } : {}),
|
||||
...(budgetCurrency(p.partnerBudgetMinimum) ? { projectBudgetMin: budgetCurrency(p.partnerBudgetMinimum) } : {}),
|
||||
...(budgetCurrency(p.partnerBudgetAverage) ? { projectBudgetTypical: budgetCurrency(p.partnerBudgetAverage) } : {}),
|
||||
...(p.linkedinLink?.primaryLinkUrl ? { linkedin: { primaryLinkUrl: p.linkedinLink.primaryLinkUrl } } : {}),
|
||||
...(companyId && APPLY ? { companyId } : {}),
|
||||
};
|
||||
|
||||
let partnerId = partnerIdBySlug.get(slug);
|
||||
if (partnerId) {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ updatePartner: { __args: { id: partnerId, data }, id: true } } as any);
|
||||
}
|
||||
partnersUpdated++;
|
||||
} else {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
const created: any = await local.mutation({ createPartner: { __args: { data }, id: true } } as any);
|
||||
partnerId = created.createPartner.id;
|
||||
} else {
|
||||
partnerId = `dry:partner:${slug}`;
|
||||
}
|
||||
partnerIdBySlug.set(slug, partnerId as string);
|
||||
partnersCreated++;
|
||||
}
|
||||
localPartnerIdByTftPersonId.set(p.id, partnerId as string);
|
||||
}
|
||||
console.log(`[import] partners created=${partnersCreated} updated=${partnersUpdated}`);
|
||||
|
||||
// -- Opportunities (upsert by tftOpportunityId) --
|
||||
let oppsCreated = 0;
|
||||
let oppsUpdated = 0;
|
||||
let oppsPartnerLinked = 0;
|
||||
for (const o of tftOpps) {
|
||||
if (!o.name) continue;
|
||||
note('oppStage', o.stage);
|
||||
note('hostingType', o.hostingType);
|
||||
note('subscriptionType', o.subscriptionType);
|
||||
note('subscriptionFrequency', o.subscriptionFrequence);
|
||||
|
||||
const companyId = await upsertCompany(o.company?.name, o.company?.domainName?.primaryLinkUrl);
|
||||
const partnerId = o.partner?.id ? localPartnerIdByTftPersonId.get(o.partner.id) : undefined;
|
||||
if (partnerId) oppsPartnerLinked++;
|
||||
|
||||
const data: Record<string, unknown> = {
|
||||
name: o.name,
|
||||
tftOpportunityId: o.id,
|
||||
matchStatus: STAGE_TO_MATCH_STATUS[o.stage] ?? 'TO_BE_MATCHED',
|
||||
...(o.numberOfSeats != null ? { numberOfSeats: o.numberOfSeats } : {}),
|
||||
...(o.useCase ? { useCase: o.useCase } : {}),
|
||||
...(o.hostingType ? { hostingType: o.hostingType } : {}),
|
||||
...(o.subscriptionType ? { subscriptionType: o.subscriptionType } : {}),
|
||||
...(o.subscriptionFrequence ? { subscriptionFrequency: o.subscriptionFrequence } : {}),
|
||||
...(o.lostReason ? { lostReason: o.lostReason } : {}),
|
||||
...(o.amount?.amountMicros != null ? { amount: { amountMicros: o.amount.amountMicros, currencyCode: o.amount.currencyCode ?? 'USD' } } : {}),
|
||||
...(o.closeDate ? { closeDate: o.closeDate } : {}),
|
||||
...(companyId && APPLY ? { companyId } : {}),
|
||||
...(partnerId && APPLY ? { partnerId } : {}),
|
||||
};
|
||||
|
||||
const existingId = oppIdByTftId.get(o.id);
|
||||
if (existingId) {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ updateOpportunity: { __args: { id: existingId, data }, id: true } } as any);
|
||||
}
|
||||
oppsUpdated++;
|
||||
} else {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ createOpportunity: { __args: { data }, id: true } } as any);
|
||||
}
|
||||
oppsCreated++;
|
||||
}
|
||||
}
|
||||
console.log(`[import] opportunities created=${oppsCreated} updated=${oppsUpdated} (partner-linked=${oppsPartnerLinked})`);
|
||||
|
||||
// -- Partner quotes (upsert by name) --
|
||||
let quotesCreated = 0;
|
||||
let quotesUpdated = 0;
|
||||
tftContent.forEach((c: any) => (Array.isArray(c.typeCustom) ? c.typeCustom : []).forEach((t: string) => note('typeCustom', t)));
|
||||
for (const q of quotes) {
|
||||
note('quoteStatus', q.status);
|
||||
const name = q.name || 'Partner quote';
|
||||
const partnerId = q.partnerPerson?.id ? localPartnerIdByTftPersonId.get(q.partnerPerson.id) : undefined;
|
||||
const data: Record<string, unknown> = {
|
||||
name,
|
||||
...(q.status ? { status: q.status } : {}),
|
||||
...(q.approvalDate ? { approvalDate: q.approvalDate } : {}),
|
||||
...(partnerId && APPLY ? { partnerId } : {}),
|
||||
};
|
||||
const existingId = quoteIdByName.get(name);
|
||||
if (existingId) {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ updatePartnerQuote: { __args: { id: existingId, data }, id: true } } as any);
|
||||
}
|
||||
quotesUpdated++;
|
||||
} else {
|
||||
if (APPLY) {
|
||||
await pace();
|
||||
await local.mutation({ createPartnerQuote: { __args: { data }, id: true } } as any);
|
||||
}
|
||||
quotesCreated++;
|
||||
}
|
||||
}
|
||||
console.log(`[import] partner quotes created=${quotesCreated} updated=${quotesUpdated}`);
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// 4. Preflight: distinct TFT values vs local option coverage.
|
||||
// ---------------------------------------------------------------------
|
||||
const report = (label: string, bucket: string, optionKey?: string) => {
|
||||
const values = [...seen[bucket]].sort();
|
||||
const allowed = optionKey ? LOCAL_OPTIONS[optionKey] : undefined;
|
||||
const uncovered = allowed ? values.filter((v) => !allowed.has(v)) : [];
|
||||
console.log(
|
||||
`[preflight] ${label}: ${values.join(', ') || '(none)'}` +
|
||||
(uncovered.length ? ` ⚠️ NOT IN LOCAL OPTIONS: ${uncovered.join(', ')}` : ''),
|
||||
);
|
||||
};
|
||||
console.log('--- preflight: distinct TFT values seen ---');
|
||||
report('partnerStage (-> validationStage map)', 'partnerStage');
|
||||
report('partnerTier', 'partnerTier', 'partnerTier');
|
||||
report('partnerScope', 'partnerScope', 'partnerScope');
|
||||
report('typeOfTeam', 'typeOfTeam', 'typeOfTeam');
|
||||
report('opp stage (-> matchStatus map)', 'oppStage');
|
||||
report('hostingType', 'hostingType', 'hostingType');
|
||||
report('subscriptionType', 'subscriptionType', 'subscriptionType');
|
||||
report('subscriptionFrequency', 'subscriptionFrequency', 'subscriptionFrequency');
|
||||
report('quote status', 'quoteStatus', 'quoteStatus');
|
||||
report('customerContent typeCustom', 'typeCustom');
|
||||
const unmappedStages = [...seen.partnerStage].filter((s) => !(s in PARTNER_STAGE_TO_VALIDATION));
|
||||
const unmappedOpps = [...seen.oppStage].filter((s) => !(s in STAGE_TO_MATCH_STATUS));
|
||||
if (unmappedStages.length) console.log(`[preflight] ⚠️ partnerStage not mapped: ${unmappedStages.join(', ')}`);
|
||||
if (unmappedOpps.length) console.log(`[preflight] ⚠️ opp stage not mapped: ${unmappedOpps.join(', ')}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
// Single demo seed for the twenty-partners app. Idempotent UPSERT by natural key.
|
||||
// Covers: partners across ALL validationStage values, companies + people,
|
||||
// opportunities across ALL 10 matchStatus values, and partner quotes across ALL
|
||||
// 5 statuses (each linked to a partner + opportunity).
|
||||
//
|
||||
// Run from this app directory, against a running Twenty server with the app
|
||||
// installed (deploy first; do NOT run `yarn test` after — its teardown wipes the app).
|
||||
// Credentials from shell env or a gitignored .env.local (TWENTY_PARTNERS_API_URL/KEY).
|
||||
//
|
||||
// yarn twenty dev --once
|
||||
// tsx src/scripts/seed.ts
|
||||
|
||||
import { config } from 'dotenv';
|
||||
config({ path: '.env.local' });
|
||||
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
|
||||
const requireEnv = (name: string): string => {
|
||||
const value = process.env[name];
|
||||
if (!value) throw new Error(`Missing ${name} env var`);
|
||||
return value;
|
||||
};
|
||||
|
||||
const CAL = 'https://calendly.com/placeholder';
|
||||
|
||||
type Partner = {
|
||||
slug: string;
|
||||
name: string;
|
||||
validationStage: string;
|
||||
availability: string;
|
||||
introduction: string;
|
||||
calendarLink: string;
|
||||
deploymentExpertise: string[];
|
||||
region: string[];
|
||||
languagesSpoken: string[];
|
||||
partnerTier: string;
|
||||
partnerScope: string[];
|
||||
typeOfTeam: string;
|
||||
country: string;
|
||||
};
|
||||
|
||||
const PARTNERS: Partner[] = [
|
||||
{ slug: 'nine-dots-ventures', name: 'Nine Dots Ventures', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Boutique CRM implementer for real-estate workflows and WhatsApp automation.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['EUROPE', 'MENA'], languagesSpoken: ['ENGLISH', 'FRENCH', 'ARABIC'], partnerTier: 'ADVANCED', partnerScope: ['DATA_MODEL', 'WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'FRANCE' },
|
||||
{ slug: 'elevate-consulting', name: 'Elevate Consulting', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Revenue-operations partner for B2B SaaS teams scaling seed to Series C.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['US', 'LATAM'], languagesSpoken: ['ENGLISH', 'SPANISH'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MIGRATION', 'DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES' },
|
||||
{ slug: 'w3villa-technologies', name: 'W3Villa Technologies', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'Engineering-heavy partner running large self-hosted Twenty deployments.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'MENA'], languagesSpoken: ['ENGLISH', 'HINDI'], partnerTier: 'ADVANCED', partnerScope: ['HOSTING_ENVIRONMENT', 'APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'INDIA' },
|
||||
{ slug: 'act-education', name: 'Act Education', validationStage: 'VALIDATED', availability: 'UNAVAILABLE', introduction: 'CRM partner for European education providers; compliance-first self-hosting.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'NEW', partnerScope: ['HOSTING_ENVIRONMENT', 'DATA_MODEL'], typeOfTeam: 'SOLO', country: 'GERMANY' },
|
||||
{ slug: 'netzero-systems', name: 'NetZero Systems', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'LATAM go-to-market partner for climate-tech and renewable-energy companies.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['LATAM', 'US'], languagesSpoken: ['ENGLISH', 'SPANISH', 'PORTUGUESE'], partnerTier: 'INTERMEDIATE', partnerScope: ['DATA_MODEL'], typeOfTeam: 'AGENCY', country: 'BRAZIL' },
|
||||
{ slug: 'meridian-craft', name: 'Meridian Craft', validationStage: 'VALIDATED', availability: 'AVAILABLE', introduction: 'APAC implementation studio for fintech and logistics; English + Chinese.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['APAC', 'AFRICA'], languagesSpoken: ['ENGLISH', 'CHINESE', 'MALAY'], partnerTier: 'ADVANCED', partnerScope: ['APPS', 'WORKFLOWS'], typeOfTeam: 'AGENCY', country: 'SINGAPORE' },
|
||||
{ slug: 'applicant-studio', name: 'Applicant Studio', validationStage: 'APPLICATION', availability: 'UNAVAILABLE', introduction: 'New applicant; awaiting first review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'FRENCH'], partnerTier: 'NEW', partnerScope: ['DATA_MODEL'], typeOfTeam: 'SOLO', country: 'FRANCE' },
|
||||
{ slug: 'rising-crm', name: 'Rising CRM', validationStage: 'POTENTIAL', availability: 'AVAILABLE', introduction: 'Promising applicant in evaluation.', calendarLink: CAL, deploymentExpertise: ['CLOUD', 'SELF_HOST'], region: ['US'], languagesSpoken: ['ENGLISH'], partnerTier: 'NEW', partnerScope: ['WORKFLOWS', 'APPS'], typeOfTeam: 'AGENCY', country: 'UNITED_STATES' },
|
||||
{ slug: 'legacy-partners', name: 'Legacy Partners', validationStage: 'FORMER', availability: 'UNAVAILABLE', introduction: 'Former partner; no longer active in the program.', calendarLink: CAL, deploymentExpertise: ['SELF_HOST'], region: ['EUROPE'], languagesSpoken: ['ENGLISH', 'GERMAN'], partnerTier: 'INTERMEDIATE', partnerScope: ['HOSTING_ENVIRONMENT'], typeOfTeam: 'AGENCY', country: 'UNITED_KINGDOM' },
|
||||
{ slug: 'declined-co', name: 'Declined Co', validationStage: 'REJECTED', availability: 'UNAVAILABLE', introduction: 'Application rejected after review.', calendarLink: CAL, deploymentExpertise: ['CLOUD'], region: ['MENA'], languagesSpoken: ['ENGLISH', 'ARABIC'], partnerTier: 'NEW', partnerScope: ['APPS'], typeOfTeam: 'SOLO', country: 'UNITED_ARAB_EMIRATES' },
|
||||
];
|
||||
|
||||
const COMPANIES = [
|
||||
{ name: 'Acme Real Estate', domain: 'https://acmerealestate.example' },
|
||||
{ name: 'Helix Bio', domain: 'https://helixbio.example' },
|
||||
{ name: 'Sunrise Logistics', domain: 'https://sunriselogistics.example' },
|
||||
];
|
||||
|
||||
const PERSONS = [
|
||||
{ firstName: 'Camille', lastName: 'Durand', companyName: 'Acme Real Estate', email: 'camille@acmerealestate.example', city: 'Paris' },
|
||||
{ firstName: 'Maya', lastName: 'Patel', companyName: 'Helix Bio', email: 'maya@helixbio.example', city: 'Boston' },
|
||||
{ firstName: 'Wei', lastName: 'Chen', companyName: 'Sunrise Logistics', email: 'wei@sunriselogistics.example', city: 'Singapore' },
|
||||
];
|
||||
|
||||
type Opp = {
|
||||
name: string;
|
||||
companyName: string;
|
||||
matchStatus: string;
|
||||
partnerSlug?: string;
|
||||
numberOfSeats?: number;
|
||||
hostingType?: string;
|
||||
subscriptionType?: string;
|
||||
subscriptionFrequency?: string;
|
||||
};
|
||||
|
||||
// One+ opportunity for every matchStatus value (all 10 covered).
|
||||
const OPPORTUNITIES: Opp[] = [
|
||||
{ name: 'Acme RE — Q3 renewal', companyName: 'Acme Real Estate', matchStatus: 'TO_BE_MATCHED', numberOfSeats: 20, hostingType: 'CLOUD', subscriptionType: 'PRO', subscriptionFrequency: 'ANNUAL' },
|
||||
{ name: 'Helix Bio — investor reporting', companyName: 'Helix Bio', matchStatus: 'MANUAL_MATCH', numberOfSeats: 12, hostingType: 'CLOUD', subscriptionType: 'ORG', subscriptionFrequency: 'MONTHLY' },
|
||||
{ name: 'Helix Bio — pipeline review', companyName: 'Helix Bio', matchStatus: 'AUTO_MATCH', numberOfSeats: 8 },
|
||||
{ name: 'Acme RE — CRM rollout', companyName: 'Acme Real Estate', matchStatus: 'MATCHED', partnerSlug: 'elevate-consulting', numberOfSeats: 30, hostingType: 'CLOUD', subscriptionType: 'ENT', subscriptionFrequency: 'ANNUAL' },
|
||||
{ name: 'Sunrise — APAC fleet CRM', companyName: 'Sunrise Logistics', matchStatus: 'INTRODUCED_TO_A_PARTNER', partnerSlug: 'nine-dots-ventures', numberOfSeats: 50, hostingType: 'SELF_HOSTING', subscriptionType: 'ENT', subscriptionFrequency: 'ANNUAL' },
|
||||
{ name: 'Helix Bio — clinical trials CRM', companyName: 'Helix Bio', matchStatus: 'WORKING_WITH_A_PARTNER', partnerSlug: 'netzero-systems', numberOfSeats: 25, hostingType: 'CLOUD', subscriptionType: 'ORG', subscriptionFrequency: 'MONTHLY' },
|
||||
{ name: 'Helix Bio — self-host evaluation', companyName: 'Helix Bio', matchStatus: 'IMPLEMENTING', partnerSlug: 'meridian-craft', numberOfSeats: 40, hostingType: 'SELF_HOSTING', subscriptionType: 'ENT', subscriptionFrequency: 'ANNUAL' },
|
||||
{ name: 'Sunrise — LATAM expansion', companyName: 'Sunrise Logistics', matchStatus: 'WON', partnerSlug: 'nine-dots-ventures', numberOfSeats: 60, hostingType: 'CLOUD', subscriptionType: 'ENT', subscriptionFrequency: 'ANNUAL' },
|
||||
{ name: 'Acme RE — annual review', companyName: 'Acme Real Estate', matchStatus: 'RECONNECT_LATER', partnerSlug: 'w3villa-technologies', numberOfSeats: 15 },
|
||||
{ name: 'Sunrise — vendor onboarding', companyName: 'Sunrise Logistics', matchStatus: 'LOST', numberOfSeats: 10, hostingType: 'CLOUD', subscriptionType: 'PRO', subscriptionFrequency: 'MONTHLY' },
|
||||
];
|
||||
|
||||
type Quote = { name: string; status: string; partnerSlug: string; oppName: string };
|
||||
const QUOTES: Quote[] = [
|
||||
{ name: 'Sunrise APAC fleet — Nine Dots quote', status: 'WIP', partnerSlug: 'nine-dots-ventures', oppName: 'Sunrise — APAC fleet CRM' },
|
||||
{ name: 'Helix clinical — NetZero quote', status: 'INTERVIEW_SCHEDULED', partnerSlug: 'netzero-systems', oppName: 'Helix Bio — clinical trials CRM' },
|
||||
{ name: 'Acme rollout — Elevate quote', status: 'UNDER_CUSTOMER_PARTNER_REVIEW', partnerSlug: 'elevate-consulting', oppName: 'Acme RE — CRM rollout' },
|
||||
{ name: 'Sunrise LATAM — Nine Dots quote', status: 'APPROVED', partnerSlug: 'nine-dots-ventures', oppName: 'Sunrise — LATAM expansion' },
|
||||
{ name: 'Helix self-host — Meridian quote', status: 'REJECTED', partnerSlug: 'meridian-craft', oppName: 'Helix Bio — self-host evaluation' },
|
||||
];
|
||||
|
||||
const nodes = (r: any, key: string): any[] => (r?.[key]?.edges ?? []).map((e: any) => e.node);
|
||||
|
||||
async function main() {
|
||||
const client = new CoreApiClient({
|
||||
url: `${requireEnv('TWENTY_PARTNERS_API_URL').replace(/\/$/, '')}/graphql`,
|
||||
headers: { Authorization: `Bearer ${requireEnv('TWENTY_PARTNERS_API_KEY')}` },
|
||||
});
|
||||
|
||||
// -- Partners (upsert by slug) --
|
||||
const existingPartners = nodes(
|
||||
await client.query({ partners: { __args: { filter: { slug: { in: PARTNERS.map((p) => p.slug) } }, first: 100 }, edges: { node: { id: true, slug: true } } } } as any),
|
||||
'partners',
|
||||
);
|
||||
const partnerIdBySlug = new Map<string, string>(existingPartners.map((n: any) => [n.slug, n.id]));
|
||||
for (const p of PARTNERS) {
|
||||
const data = {
|
||||
name: p.name, slug: p.slug, validationStage: p.validationStage, availability: p.availability,
|
||||
introduction: p.introduction, calendarLink: { primaryLinkUrl: p.calendarLink },
|
||||
deploymentExpertise: p.deploymentExpertise, region: p.region, languagesSpoken: p.languagesSpoken,
|
||||
partnerTier: p.partnerTier, partnerScope: p.partnerScope, typeOfTeam: p.typeOfTeam,
|
||||
country: p.country,
|
||||
};
|
||||
const id = partnerIdBySlug.get(p.slug);
|
||||
if (id) {
|
||||
await client.mutation({ updatePartner: { __args: { id, data }, id: true } } as any);
|
||||
} else {
|
||||
const r: any = await client.mutation({ createPartner: { __args: { data }, id: true } } as any);
|
||||
partnerIdBySlug.set(p.slug, r.createPartner.id);
|
||||
}
|
||||
}
|
||||
console.log(`[seed] partners: ${partnerIdBySlug.size}`);
|
||||
|
||||
// -- Companies (upsert by name) --
|
||||
const companyIdByName = new Map<string, string>();
|
||||
for (const c of COMPANIES) {
|
||||
const existing = nodes(await client.query({ companies: { __args: { filter: { name: { eq: c.name } }, first: 1 }, edges: { node: { id: true } } } } as any), 'companies');
|
||||
let id = existing[0]?.id;
|
||||
if (!id) {
|
||||
const r: any = await client.mutation({ createCompany: { __args: { data: { name: c.name, domainName: { primaryLinkUrl: c.domain } } }, id: true } } as any);
|
||||
id = r.createCompany.id;
|
||||
}
|
||||
companyIdByName.set(c.name, id);
|
||||
}
|
||||
|
||||
// -- People (upsert by firstName+lastName) --
|
||||
for (const person of PERSONS) {
|
||||
const existing = nodes(await client.query({ people: { __args: { filter: { name: { firstName: { eq: person.firstName } } }, first: 10 }, edges: { node: { id: true, name: { firstName: true, lastName: true } } } } } as any), 'people');
|
||||
const match = existing.find((n: any) => n.name?.firstName === person.firstName && n.name?.lastName === person.lastName);
|
||||
if (!match) {
|
||||
await client.mutation({ createPerson: { __args: { data: { name: { firstName: person.firstName, lastName: person.lastName }, emails: { primaryEmail: person.email }, city: person.city, companyId: companyIdByName.get(person.companyName) } }, id: true } } as any);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Opportunities (upsert by name) --
|
||||
const oppIdByName = new Map<string, string>();
|
||||
for (const o of OPPORTUNITIES) {
|
||||
const data: Record<string, unknown> = {
|
||||
name: o.name, matchStatus: o.matchStatus, companyId: companyIdByName.get(o.companyName),
|
||||
...(o.partnerSlug ? { partnerId: partnerIdBySlug.get(o.partnerSlug) } : {}),
|
||||
...(o.numberOfSeats != null ? { numberOfSeats: o.numberOfSeats } : {}),
|
||||
...(o.hostingType ? { hostingType: o.hostingType } : {}),
|
||||
...(o.subscriptionType ? { subscriptionType: o.subscriptionType } : {}),
|
||||
...(o.subscriptionFrequency ? { subscriptionFrequency: o.subscriptionFrequency } : {}),
|
||||
};
|
||||
const existing = nodes(await client.query({ opportunities: { __args: { filter: { name: { eq: o.name } }, first: 1 }, edges: { node: { id: true } } } } as any), 'opportunities');
|
||||
let id = existing[0]?.id;
|
||||
if (id) {
|
||||
await client.mutation({ updateOpportunity: { __args: { id, data }, id: true } } as any);
|
||||
} else {
|
||||
const r: any = await client.mutation({ createOpportunity: { __args: { data }, id: true } } as any);
|
||||
id = r.createOpportunity.id;
|
||||
}
|
||||
oppIdByName.set(o.name, id);
|
||||
}
|
||||
console.log(`[seed] opportunities: ${oppIdByName.size}`);
|
||||
|
||||
// -- Partner quotes (upsert by name) --
|
||||
let quoteCount = 0;
|
||||
for (const q of QUOTES) {
|
||||
const data = { name: q.name, status: q.status, partnerId: partnerIdBySlug.get(q.partnerSlug), opportunityId: oppIdByName.get(q.oppName) };
|
||||
const existing = nodes(await client.query({ partnerQuotes: { __args: { filter: { name: { eq: q.name } }, first: 1 }, edges: { node: { id: true } } } } as any), 'partnerQuotes');
|
||||
if (existing[0]?.id) {
|
||||
await client.mutation({ updatePartnerQuote: { __args: { id: existing[0].id, data }, id: true } } as any);
|
||||
} else {
|
||||
await client.mutation({ createPartnerQuote: { __args: { data }, id: true } } as any);
|
||||
}
|
||||
quoteCount++;
|
||||
}
|
||||
console.log(`[seed] partner quotes: ${quoteCount}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
ViewFilterOperand,
|
||||
ViewType,
|
||||
defineView,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
ALL_MATCHED_DEALS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// Opportunities where a partner is engaged (MATCHED or later). Distinct from
|
||||
// Matches overview, which also includes AUTO_MATCH (in-flight, no partner yet).
|
||||
export default defineView({
|
||||
universalIdentifier: ALL_MATCHED_DEALS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'All matched deals',
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{ universalIdentifier: '76f6aea5-0e0b-4787-84f0-430d0799e913', fieldMetadataUniversalIdentifier: '20202020-8609-4f65-a2d9-44009eb422b5', position: 0, isVisible: true },
|
||||
{ universalIdentifier: 'd9862d49-eff8-4103-9f48-a193cf8e1de2', fieldMetadataUniversalIdentifier: MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER, position: 1, isVisible: true },
|
||||
{ universalIdentifier: '91c42a01-4ec8-4527-b9ac-9bdeb58e7243', fieldMetadataUniversalIdentifier: 'd9eeacaa-2f9e-44cc-b5f6-5e1526256d49', position: 2, isVisible: true },
|
||||
{ universalIdentifier: 'c9689260-86f5-4e19-a86c-7afc95d4d6fe', fieldMetadataUniversalIdentifier: 'cc6b8a59-f860-493f-8b9a-f138c078fbf1', position: 3, isVisible: true },
|
||||
{ universalIdentifier: 'd51c2737-26f8-4f27-b078-7bb0cf58c662', fieldMetadataUniversalIdentifier: 'fcf39b0c-0547-415e-806d-b238131ad7cc', position: 4, isVisible: true },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '71de9b3a-e59b-4baf-99e6-84fe01e037ee',
|
||||
fieldMetadataUniversalIdentifier: MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
// See filter-syntax note in Task 8.1.
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['MATCHED', 'INTRODUCED_TO_A_PARTNER', 'WORKING_WITH_A_PARTNER', 'IMPLEMENTING', 'WON', 'RECONNECT_LATER', 'LOST'],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
ViewType,
|
||||
defineView,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
ALL_OPPORTUNITIES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// Default Opportunities view replacement. Surfaces matchStatus + partner alongside
|
||||
// the standard Opportunity columns. partnerEligible column was dropped as part of
|
||||
// the match-status redesign.
|
||||
export default defineView({
|
||||
universalIdentifier: ALL_OPPORTUNITIES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Opportunities',
|
||||
icon: 'IconTargetArrow',
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{ universalIdentifier: '62844317-546c-4b65-a292-917bf0b5bfce', fieldMetadataUniversalIdentifier: '20202020-8609-4f65-a2d9-44009eb422b5', position: 0, isVisible: true },
|
||||
{ universalIdentifier: '295a5b86-0b37-475a-8645-26f7e7a3dd0a', fieldMetadataUniversalIdentifier: '20202020-cbac-457e-b565-adece5fc815f', position: 1, isVisible: true },
|
||||
{ universalIdentifier: 'ce684df4-4456-427c-b33b-38a34368e380', fieldMetadataUniversalIdentifier: '20202020-6f76-477d-8551-28cd65b2b4b9', position: 2, isVisible: true },
|
||||
{ universalIdentifier: '9f72d1ce-7c39-418c-95cb-480d1b176821', fieldMetadataUniversalIdentifier: 'd9eeacaa-2f9e-44cc-b5f6-5e1526256d49', position: 3, isVisible: true },
|
||||
{ universalIdentifier: '5db9ee26-8688-4a5c-9fe8-f76b41d8e80b', fieldMetadataUniversalIdentifier: MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER, position: 4, isVisible: true },
|
||||
{ universalIdentifier: '3727d213-e3f5-43c7-ab05-b0fb2f211273', fieldMetadataUniversalIdentifier: '20202020-527e-44d6-b1ac-c4158d307b97', position: 5, isVisible: true },
|
||||
{ universalIdentifier: 'c9ad9056-fd3a-448c-b4dc-e95e0c5d22e9', fieldMetadataUniversalIdentifier: '20202020-a63e-4a62-8e63-42a51828f831', position: 6, isVisible: true },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ViewType, defineView } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Partners',
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{ universalIdentifier: '21afcc69-09c5-42eb-a609-26c062de3bd3', fieldMetadataUniversalIdentifier: 'a0000001-0000-4000-8000-000000000001', position: 0, isVisible: true },
|
||||
{ universalIdentifier: '529912f0-38fb-4821-92d3-8a0a68b9f340', fieldMetadataUniversalIdentifier: '2ca9856f-f54a-4326-9ff3-668fd7da0b50', position: 1, isVisible: true },
|
||||
{ universalIdentifier: '68c6b96d-8c3d-4a3e-b4cd-3751d035b085', fieldMetadataUniversalIdentifier: 'a0000004-0000-4000-8000-000000000004', position: 2, isVisible: true },
|
||||
{ universalIdentifier: '8862e4a5-525a-4a0c-8381-93ff0d01ccf0', fieldMetadataUniversalIdentifier: 'a0000007-0000-4000-8000-000000000007', position: 3, isVisible: true },
|
||||
{ universalIdentifier: '4ebe0b9d-0c2d-4416-b187-150b02473a01', fieldMetadataUniversalIdentifier: 'a0000010-0000-4000-8000-000000000010', position: 4, isVisible: true },
|
||||
{ universalIdentifier: '52408b5f-5e13-4e3c-af2d-ce50033ec126', fieldMetadataUniversalIdentifier: '560503de-6330-4c1d-af97-a8dee125f2ad', position: 5, isVisible: true },
|
||||
{ universalIdentifier: '02cc471b-e9ba-4643-b403-40299d6bbbdd', fieldMetadataUniversalIdentifier: 'a0000005-0000-4000-8000-000000000005', position: 6, isVisible: true },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
ViewType,
|
||||
defineView,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
MATCHES_OVERVIEW_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// Full matching funnel: a Kanban grouped by matchStatus, no filter (every
|
||||
// opportunity appears in its matchStatus column).
|
||||
export default defineView({
|
||||
universalIdentifier: MATCHES_OVERVIEW_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Matches overview',
|
||||
icon: 'IconLayoutKanban',
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: ViewType.KANBAN,
|
||||
mainGroupByFieldMetadataUniversalIdentifier: MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{ universalIdentifier: '7a6403c1-7ab9-4c3a-b833-3c028d43140e', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.fields.name.universalIdentifier, position: 0, isVisible: true },
|
||||
{ universalIdentifier: '2e718b4b-fde8-4839-9cdf-deb09db0e6b6', fieldMetadataUniversalIdentifier: 'd9eeacaa-2f9e-44cc-b5f6-5e1526256d49', position: 1, isVisible: true },
|
||||
{ universalIdentifier: '5ae8805c-3d71-4ccc-a2be-38368f32e3e1', fieldMetadataUniversalIdentifier: MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER, position: 2, isVisible: true },
|
||||
{ universalIdentifier: 'cb4e5d2b-7003-4f30-874c-acda310b250c', fieldMetadataUniversalIdentifier: 'fcf39b0c-0547-415e-806d-b238131ad7cc', position: 3, isVisible: true },
|
||||
{ universalIdentifier: '0fc87e70-7aa1-4c85-9152-d0edff8ae8a4', fieldMetadataUniversalIdentifier: 'cc6b8a59-f860-493f-8b9a-f138c078fbf1', position: 4, isVisible: true },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ViewFilterOperand, ViewType, defineView } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_APPLICATIONS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// Partners still in the application stage (not yet validated).
|
||||
export default defineView({
|
||||
universalIdentifier: PARTNER_APPLICATIONS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Partner applications',
|
||||
icon: 'IconUserPlus',
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{ universalIdentifier: 'b4f505d7-3849-4a74-a27f-1c91733702b5', fieldMetadataUniversalIdentifier: 'a0000001-0000-4000-8000-000000000001', position: 0, isVisible: true },
|
||||
{ universalIdentifier: '8a39e510-e533-4cd7-9b65-5e16b5f773d0', fieldMetadataUniversalIdentifier: '2ca9856f-f54a-4326-9ff3-668fd7da0b50', position: 1, isVisible: true },
|
||||
{ universalIdentifier: 'b92d7fd4-4a24-4333-89fe-5d726634d428', fieldMetadataUniversalIdentifier: 'd4fa6461-37b6-49ee-9181-dd482e74a70b', position: 2, isVisible: true },
|
||||
{ universalIdentifier: '2c34a120-b0f8-421b-9546-6483f1202d9f', fieldMetadataUniversalIdentifier: 'a77d7fa6-c398-47db-af0f-036a5c719f20', position: 3, isVisible: true },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '210deb57-cfca-4086-b9da-ca346fbd3126',
|
||||
fieldMetadataUniversalIdentifier: '2ca9856f-f54a-4326-9ff3-668fd7da0b50',
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['APPLICATION'],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ViewType, defineView } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_QUOTES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// Index view for partner quotes.
|
||||
export default defineView({
|
||||
universalIdentifier: PARTNER_QUOTES_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Partner quotes',
|
||||
icon: 'IconFileDollar',
|
||||
objectUniversalIdentifier: PARTNER_QUOTE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{ universalIdentifier: 'ad7b3702-b552-4355-afec-1e1e96d9f3df', fieldMetadataUniversalIdentifier: '9e688624-83d2-4715-8b18-80492a6de2b6', position: 0, isVisible: true },
|
||||
{ universalIdentifier: '426e0c2d-449d-4a06-860b-0cfe0ed501e6', fieldMetadataUniversalIdentifier: 'a0fe09c4-c1f4-4b96-93c6-d7ec38f1166a', position: 1, isVisible: true },
|
||||
{ universalIdentifier: '62bb1536-539c-4fb0-95bf-440c5c5da89f', fieldMetadataUniversalIdentifier: '2cbe67e3-24ec-421b-bab5-50f3306c2391', position: 2, isVisible: true },
|
||||
{ universalIdentifier: 'fbd1f953-1dd2-4d0f-a239-148a0688fbff', fieldMetadataUniversalIdentifier: 'b52d263e-423e-40b0-b82c-29214597c005', position: 3, isVisible: true },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ViewFilterOperand, ViewType, defineView } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
VALIDATED_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// Validated partners — serves both partner intros and the public website list.
|
||||
export default defineView({
|
||||
universalIdentifier: VALIDATED_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Validated partners',
|
||||
icon: 'IconCircleCheck',
|
||||
objectUniversalIdentifier: PARTNER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
type: ViewType.TABLE,
|
||||
fields: [
|
||||
{ universalIdentifier: '9463bf58-69d5-4309-bc6e-4835df346246', fieldMetadataUniversalIdentifier: 'a0000001-0000-4000-8000-000000000001', position: 0, isVisible: true },
|
||||
{ universalIdentifier: 'c8fd88c5-ffa9-4944-b5b9-deec3acd3dff', fieldMetadataUniversalIdentifier: 'd4fa6461-37b6-49ee-9181-dd482e74a70b', position: 1, isVisible: true },
|
||||
{ universalIdentifier: '9cb93542-2a91-4e75-a9f8-4a1866445322', fieldMetadataUniversalIdentifier: '500021ad-ca42-4fd3-8727-392dd26b722a', position: 2, isVisible: true },
|
||||
{ universalIdentifier: 'd6df98ac-9c6c-46c2-8784-d4e1d6521f75', fieldMetadataUniversalIdentifier: '560503de-6330-4c1d-af97-a8dee125f2ad', position: 3, isVisible: true },
|
||||
{ universalIdentifier: '375cc871-fdda-42d0-8308-e60174b6d467', fieldMetadataUniversalIdentifier: 'a0000004-0000-4000-8000-000000000004', position: 4, isVisible: true },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: 'a3b7e215-693d-420c-b444-37d4109ca535',
|
||||
fieldMetadataUniversalIdentifier: '2ca9856f-f54a-4326-9ff3-668fd7da0b50',
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['VALIDATED'],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
ViewFilterOperand,
|
||||
ViewSortDirection,
|
||||
ViewType,
|
||||
defineView,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
WAITING_FOR_MATCH_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
// Ops inbox: opportunities awaiting a human matching decision.
|
||||
// Includes TO_BE_MATCHED (default for new opps) and MANUAL_MATCH
|
||||
// (opps that auto-match couldn't resolve or that ops opted to handle manually).
|
||||
export default defineView({
|
||||
universalIdentifier: WAITING_FOR_MATCH_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Waiting for match',
|
||||
icon: 'IconClockHour4',
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
|
||||
type: ViewType.KANBAN,
|
||||
mainGroupByFieldMetadataUniversalIdentifier: MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{ universalIdentifier: 'd74b5eb3-21ee-48fa-b703-4cfd629738b4', fieldMetadataUniversalIdentifier: '20202020-8609-4f65-a2d9-44009eb422b5', position: 0, isVisible: true },
|
||||
{ universalIdentifier: '50822cf9-c238-4450-ba31-5807011afa65', fieldMetadataUniversalIdentifier: '20202020-cbac-457e-b565-adece5fc815f', position: 1, isVisible: true },
|
||||
{ universalIdentifier: 'f432a71f-3bd0-495e-b5b1-8a78e155dc5a', fieldMetadataUniversalIdentifier: '20202020-d01b-4132-9b32-123456789abc', position: 2, isVisible: true },
|
||||
{ universalIdentifier: '9e47592f-9965-4ee7-9c6a-303477b293f4', fieldMetadataUniversalIdentifier: 'cc6b8a59-f860-493f-8b9a-f138c078fbf1', position: 3, isVisible: true },
|
||||
// matchStatus column (replaces the dropped partnerEligible column)
|
||||
{ universalIdentifier: '909e1eee-077a-4f23-8c9b-4c8027623a78', fieldMetadataUniversalIdentifier: MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER, position: 4, isVisible: true },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: '93476207-1471-49d9-898c-f8a1d52f468f',
|
||||
fieldMetadataUniversalIdentifier: MATCH_STATUS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['TO_BE_MATCHED', 'MANUAL_MATCH'],
|
||||
},
|
||||
],
|
||||
sorts: [
|
||||
{
|
||||
universalIdentifier: 'a7c5a89e-d9d7-4cf6-a6d2-3ad9f12a7b1f',
|
||||
fieldMetadataUniversalIdentifier: '20202020-d01b-4132-9b32-123456789abc',
|
||||
direction: ViewSortDirection.ASC,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"src/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.integration-test.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["vitest/globals", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { loadEnv } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
// Integration tests authenticate to a local Twenty server. Credentials are
|
||||
// resolved from the shell env, then a gitignored .env.local in this directory
|
||||
// (see .env.example). No API key is committed; if none is found, global-setup
|
||||
// fails with a clear message.
|
||||
const fileEnv = loadEnv('test', process.cwd(), 'TWENTY_');
|
||||
|
||||
const TWENTY_API_URL =
|
||||
process.env.TWENTY_API_URL ?? fileEnv.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? fileEnv.TWENTY_API_KEY;
|
||||
|
||||
// Make env available to globalSetup (runs in the main process); test.env below
|
||||
// covers the worker processes.
|
||||
process.env.TWENTY_API_URL = TWENTY_API_URL;
|
||||
if (TWENTY_API_KEY) {
|
||||
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
fileParallelism: false,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
globalSetup: ['src/__tests__/global-setup.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL,
|
||||
...(TWENTY_API_KEY ? { TWENTY_API_KEY } : {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1764,6 +1764,18 @@ type WorkspaceUrls {
|
||||
subdomainUrl: String!
|
||||
}
|
||||
|
||||
type ApplicationRegistrationVariableDTO {
|
||||
id: UUID!
|
||||
key: String!
|
||||
value: String
|
||||
description: String!
|
||||
isSecret: Boolean!
|
||||
isRequired: Boolean!
|
||||
isFilled: Boolean!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type BillingTrialPeriod {
|
||||
duration: Float!
|
||||
isCreditCardRequired: Boolean!
|
||||
@@ -1968,18 +1980,6 @@ type RotateClientSecret {
|
||||
clientSecret: String!
|
||||
}
|
||||
|
||||
type ApplicationRegistrationVariableDTO {
|
||||
id: UUID!
|
||||
key: String!
|
||||
value: String
|
||||
description: String!
|
||||
isSecret: Boolean!
|
||||
isRequired: Boolean!
|
||||
isFilled: Boolean!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Relation {
|
||||
type: RelationType!
|
||||
sourceObjectMetadata: Object!
|
||||
@@ -3294,7 +3294,6 @@ type Mutation {
|
||||
updateWorkspace(data: UpdateWorkspaceInput!): Workspace!
|
||||
deleteCurrentWorkspace: Workspace!
|
||||
checkCustomDomainValidRecords: DomainValidRecords
|
||||
installApplication(appRegistrationId: String!, version: String): Boolean!
|
||||
runWorkspaceMigration(workspaceMigration: WorkspaceMigrationInput!): Boolean!
|
||||
uninstallApplication(universalIdentifier: String!): Boolean!
|
||||
createOIDCIdentityProvider(input: SetupOIDCSsoInput!): SetupSso!
|
||||
@@ -3315,7 +3314,8 @@ type Mutation {
|
||||
deleteEmailingDomain(id: String!): Boolean!
|
||||
verifyEmailingDomain(id: String!): EmailingDomain!
|
||||
createOneAppToken(input: CreateOneAppTokenInput!): AppToken!
|
||||
installMarketplaceApp(universalIdentifier: String!, version: String): Boolean!
|
||||
installMarketplaceApp(universalIdentifier: String!, version: String): Boolean! @deprecated(reason: "Use installApplication instead")
|
||||
installApplication(universalIdentifier: String!, version: String): Application!
|
||||
syncMarketplaceCatalog: Boolean!
|
||||
createDevelopmentApplication(universalIdentifier: String!, name: String!): DevelopmentApplication!
|
||||
generateApplicationToken(applicationId: UUID!): ApplicationTokenPair!
|
||||
|
||||
@@ -1401,6 +1401,19 @@ export interface WorkspaceUrls {
|
||||
__typename: 'WorkspaceUrls'
|
||||
}
|
||||
|
||||
export interface ApplicationRegistrationVariableDTO {
|
||||
id: Scalars['UUID']
|
||||
key: Scalars['String']
|
||||
value?: Scalars['String']
|
||||
description: Scalars['String']
|
||||
isSecret: Scalars['Boolean']
|
||||
isRequired: Scalars['Boolean']
|
||||
isFilled: Scalars['Boolean']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'ApplicationRegistrationVariableDTO'
|
||||
}
|
||||
|
||||
export interface BillingTrialPeriod {
|
||||
duration: Scalars['Float']
|
||||
isCreditCardRequired: Scalars['Boolean']
|
||||
@@ -1609,19 +1622,6 @@ export interface RotateClientSecret {
|
||||
__typename: 'RotateClientSecret'
|
||||
}
|
||||
|
||||
export interface ApplicationRegistrationVariableDTO {
|
||||
id: Scalars['UUID']
|
||||
key: Scalars['String']
|
||||
value?: Scalars['String']
|
||||
description: Scalars['String']
|
||||
isSecret: Scalars['Boolean']
|
||||
isRequired: Scalars['Boolean']
|
||||
isFilled: Scalars['Boolean']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
__typename: 'ApplicationRegistrationVariableDTO'
|
||||
}
|
||||
|
||||
export interface Relation {
|
||||
type: RelationType
|
||||
sourceObjectMetadata: Object
|
||||
@@ -2827,7 +2827,6 @@ export interface Mutation {
|
||||
updateWorkspace: Workspace
|
||||
deleteCurrentWorkspace: Workspace
|
||||
checkCustomDomainValidRecords?: DomainValidRecords
|
||||
installApplication: Scalars['Boolean']
|
||||
runWorkspaceMigration: Scalars['Boolean']
|
||||
uninstallApplication: Scalars['Boolean']
|
||||
createOIDCIdentityProvider: SetupSso
|
||||
@@ -2848,7 +2847,9 @@ export interface Mutation {
|
||||
deleteEmailingDomain: Scalars['Boolean']
|
||||
verifyEmailingDomain: EmailingDomain
|
||||
createOneAppToken: AppToken
|
||||
/** @deprecated Use installApplication instead */
|
||||
installMarketplaceApp: Scalars['Boolean']
|
||||
installApplication: Application
|
||||
syncMarketplaceCatalog: Scalars['Boolean']
|
||||
createDevelopmentApplication: DevelopmentApplication
|
||||
generateApplicationToken: ApplicationTokenPair
|
||||
@@ -4332,6 +4333,20 @@ export interface WorkspaceUrlsGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApplicationRegistrationVariableDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
key?: boolean | number
|
||||
value?: boolean | number
|
||||
description?: boolean | number
|
||||
isSecret?: boolean | number
|
||||
isRequired?: boolean | number
|
||||
isFilled?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface BillingTrialPeriodGenqlSelection{
|
||||
duration?: boolean | number
|
||||
isCreditCardRequired?: boolean | number
|
||||
@@ -4553,20 +4568,6 @@ export interface RotateClientSecretGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApplicationRegistrationVariableDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
key?: boolean | number
|
||||
value?: boolean | number
|
||||
description?: boolean | number
|
||||
isSecret?: boolean | number
|
||||
isRequired?: boolean | number
|
||||
isFilled?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface RelationGenqlSelection{
|
||||
type?: boolean | number
|
||||
sourceObjectMetadata?: ObjectGenqlSelection
|
||||
@@ -5879,7 +5880,6 @@ export interface MutationGenqlSelection{
|
||||
updateWorkspace?: (WorkspaceGenqlSelection & { __args: {data: UpdateWorkspaceInput} })
|
||||
deleteCurrentWorkspace?: WorkspaceGenqlSelection
|
||||
checkCustomDomainValidRecords?: DomainValidRecordsGenqlSelection
|
||||
installApplication?: { __args: {appRegistrationId: Scalars['String'], version?: (Scalars['String'] | null)} }
|
||||
runWorkspaceMigration?: { __args: {workspaceMigration: WorkspaceMigrationInput} }
|
||||
uninstallApplication?: { __args: {universalIdentifier: Scalars['String']} }
|
||||
createOIDCIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupOIDCSsoInput} })
|
||||
@@ -5900,7 +5900,9 @@ export interface MutationGenqlSelection{
|
||||
deleteEmailingDomain?: { __args: {id: Scalars['String']} }
|
||||
verifyEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
createOneAppToken?: (AppTokenGenqlSelection & { __args: {input: CreateOneAppTokenInput} })
|
||||
/** @deprecated Use installApplication instead */
|
||||
installMarketplaceApp?: { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} }
|
||||
installApplication?: (ApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], version?: (Scalars['String'] | null)} })
|
||||
syncMarketplaceCatalog?: boolean | number
|
||||
createDevelopmentApplication?: (DevelopmentApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], name: Scalars['String']} })
|
||||
generateApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
|
||||
@@ -7183,6 +7185,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ApplicationRegistrationVariableDTO_possibleTypes: string[] = ['ApplicationRegistrationVariableDTO']
|
||||
export const isApplicationRegistrationVariableDTO = (obj?: { __typename?: any } | null): obj is ApplicationRegistrationVariableDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationRegistrationVariableDTO"')
|
||||
return ApplicationRegistrationVariableDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const BillingTrialPeriod_possibleTypes: string[] = ['BillingTrialPeriod']
|
||||
export const isBillingTrialPeriod = (obj?: { __typename?: any } | null): obj is BillingTrialPeriod => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingTrialPeriod"')
|
||||
@@ -7367,14 +7377,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ApplicationRegistrationVariableDTO_possibleTypes: string[] = ['ApplicationRegistrationVariableDTO']
|
||||
export const isApplicationRegistrationVariableDTO = (obj?: { __typename?: any } | null): obj is ApplicationRegistrationVariableDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationRegistrationVariableDTO"')
|
||||
return ApplicationRegistrationVariableDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Relation_possibleTypes: string[] = ['Relation']
|
||||
export const isRelation = (obj?: { __typename?: any } | null): obj is Relation => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isRelation"')
|
||||
|
||||
@@ -51,11 +51,11 @@ export default {
|
||||
163,
|
||||
167,
|
||||
169,
|
||||
173,
|
||||
174,
|
||||
181,
|
||||
184,
|
||||
187,
|
||||
175,
|
||||
182,
|
||||
185,
|
||||
188,
|
||||
201,
|
||||
226,
|
||||
262,
|
||||
@@ -3434,6 +3434,38 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"ApplicationRegistrationVariableDTO": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"key": [
|
||||
1
|
||||
],
|
||||
"value": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"isSecret": [
|
||||
6
|
||||
],
|
||||
"isRequired": [
|
||||
6
|
||||
],
|
||||
"isFilled": [
|
||||
6
|
||||
],
|
||||
"createdAt": [
|
||||
4
|
||||
],
|
||||
"updatedAt": [
|
||||
4
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingTrialPeriod": {
|
||||
"duration": [
|
||||
11
|
||||
@@ -3453,10 +3485,10 @@ export default {
|
||||
1
|
||||
],
|
||||
"type": [
|
||||
173
|
||||
174
|
||||
],
|
||||
"status": [
|
||||
174
|
||||
175
|
||||
],
|
||||
"issuer": [
|
||||
1
|
||||
@@ -3469,7 +3501,7 @@ export default {
|
||||
"SSOIdentityProviderStatus": {},
|
||||
"AuthProviders": {
|
||||
"sso": [
|
||||
172
|
||||
173
|
||||
],
|
||||
"google": [
|
||||
6
|
||||
@@ -3506,10 +3538,10 @@ export default {
|
||||
3
|
||||
],
|
||||
"authProviders": [
|
||||
175
|
||||
176
|
||||
],
|
||||
"authBypassProviders": [
|
||||
176
|
||||
177
|
||||
],
|
||||
"logo": [
|
||||
1
|
||||
@@ -3557,7 +3589,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"modelFamily": [
|
||||
181
|
||||
182
|
||||
],
|
||||
"modelFamilyLabel": [
|
||||
1
|
||||
@@ -3572,7 +3604,7 @@ export default {
|
||||
11
|
||||
],
|
||||
"nativeCapabilities": [
|
||||
179
|
||||
180
|
||||
],
|
||||
"isDeprecated": [
|
||||
6
|
||||
@@ -3608,7 +3640,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"trialPeriods": [
|
||||
171
|
||||
172
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -3616,7 +3648,7 @@ export default {
|
||||
},
|
||||
"Support": {
|
||||
"supportDriver": [
|
||||
184
|
||||
185
|
||||
],
|
||||
"supportFrontChatId": [
|
||||
1
|
||||
@@ -3642,7 +3674,7 @@ export default {
|
||||
},
|
||||
"Captcha": {
|
||||
"provider": [
|
||||
187
|
||||
188
|
||||
],
|
||||
"siteKey": [
|
||||
1
|
||||
@@ -3679,7 +3711,7 @@ export default {
|
||||
169
|
||||
],
|
||||
"metadata": [
|
||||
189
|
||||
190
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -3704,13 +3736,13 @@ export default {
|
||||
1
|
||||
],
|
||||
"authProviders": [
|
||||
175
|
||||
176
|
||||
],
|
||||
"billing": [
|
||||
182
|
||||
183
|
||||
],
|
||||
"aiModels": [
|
||||
180
|
||||
181
|
||||
],
|
||||
"signInPrefilled": [
|
||||
6
|
||||
@@ -3731,25 +3763,25 @@ export default {
|
||||
6
|
||||
],
|
||||
"support": [
|
||||
183
|
||||
184
|
||||
],
|
||||
"isAttachmentPreviewEnabled": [
|
||||
6
|
||||
],
|
||||
"sentry": [
|
||||
185
|
||||
],
|
||||
"captcha": [
|
||||
186
|
||||
],
|
||||
"captcha": [
|
||||
187
|
||||
],
|
||||
"api": [
|
||||
188
|
||||
189
|
||||
],
|
||||
"canManageFeatureFlags": [
|
||||
6
|
||||
],
|
||||
"publicFeatureFlags": [
|
||||
190
|
||||
191
|
||||
],
|
||||
"isMicrosoftMessagingEnabled": [
|
||||
6
|
||||
@@ -3788,7 +3820,7 @@ export default {
|
||||
6
|
||||
],
|
||||
"maintenance": [
|
||||
191
|
||||
192
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -3827,7 +3859,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"versionDistribution": [
|
||||
194
|
||||
195
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -3872,38 +3904,6 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"ApplicationRegistrationVariableDTO": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"key": [
|
||||
1
|
||||
],
|
||||
"value": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"isSecret": [
|
||||
6
|
||||
],
|
||||
"isRequired": [
|
||||
6
|
||||
],
|
||||
"isFilled": [
|
||||
6
|
||||
],
|
||||
"createdAt": [
|
||||
4
|
||||
],
|
||||
"updatedAt": [
|
||||
4
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"Relation": {
|
||||
"type": [
|
||||
201
|
||||
@@ -4095,7 +4095,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"type": [
|
||||
173
|
||||
174
|
||||
],
|
||||
"issuer": [
|
||||
1
|
||||
@@ -4104,7 +4104,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"status": [
|
||||
174
|
||||
175
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4123,7 +4123,7 @@ export default {
|
||||
},
|
||||
"FindAvailableSSOIDP": {
|
||||
"type": [
|
||||
173
|
||||
174
|
||||
],
|
||||
"id": [
|
||||
3
|
||||
@@ -4135,7 +4135,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"status": [
|
||||
174
|
||||
175
|
||||
],
|
||||
"workspace": [
|
||||
218
|
||||
@@ -4149,7 +4149,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"type": [
|
||||
173
|
||||
174
|
||||
],
|
||||
"issuer": [
|
||||
1
|
||||
@@ -4158,7 +4158,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"status": [
|
||||
174
|
||||
175
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4166,7 +4166,7 @@ export default {
|
||||
},
|
||||
"SSOConnection": {
|
||||
"type": [
|
||||
173
|
||||
174
|
||||
],
|
||||
"id": [
|
||||
3
|
||||
@@ -4178,7 +4178,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"status": [
|
||||
174
|
||||
175
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4529,13 +4529,13 @@ export default {
|
||||
},
|
||||
"UsageAnalytics": {
|
||||
"usageByUser": [
|
||||
193
|
||||
194
|
||||
],
|
||||
"usageByOperationType": [
|
||||
193
|
||||
194
|
||||
],
|
||||
"usageByModel": [
|
||||
193
|
||||
194
|
||||
],
|
||||
"timeSeries": [
|
||||
251
|
||||
@@ -6293,7 +6293,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"findApplicationRegistrationByClientId": [
|
||||
197,
|
||||
198,
|
||||
{
|
||||
"clientId": [
|
||||
1,
|
||||
@@ -6323,7 +6323,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"findApplicationRegistrationStats": [
|
||||
195,
|
||||
196,
|
||||
{
|
||||
"id": [
|
||||
1,
|
||||
@@ -6332,7 +6332,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"findApplicationRegistrationVariables": [
|
||||
199,
|
||||
171,
|
||||
{
|
||||
"applicationRegistrationId": [
|
||||
1,
|
||||
@@ -6356,7 +6356,7 @@ export default {
|
||||
75
|
||||
],
|
||||
"getPublicWorkspaceDataByDomain": [
|
||||
177,
|
||||
178,
|
||||
{
|
||||
"origin": [
|
||||
1
|
||||
@@ -6364,7 +6364,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"getPublicWorkspaceDataById": [
|
||||
178,
|
||||
179,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -8197,7 +8197,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"createApplicationRegistration": [
|
||||
196,
|
||||
197,
|
||||
{
|
||||
"input": [
|
||||
443,
|
||||
@@ -8224,7 +8224,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"rotateApplicationRegistrationClientSecret": [
|
||||
198,
|
||||
199,
|
||||
{
|
||||
"id": [
|
||||
1,
|
||||
@@ -8388,18 +8388,6 @@ export default {
|
||||
"checkCustomDomainValidRecords": [
|
||||
228
|
||||
],
|
||||
"installApplication": [
|
||||
6,
|
||||
{
|
||||
"appRegistrationId": [
|
||||
1,
|
||||
"String!"
|
||||
],
|
||||
"version": [
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"runWorkspaceMigration": [
|
||||
6,
|
||||
{
|
||||
@@ -8613,6 +8601,18 @@ export default {
|
||||
]
|
||||
}
|
||||
],
|
||||
"installApplication": [
|
||||
58,
|
||||
{
|
||||
"universalIdentifier": [
|
||||
1,
|
||||
"String!"
|
||||
],
|
||||
"version": [
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"syncMarketplaceCatalog": [
|
||||
6
|
||||
],
|
||||
@@ -11010,7 +11010,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"status": [
|
||||
174
|
||||
175
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
|
||||
@@ -86,7 +86,11 @@ RUN if [ -d /app/packages/twenty-front/build ]; then \
|
||||
|
||||
FROM node:24.15.0-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS twenty-server
|
||||
|
||||
RUN apk add --no-cache curl jq postgresql-client
|
||||
RUN apk add --no-cache \
|
||||
'curl>=8.19.0-r0' \
|
||||
'nghttp2-libs>=1.69.0-r0' \
|
||||
'postgresql18-client>=18.4-r0' \
|
||||
jq
|
||||
|
||||
COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"ignorePatterns": ["node_modules", ".next", "storybook-static"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
|
||||
"no-console": [
|
||||
"warn",
|
||||
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
|
||||
],
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
@@ -13,22 +16,31 @@
|
||||
"no-redeclare": "off",
|
||||
"import/no-duplicates": "error",
|
||||
"typescript/no-redeclare": "error",
|
||||
"typescript/consistent-type-imports": ["error", {
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "inline-type-imports"
|
||||
}],
|
||||
"typescript/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "inline-type-imports"
|
||||
}
|
||||
],
|
||||
"typescript/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"typescript/no-empty-object-type": ["error", {
|
||||
"allowInterfaces": "with-single-extends"
|
||||
}],
|
||||
"typescript/no-empty-object-type": [
|
||||
"error",
|
||||
{
|
||||
"allowInterfaces": "with-single-extends"
|
||||
}
|
||||
],
|
||||
"typescript/no-empty-function": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-unused-vars": ["warn", {
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}]
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
opacity: 0.85 !important;
|
||||
}
|
||||
|
||||
:is(.dark, [data-theme="dark"]) #topbar-cta-button a,
|
||||
:is(.dark, [data-theme="dark"]) #topbar-cta-button a span {
|
||||
:is(.dark, [data-theme='dark']) #topbar-cta-button a,
|
||||
:is(.dark, [data-theme='dark']) #topbar-cta-button a span {
|
||||
background-color: #ffffff !important;
|
||||
color: #141414 !important;
|
||||
}
|
||||
@@ -35,14 +35,14 @@
|
||||
* We use :has() to scope the rule to only the sidebar group that contains
|
||||
* the developers/introduction link, so other tabs are unaffected.
|
||||
*/
|
||||
div:has(> .sidebar-group a[href="/developers/introduction"]),
|
||||
div:has(> .sidebar-group a[href="/user-guide/introduction"]) {
|
||||
div:has(> .sidebar-group a[href='/developers/introduction']),
|
||||
div:has(> .sidebar-group a[href='/user-guide/introduction']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Remove the top margin on the group that follows the hidden overview group,
|
||||
so it sits flush at the top of the sidebar without a gap. */
|
||||
div:has(> .sidebar-group a[href="/developers/introduction"]) + div,
|
||||
div:has(> .sidebar-group a[href="/user-guide/introduction"]) + div {
|
||||
div:has(> .sidebar-group a[href='/developers/introduction']) + div,
|
||||
div:has(> .sidebar-group a[href='/user-guide/introduction']) + div {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,22 @@ Klicken Sie auf **+ Regel hinzufügen** und wählen Sie ein Objekt aus, um eine
|
||||
| Opportunities → „Datensätze ansehen“ deaktivieren | Praktikant kann das Opportunities-Objekt überhaupt nicht sehen |
|
||||
| People → „Datensätze bearbeiten“ aktivieren | Praktikant kann People-Datensätze bearbeiten (aber keine anderen Objekte) |
|
||||
|
||||
### Berechtigungen auf Zeilenebene
|
||||
|
||||
<Note>
|
||||
Berechtigungen auf Zeilenebene sind ein **Premium-Feature**, das im Tarif **Organization** (Cloud und Self-Hosted) verfügbar ist.
|
||||
</Note>
|
||||
|
||||
Berechtigungen auf Zeilenebene ermöglichen es dir, anhand dynamischer Kriterien einzuschränken, welche einzelnen Datensätze eine Rolle sehen oder bearbeiten kann. Im Gegensatz zu Objektberechtigungen (die für einen gesamten Objekttyp gelten) werden Berechtigungen auf Zeilenebene für jeden Datensatz einzeln ausgewertet.
|
||||
|
||||
**Beispielanwendungsfälle:**
|
||||
|
||||
* Vertriebsmitarbeitende können nur ihre eigenen Opportunities sehen
|
||||
* Manager:innen können alle Datensätze in ihrer Region sehen
|
||||
* Support-Mitarbeitende können nur Tickets sehen, die ihnen zugewiesen sind
|
||||
|
||||
Um Berechtigungen auf Zeilenebene zu konfigurieren, öffne eine Rolle, navigiere zum Tab **Objects** und verwende den Abschnitt **Row-Level**, um Filterbedingungen für ein bestimmtes Objekt zu definieren.
|
||||
|
||||
### Feldberechtigungen
|
||||
|
||||
Innerhalb jeder Regel auf Objektebene können Sie weitergehen und **Berechtigungen auf Feldebene** konfigurieren, um den Zugriff auf bestimmte Felder zu steuern.
|
||||
|
||||
@@ -60,7 +60,7 @@ Für Felder:
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Kann ich den Zugriff auf bestimmte Datensätze einschränken (Berechtigungen auf Zeilenebene)?">
|
||||
Berechtigungen auf Zeilenebene werden bis Q1 2026 im Tarif **Organization** verfügbar sein. Damit können Sie den Zugriff auf bestimmte Datensätze anhand von Kriterien einschränken (z. B. nur die eigenen Verkaufschancen sehen).
|
||||
Berechtigungen auf Zeilenebene sind im Tarif **Organization** verfügbar. Damit können Sie den Zugriff auf bestimmte Datensätze anhand von Kriterien einschränken (z. B. nur die eigenen Verkaufschancen sehen).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Wie mache ich ein Feld für bestimmte Benutzer schreibgeschützt?">
|
||||
|
||||
@@ -98,6 +98,22 @@ Clique em **+ Adicionar regra** e selecione um objeto para criar uma exceção.
|
||||
| Oportunidades → desativar "Ver registros" | O estagiário não consegue ver o objeto Oportunidades. |
|
||||
| Pessoas → ativar "Editar registros" | O estagiário pode editar registros de Pessoas (mas não de outros objetos) |
|
||||
|
||||
### Permissões em Nível de Linha
|
||||
|
||||
<Note>
|
||||
As permissões em nível de linha são um **recurso Premium** disponível no plano **Organization** (Cloud e Self-Hosted).
|
||||
</Note>
|
||||
|
||||
As permissões em nível de linha permitem restringir quais registros individuais uma função pode ver ou editar, com base em critérios dinâmicos. Diferente das permissões de objeto (que se aplicam a todo um tipo de objeto), as permissões em nível de linha avaliam cada registro de forma independente.
|
||||
|
||||
**Casos de uso de exemplo:**
|
||||
|
||||
* Representantes de vendas só podem ver suas próprias oportunidades
|
||||
* Gerentes podem ver todos os registros em sua região
|
||||
* Agentes de suporte só podem visualizar tickets atribuídos a eles
|
||||
|
||||
Para configurar permissões em nível de linha, abra uma função, navegue até a guia **Objects** e use a seção **Row-Level** para definir condições de filtro para um objeto específico.
|
||||
|
||||
### Permissões de Campos
|
||||
|
||||
Em cada regra em nível de objeto, você pode ir além e configurar **permissões em nível de campo** para controlar o acesso a campos específicos.
|
||||
|
||||
@@ -60,7 +60,7 @@ Para campos:
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Posso restringir o acesso a registros específicos (permissões em nível de linha)?">
|
||||
As permissões em nível de linha estarão disponíveis no plano **Organization** até o 1º trimestre de 2026. Isso permite restringir o acesso a registros específicos com base em critérios (por exemplo, ver apenas suas próprias oportunidades).
|
||||
As permissões em nível de linha estão disponíveis no plano **Organization**. Isso permite restringir o acesso a registros específicos com base em critérios (por exemplo, ver apenas suas próprias oportunidades).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Como deixar um campo somente leitura para determinados usuários?">
|
||||
|
||||
@@ -98,6 +98,22 @@ Faceți clic pe **+ Add rule** și selectați un obiect pentru a crea o excepți
|
||||
| Opportunities → dezactivați "See Records" | Internul nu poate vedea deloc obiectul Opportunities |
|
||||
| People → activați "Edit Records" | Internul poate edita înregistrările People (dar nu și alte obiecte) |
|
||||
|
||||
### Permisiuni la Nivel de Rând
|
||||
|
||||
<Note>
|
||||
Permisiunile la nivel de rând sunt o **funcționalitate Premium** disponibilă în planul **Organization** (Cloud și Self-Hosted).
|
||||
</Note>
|
||||
|
||||
Permisiunile la nivel de rând îți permit să restricționezi ce înregistrări individuale poate vedea sau edita un rol, pe baza unor criterii dinamice. Spre deosebire de permisiunile la nivel de obiect (care se aplică unui întreg tip de obiect), permisiunile la nivel de rând evaluează independent fiecare înregistrare.
|
||||
|
||||
**Exemple de cazuri de utilizare:**
|
||||
|
||||
* Reprezentanții de vânzări pot vedea doar oportunitățile proprii
|
||||
* Managerii pot vedea toate înregistrările din regiunea lor
|
||||
* Agenții de suport pot vizualiza doar tichetele alocate lor
|
||||
|
||||
Pentru a configura permisiunile la nivel de rând, deschide un rol, navighează la fila **Objects** și folosește secțiunea **Row-Level** pentru a defini condițiile de filtrare pentru un anumit obiect.
|
||||
|
||||
### Permisiuni pentru câmpuri
|
||||
|
||||
În cadrul fiecărei reguli la nivel de obiect, puteți merge mai departe și configura **permisiuni la nivel de câmp** pentru a controla accesul la câmpuri specifice.
|
||||
|
||||
@@ -60,7 +60,7 @@ Pentru câmpuri:
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Pot restricționa accesul la anumite înregistrări (permisiuni la nivel de rând)?">
|
||||
Permisiunile la nivel de rând vor fi disponibile în planul **Organization** până în trimestrul I 2026. Acest lucru vă permite să restricționați accesul la anumite înregistrări pe baza unor criterii (de exemplu, să vedeți doar propriile oportunități).
|
||||
Permisiunile la nivel de rând sunt disponibile în planul **Organization**. Acest lucru vă permite să restricționați accesul la anumite înregistrări pe baza unor criterii (de exemplu, să vedeți doar propriile oportunități).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Cum fac un câmp doar pentru citire pentru anumiți utilizatori?">
|
||||
|
||||
@@ -98,6 +98,22 @@ description: Управляйте доступом к объектам, поля
|
||||
| Opportunities → отключить "Просмотр записей" | Стажёр вообще не видит объект Opportunities |
|
||||
| People → включить "Редактирование записей" | Стажёр может редактировать записи People (но не других объектов) |
|
||||
|
||||
### Разрешения на уровне строк
|
||||
|
||||
<Note>
|
||||
Разрешения на уровне строк — это **премиальная функция**, доступная в тарифе **Organization** (Cloud и Self-Hosted).
|
||||
</Note>
|
||||
|
||||
Разрешения на уровне строк позволяют ограничить, какие отдельные записи может просматривать или редактировать роль, на основе динамических критериев. В отличие от разрешений на объекты (которые применяются ко всему типу объекта), разрешения на уровне строк оценивают каждую запись независимо.
|
||||
|
||||
**Примеры случаев использования:**
|
||||
|
||||
* Торговые представители могут видеть только свои сделки
|
||||
* Менеджеры могут видеть все записи в своём регионе
|
||||
* Сотрудники службы поддержки могут просматривать только назначенные им заявки
|
||||
|
||||
Чтобы настроить разрешения на уровне строк, откройте роль, перейдите на вкладку **Objects** и используйте раздел **Row-Level** для определения условий фильтрации для конкретного объекта.
|
||||
|
||||
### Разрешения на уровне поля
|
||||
|
||||
В рамках каждого правила на уровне объекта вы можете дополнительно настроить **разрешения на уровне поля**, чтобы управлять доступом к конкретным полям.
|
||||
|
||||
@@ -60,7 +60,7 @@ description: Часто задаваемые вопросы о ролях и р
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Могу ли я ограничить доступ к отдельным записям (разрешения на уровне строки)?">
|
||||
Разрешения на уровне строки будут доступны в тарифе **Organization** к первому кварталу 2026 года. Это позволит ограничивать доступ к отдельным записям на основе критериев (например, видеть только собственные сделки).
|
||||
Разрешения на уровне строки доступны в тарифе **Organization**. Это позволит ограничивать доступ к отдельным записям на основе критериев (например, видеть только собственные сделки).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Как сделать поле доступным только для чтения для отдельных пользователей?">
|
||||
|
||||
@@ -98,6 +98,22 @@ Varsayılanları ayarladıktan sonra, belirli nesneler için varsayılanları ge
|
||||
| Fırsatlar → "Kayıtları Gör"ü devre dışı bırakın | Stajyer Fırsatlar nesnesini hiç göremez |
|
||||
| Kişiler → "Kayıtları Düzenle"yi etkinleştirin | Stajyer Kişiler kayıtlarını düzenleyebilir (ancak diğer nesneleri düzenleyemez) |
|
||||
|
||||
### Satır Seviyesi İzinleri
|
||||
|
||||
<Note>
|
||||
Satır seviyesi izinler, **Organization** planında (Bulut ve Kendi Sunucunuzda) sunulan bir **Premium özellik**tir.
|
||||
</Note>
|
||||
|
||||
Satır seviyesi izinler, bir rolün dinamik ölçütlere göre hangi tekil kayıtları görebileceğini veya düzenleyebileceğini kısıtlamanıza olanak tanır. Nesne izinlerinden (tüm bir nesne türüne uygulanan) farklı olarak, satır seviyesi izinler her kaydı bağımsız olarak değerlendirir.
|
||||
|
||||
**Örnek Kullanım Durumları:**
|
||||
|
||||
* Satış temsilcileri yalnızca kendi fırsatlarını görebilir
|
||||
* Yöneticiler kendi bölgelerindeki tüm kayıtları görebilir
|
||||
* Destek temsilcileri yalnızca kendilerine atanmış talepleri görüntüleyebilir
|
||||
|
||||
Satır seviyesi izinleri yapılandırmak için bir rol açın, **Nesneler** sekmesine gidin ve belirli bir nesne için filtre koşullarını tanımlamak üzere **Satır Seviyesi** bölümünü kullanın.
|
||||
|
||||
### Alan İzinleri
|
||||
|
||||
Her nesne düzeyi kuralın içinde, belirli alanlara erişimi denetlemek için **alan düzeyi izinlerini** yapılandırabilirsiniz.
|
||||
|
||||
@@ -60,7 +60,7 @@ For fields:
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Can I restrict access to specific records (row-level permissions)?">
|
||||
Row-level permissions will be available on the **Organization** plan by Q1 2026. This allows you to restrict access to specific records based on criteria (e.g., only see your own opportunities).
|
||||
Satır düzeyi izinler **Organization** planında mevcuttur. This allows you to restrict access to specific records based on criteria (e.g., only see your own opportunities).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="How do I make a field read-only for certain users?">
|
||||
|
||||
@@ -98,6 +98,22 @@ Twenty 的权限系统允许您控制对三个主要区域的访问:
|
||||
| Opportunities → 禁用 "查看记录" | 实习生完全无法查看 Opportunities 对象 |
|
||||
| People → 启用 "编辑记录" | 实习生可以编辑 People 记录(但不能编辑其他对象) |
|
||||
|
||||
### 行级权限
|
||||
|
||||
<Note>
|
||||
行级权限是 **Premium 功能**,可在 **Organization** 方案(云端和自托管)中使用。
|
||||
</Note>
|
||||
|
||||
行级权限可以根据动态条件,限制某个角色可以查看或编辑的具体记录。 与对象权限(适用于整个对象类型)不同,行级权限会独立评估每条记录。
|
||||
|
||||
**示例用例:**
|
||||
|
||||
* 销售代表只能查看自己的销售机会
|
||||
* 经理可以查看其所在区域的所有记录
|
||||
* 客服人员只能查看分配给他们的工单
|
||||
|
||||
要配置行级权限,打开某个角色,导航到 **Objects** 选项卡,并使用 **Row-Level** 区域为特定对象定义筛选条件。
|
||||
|
||||
### 字段权限
|
||||
|
||||
在每个对象级规则内,您还可以进一步配置**字段级权限**,以控制对特定字段的访问。
|
||||
|
||||
@@ -60,7 +60,7 @@ Twenty 默认提供 **管理员** 和 **成员** 角色。 您可以根据团队
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="我可以限制对特定记录的访问(行级权限)吗?">
|
||||
行级权限将在 2026 年第一季度于 **Organization** 方案中提供。 这将允许您根据条件限制对特定记录的访问(例如,仅查看自己的商机)。
|
||||
行级权限在 **Organization** 方案中可用。 这将允许您根据条件限制对特定记录的访问(例如,仅查看自己的商机)。
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="如何为特定用户将字段设为只读?">
|
||||
|
||||
@@ -36,9 +36,7 @@
|
||||
{
|
||||
"key": "userGuideOverview",
|
||||
"label": "Overview",
|
||||
"pages": [
|
||||
"user-guide/introduction"
|
||||
]
|
||||
"pages": ["user-guide/introduction"]
|
||||
},
|
||||
{
|
||||
"key": "dataModel",
|
||||
@@ -220,9 +218,7 @@
|
||||
{
|
||||
"key": "aiHowTos",
|
||||
"label": "How-Tos",
|
||||
"pages": [
|
||||
"user-guide/ai/how-tos/ai-faq"
|
||||
]
|
||||
"pages": ["user-guide/ai/how-tos/ai-faq"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -309,9 +305,7 @@
|
||||
{
|
||||
"key": "permissionsAccessHowTos",
|
||||
"label": "How-Tos",
|
||||
"pages": [
|
||||
"user-guide/permissions-access/how-tos/permissions-faq"
|
||||
]
|
||||
"pages": ["user-guide/permissions-access/how-tos/permissions-faq"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -332,9 +326,7 @@
|
||||
{
|
||||
"key": "billingHowTos",
|
||||
"label": "How-Tos",
|
||||
"pages": [
|
||||
"user-guide/billing/how-tos/billing-faq"
|
||||
]
|
||||
"pages": ["user-guide/billing/how-tos/billing-faq"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -359,9 +351,7 @@
|
||||
{
|
||||
"key": "settingsHowTos",
|
||||
"label": "How-Tos",
|
||||
"pages": [
|
||||
"user-guide/settings/how-tos/settings-faq"
|
||||
]
|
||||
"pages": ["user-guide/settings/how-tos/settings-faq"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -374,9 +364,7 @@
|
||||
{
|
||||
"key": "developersOverview",
|
||||
"label": "Overview",
|
||||
"pages": [
|
||||
"developers/introduction"
|
||||
]
|
||||
"pages": ["developers/introduction"]
|
||||
},
|
||||
{
|
||||
"key": "apps",
|
||||
|
||||
@@ -209,9 +209,7 @@
|
||||
{
|
||||
"key": "aiHowTos",
|
||||
"label": "How-Tos",
|
||||
"pages": [
|
||||
"user-guide/ai/how-tos/ai-faq"
|
||||
]
|
||||
"pages": ["user-guide/ai/how-tos/ai-faq"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -290,9 +288,7 @@
|
||||
{
|
||||
"key": "permissionsAccessHowTos",
|
||||
"label": "How-Tos",
|
||||
"pages": [
|
||||
"user-guide/permissions-access/how-tos/permissions-faq"
|
||||
]
|
||||
"pages": ["user-guide/permissions-access/how-tos/permissions-faq"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -313,9 +309,7 @@
|
||||
{
|
||||
"key": "billingHowTos",
|
||||
"label": "How-Tos",
|
||||
"pages": [
|
||||
"user-guide/billing/how-tos/billing-faq"
|
||||
]
|
||||
"pages": ["user-guide/billing/how-tos/billing-faq"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -340,9 +334,7 @@
|
||||
{
|
||||
"key": "settingsHowTos",
|
||||
"label": "How-Tos",
|
||||
"pages": [
|
||||
"user-guide/settings/how-tos/settings-faq"
|
||||
]
|
||||
"pages": ["user-guide/settings/how-tos/settings-faq"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -355,9 +347,7 @@
|
||||
{
|
||||
"key": "developersGroup",
|
||||
"label": "Developers",
|
||||
"pages": [
|
||||
"developers/introduction"
|
||||
]
|
||||
"pages": ["developers/introduction"]
|
||||
},
|
||||
{
|
||||
"key": "extend",
|
||||
@@ -433,9 +423,7 @@
|
||||
{
|
||||
"key": "feedback",
|
||||
"label": "Feedback",
|
||||
"pages": [
|
||||
"twenty-ui/progress-bar"
|
||||
]
|
||||
"pages": ["twenty-ui/progress-bar"]
|
||||
},
|
||||
{
|
||||
"key": "input",
|
||||
|
||||
@@ -21,11 +21,17 @@
|
||||
},
|
||||
"lint": {},
|
||||
"fmt": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"files": "."
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "prettier . --check --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
|
||||
},
|
||||
"configurations": {
|
||||
"fix": {}
|
||||
"ci": {},
|
||||
"fix": {
|
||||
"command": "prettier . --write --cache --cache-location ../../.cache/prettier/{projectRoot} --cache-strategy metadata"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
SUPPORTED_LANGUAGES,
|
||||
DEFAULT_LANGUAGE,
|
||||
SUPPORTED_LANGUAGES,
|
||||
} from '../navigation/supported-languages';
|
||||
|
||||
type BasePage = string | BaseGroup;
|
||||
@@ -71,15 +71,11 @@ const baseStructure: BaseStructure = JSON.parse(
|
||||
|
||||
const docsConfig = JSON.parse(fs.readFileSync(docsPath, 'utf8'));
|
||||
|
||||
const collectTranslations = (
|
||||
file: TranslationFile | null,
|
||||
): TranslationMaps => {
|
||||
const collectTranslations = (file: TranslationFile | null): TranslationMaps => {
|
||||
const tabLabels = new Map<string, string>();
|
||||
const groupLabels = new Map<string, string>();
|
||||
|
||||
const collectGroups = (
|
||||
groups?: Record<string, TranslationGroupEntry>,
|
||||
) => {
|
||||
const collectGroups = (groups?: Record<string, TranslationGroupEntry>) => {
|
||||
if (!groups) {
|
||||
return;
|
||||
}
|
||||
@@ -99,17 +95,15 @@ const collectTranslations = (
|
||||
};
|
||||
|
||||
const loadTranslationFile = (language: string): TranslationFile | null => {
|
||||
const translationPath = path.join(
|
||||
localesRoot,
|
||||
language,
|
||||
'navigation.json',
|
||||
);
|
||||
const translationPath = path.join(localesRoot, language, 'navigation.json');
|
||||
|
||||
if (!fs.existsSync(translationPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(fs.readFileSync(translationPath, 'utf8')) as TranslationFile;
|
||||
return JSON.parse(
|
||||
fs.readFileSync(translationPath, 'utf8'),
|
||||
) as TranslationFile;
|
||||
};
|
||||
|
||||
const buildLanguageEntry = (language: string): GeneratedLanguage => {
|
||||
@@ -161,9 +155,8 @@ const hasLocaleContent = (language: string): boolean => {
|
||||
return fs.existsSync(localeDir);
|
||||
};
|
||||
|
||||
const languages = SUPPORTED_LANGUAGES.filter(hasLocaleContent).map(
|
||||
buildLanguageEntry,
|
||||
);
|
||||
const languages =
|
||||
SUPPORTED_LANGUAGES.filter(hasLocaleContent).map(buildLanguageEntry);
|
||||
|
||||
if (!docsConfig.navigation) {
|
||||
docsConfig.navigation = {};
|
||||
|
||||
@@ -53,75 +53,81 @@ async function main() {
|
||||
const base64 = imageBytes.toString('base64');
|
||||
const mime = sourcePath.endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||
|
||||
const resultBase64 = await page.evaluate(async ({ b64, m }) => {
|
||||
const LINE_SPACING = 8;
|
||||
const MIN_LINE_WIDTH = 0.5;
|
||||
const MAX_LINE_WIDTH = 6;
|
||||
const COLOR = { r: 74, g: 56, b: 245 }; // #4A38F5
|
||||
const TRANSPARENT_BG = true;
|
||||
const resultBase64 = await page.evaluate(
|
||||
async ({ b64, m }) => {
|
||||
const LINE_SPACING = 8;
|
||||
const MIN_LINE_WIDTH = 0.5;
|
||||
const MAX_LINE_WIDTH = 6;
|
||||
const COLOR = { r: 74, g: 56, b: 245 }; // #4A38F5
|
||||
const TRANSPARENT_BG = true;
|
||||
|
||||
const img = new Image();
|
||||
await new Promise((res, rej) => {
|
||||
img.onload = res;
|
||||
img.onerror = rej;
|
||||
img.src = `data:${m};base64,${b64}`;
|
||||
});
|
||||
const img = new Image();
|
||||
await new Promise((res, rej) => {
|
||||
img.onload = res;
|
||||
img.onerror = rej;
|
||||
img.src = `data:${m};base64,${b64}`;
|
||||
});
|
||||
|
||||
const { width, height } = img;
|
||||
const scale = 2;
|
||||
const outW = width * scale;
|
||||
const outH = height * scale;
|
||||
const { width, height } = img;
|
||||
const scale = 2;
|
||||
const outW = width * scale;
|
||||
const outH = height * scale;
|
||||
|
||||
// Read source pixels
|
||||
const tmp = document.createElement('canvas');
|
||||
tmp.width = width;
|
||||
tmp.height = height;
|
||||
const tmpCtx = tmp.getContext('2d');
|
||||
tmpCtx.drawImage(img, 0, 0);
|
||||
const px = tmpCtx.getImageData(0, 0, width, height).data;
|
||||
// Read source pixels
|
||||
const tmp = document.createElement('canvas');
|
||||
tmp.width = width;
|
||||
tmp.height = height;
|
||||
const tmpCtx = tmp.getContext('2d');
|
||||
tmpCtx.drawImage(img, 0, 0);
|
||||
const px = tmpCtx.getImageData(0, 0, width, height).data;
|
||||
|
||||
// Output canvas
|
||||
const out = document.createElement('canvas');
|
||||
out.width = outW;
|
||||
out.height = outH;
|
||||
const ctx = out.getContext('2d');
|
||||
// Transparent background — adapts to both light and dark mode
|
||||
// Output canvas
|
||||
const out = document.createElement('canvas');
|
||||
out.width = outW;
|
||||
out.height = outH;
|
||||
const ctx = out.getContext('2d');
|
||||
// Transparent background — adapts to both light and dark mode
|
||||
|
||||
const sp = LINE_SPACING * scale;
|
||||
const sp = LINE_SPACING * scale;
|
||||
|
||||
for (let y = sp / 2; y < outH; y += sp) {
|
||||
const srcY = Math.min(Math.floor(y / scale), height - 1);
|
||||
let x = 0;
|
||||
for (let y = sp / 2; y < outH; y += sp) {
|
||||
const srcY = Math.min(Math.floor(y / scale), height - 1);
|
||||
let x = 0;
|
||||
|
||||
while (x < outW) {
|
||||
const srcX = Math.min(Math.floor(x / scale), width - 1);
|
||||
const i = (srcY * width + srcX) * 4;
|
||||
const lum = (0.299 * px[i] + 0.587 * px[i+1] + 0.114 * px[i+2]) / 255;
|
||||
const a = px[i+3] / 255;
|
||||
const dark = (1 - lum) * a;
|
||||
while (x < outW) {
|
||||
const srcX = Math.min(Math.floor(x / scale), width - 1);
|
||||
const i = (srcY * width + srcX) * 4;
|
||||
const lum =
|
||||
(0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2]) / 255;
|
||||
const a = px[i + 3] / 255;
|
||||
const dark = (1 - lum) * a;
|
||||
|
||||
if (dark > 0.2) {
|
||||
const w = (MIN_LINE_WIDTH + (MAX_LINE_WIDTH - MIN_LINE_WIDTH) * dark) * scale;
|
||||
const dashLen = sp * (0.3 + 0.7 * dark);
|
||||
const gap = sp * (0.1 + 0.4 * (1 - dark));
|
||||
const alpha = 0.3 + 0.7 * dark;
|
||||
if (dark > 0.2) {
|
||||
const w =
|
||||
(MIN_LINE_WIDTH + (MAX_LINE_WIDTH - MIN_LINE_WIDTH) * dark) *
|
||||
scale;
|
||||
const dashLen = sp * (0.3 + 0.7 * dark);
|
||||
const gap = sp * (0.1 + 0.4 * (1 - dark));
|
||||
const alpha = 0.3 + 0.7 * dark;
|
||||
|
||||
ctx.strokeStyle = `rgba(${COLOR.r},${COLOR.g},${COLOR.b},${alpha})`;
|
||||
ctx.lineWidth = w;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(Math.min(x + dashLen, outW), y);
|
||||
ctx.stroke();
|
||||
x += dashLen + gap;
|
||||
} else {
|
||||
x += sp * 0.5;
|
||||
ctx.strokeStyle = `rgba(${COLOR.r},${COLOR.g},${COLOR.b},${alpha})`;
|
||||
ctx.lineWidth = w;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(Math.min(x + dashLen, outW), y);
|
||||
ctx.stroke();
|
||||
x += dashLen + gap;
|
||||
} else {
|
||||
x += sp * 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out.toDataURL('image/png').split(',')[1];
|
||||
}, { b64: base64, m: mime });
|
||||
return out.toDataURL('image/png').split(',')[1];
|
||||
},
|
||||
{ b64: base64, m: mime },
|
||||
);
|
||||
|
||||
const outputPath = join(OUTPUT_DIR, `${illust.name}.png`);
|
||||
writeFileSync(outputPath, Buffer.from(resultBase64, 'base64'));
|
||||
|
||||
@@ -46,9 +46,7 @@ const baseStructure: BaseStructure = JSON.parse(
|
||||
fs.readFileSync(baseStructurePath, 'utf8'),
|
||||
);
|
||||
|
||||
const buildGroupMap = (
|
||||
groups: BaseGroup[],
|
||||
): Record<string, TemplateGroup> =>
|
||||
const buildGroupMap = (groups: BaseGroup[]): Record<string, TemplateGroup> =>
|
||||
groups.reduce<Record<string, TemplateGroup>>((acc, group) => {
|
||||
const nestedGroups = group.pages.filter(
|
||||
(page): page is BaseGroup => typeof page !== 'string',
|
||||
@@ -78,4 +76,3 @@ const template: TemplateFile = {
|
||||
};
|
||||
|
||||
fs.writeFileSync(templatePath, `${JSON.stringify(template, null, 2)}\n`);
|
||||
|
||||
|
||||
@@ -95,6 +95,21 @@ Click **+ Add rule** and select an object to create an exception.
|
||||
| Opportunities → disable "See Records" | Intern cannot see the Opportunities object at all |
|
||||
| People → enable "Edit Records" | Intern can edit People records (but not other objects) |
|
||||
|
||||
### Row-Level Permissions
|
||||
|
||||
<Note>
|
||||
Row-level permissions are a **Premium feature** available on the **Organization** plan (Cloud and Self-Hosted).
|
||||
</Note>
|
||||
|
||||
Row-level permissions let you restrict which individual records a role can see or edit, based on dynamic criteria. Unlike object permissions (which apply to an entire object type), row-level permissions evaluate each record independently.
|
||||
|
||||
**Example use cases:**
|
||||
- Sales reps can only see their own opportunities
|
||||
- Managers can see all records in their region
|
||||
- Support agents can only view tickets assigned to them
|
||||
|
||||
To configure row-level permissions, open a role, navigate to the **Objects** tab, and use the **Row-Level** section to define filter conditions for a specific object.
|
||||
|
||||
### Field Permissions
|
||||
|
||||
Within each object-level rule, you can go further and configure **field-level permissions** to control access to specific fields.
|
||||
|
||||
@@ -60,7 +60,7 @@ For fields:
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Can I restrict access to specific records (row-level permissions)?">
|
||||
Row-level permissions will be available on the **Organization** plan by Q1 2026. This allows you to restrict access to specific records based on criteria (e.g., only see your own opportunities).
|
||||
Row-level permissions are available on the **Organization** plan. This allows you to restrict access to specific records based on criteria (e.g., only see your own opportunities).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="How do I make a field read-only for certain users?">
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": ["warn", { "allow": ["group", "groupCollapsed", "groupEnd"] }],
|
||||
"no-console": [
|
||||
"warn",
|
||||
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
|
||||
],
|
||||
"no-script-url": "error",
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
@@ -29,27 +32,30 @@
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
"no-restricted-imports": ["error", {
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["../*"],
|
||||
"message": "Relative parent imports are not allowed. Use @/ alias instead."
|
||||
},
|
||||
{
|
||||
"group": ["@tabler/icons-react"],
|
||||
"message": "Please import icons from `twenty-ui`"
|
||||
},
|
||||
{
|
||||
"group": ["react-hotkeys-web-hook"],
|
||||
"importNames": ["useHotkeys"],
|
||||
"message": "Please use the custom wrapper: `useScopedHotkeys` from `twenty-ui`"
|
||||
},
|
||||
{
|
||||
"group": ["lodash"],
|
||||
"message": "Please use the standalone lodash package (for instance: `import groupBy from 'lodash.groupby'` instead of `import { groupBy } from 'lodash'`)"
|
||||
}
|
||||
]
|
||||
}],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["../*"],
|
||||
"message": "Relative parent imports are not allowed. Use @/ alias instead."
|
||||
},
|
||||
{
|
||||
"group": ["@tabler/icons-react"],
|
||||
"message": "Please import icons from `twenty-ui`"
|
||||
},
|
||||
{
|
||||
"group": ["react-hotkeys-web-hook"],
|
||||
"importNames": ["useHotkeys"],
|
||||
"message": "Please use the custom wrapper: `useScopedHotkeys` from `twenty-ui`"
|
||||
},
|
||||
{
|
||||
"group": ["lodash"],
|
||||
"message": "Please use the standalone lodash package (for instance: `import groupBy from 'lodash.groupby'` instead of `import { groupBy } from 'lodash'`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"import/no-duplicates": "error",
|
||||
|
||||
@@ -68,34 +74,46 @@
|
||||
|
||||
"typescript/no-redeclare": "error",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/consistent-type-imports": ["error", {
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "inline-type-imports"
|
||||
}],
|
||||
"typescript/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "inline-type-imports"
|
||||
}
|
||||
],
|
||||
"typescript/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"typescript/no-empty-object-type": ["error", {
|
||||
"allowInterfaces": "with-single-extends"
|
||||
}],
|
||||
"typescript/no-empty-object-type": [
|
||||
"error",
|
||||
{
|
||||
"allowInterfaces": "with-single-extends"
|
||||
}
|
||||
],
|
||||
"typescript/no-empty-function": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-unused-vars": ["warn", {
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}],
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
|
||||
"typescript/strict-boolean-expressions": ["error", {
|
||||
"allowNullableBoolean": true,
|
||||
"allowNullableObject": true,
|
||||
"allowString": true,
|
||||
"allowNumber": true,
|
||||
"allowNullableString": true,
|
||||
"allowNullableNumber": true,
|
||||
"allowNullableEnum": true,
|
||||
"allowAny": true
|
||||
}],
|
||||
"typescript/strict-boolean-expressions": [
|
||||
"error",
|
||||
{
|
||||
"allowNullableBoolean": true,
|
||||
"allowNullableObject": true,
|
||||
"allowString": true,
|
||||
"allowNumber": true,
|
||||
"allowNullableString": true,
|
||||
"allowNullableNumber": true,
|
||||
"allowNullableEnum": true,
|
||||
"allowAny": true
|
||||
}
|
||||
],
|
||||
|
||||
"twenty/effect-components": "error",
|
||||
"twenty/no-hardcoded-colors": "error",
|
||||
@@ -108,14 +126,17 @@
|
||||
"twenty/no-jotai-store-in-selector": "error",
|
||||
"twenty/no-direct-atom-family-in-selector": "error",
|
||||
"twenty/folder-structure": "error",
|
||||
"twenty/enforce-module-boundaries": ["error", {
|
||||
"depConstraints": [
|
||||
{
|
||||
"sourceTag": "scope:frontend",
|
||||
"onlyDependOnLibsWithTags": ["scope:shared", "scope:frontend"]
|
||||
}
|
||||
]
|
||||
}]
|
||||
"twenty/enforce-module-boundaries": [
|
||||
"error",
|
||||
{
|
||||
"depConstraints": [
|
||||
{
|
||||
"sourceTag": "scope:frontend",
|
||||
"onlyDependOnLibsWithTags": ["scope:shared", "scope:frontend"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user