Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d0acad6dc |
@@ -109,7 +109,7 @@ jobs:
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --example hello-world --display-name "Test hello-world app" --description "E2E test hello-world app" --api-url http://localhost:3000
|
||||
create-twenty-app test-app --example hello-world --display-name "Test hello-world app" --description "E2E test hello-world app" --skip-local-instance
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --api-url http://localhost:3000
|
||||
create-twenty-app test-app --display-name "Test scaffolded app" --description "E2E test scaffolded app" --skip-local-instance --yes
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
create-twenty-app --version
|
||||
mkdir -p /tmp/e2e-test-workspace
|
||||
cd /tmp/e2e-test-workspace
|
||||
create-twenty-app test-app --example postcard --display-name "Test postcard app" --description "E2E test postcard app" --api-url http://localhost:3000
|
||||
create-twenty-app test-app --example postcard --display-name "Test postcard app" --description "E2E test postcard app" --skip-local-instance
|
||||
|
||||
- name: Install scaffolded app dependencies
|
||||
run: |
|
||||
|
||||
@@ -23,11 +23,9 @@ jobs:
|
||||
packages/twenty-sdk/**
|
||||
packages/twenty-client-sdk/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-server/**
|
||||
!packages/twenty-sdk/package.json
|
||||
!packages/twenty-client-sdk/package.json
|
||||
!packages/twenty-shared/package.json
|
||||
!packages/twenty-server/package.json
|
||||
|
||||
example-app-postcard:
|
||||
needs: changed-files-check
|
||||
@@ -85,31 +83,6 @@ jobs:
|
||||
working-directory: packages/twenty-apps/examples/postcard
|
||||
run: npx vitest run
|
||||
|
||||
- name: Configure remote for SDK CLI
|
||||
run: |
|
||||
mkdir -p ~/.twenty
|
||||
cat > ~/.twenty/config.json <<EOF
|
||||
{
|
||||
"version": 1,
|
||||
"remotes": {
|
||||
"target": {
|
||||
"apiUrl": "${TWENTY_API_URL}",
|
||||
"apiKey": "${TWENTY_API_KEY}",
|
||||
"accessToken": "${TWENTY_API_KEY}"
|
||||
}
|
||||
},
|
||||
"defaultRemote": "target"
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Deploy postcard app (registry install path)
|
||||
working-directory: packages/twenty-apps/examples/postcard
|
||||
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs deploy --remote target
|
||||
|
||||
- name: Install postcard app (registry install path)
|
||||
working-directory: packages/twenty-apps/examples/postcard
|
||||
run: node ${{ github.workspace }}/packages/twenty-sdk/dist/cli.cjs install --remote target
|
||||
|
||||
ci-example-app-postcard-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
|
||||
@@ -51,7 +51,6 @@ jobs:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
@@ -124,7 +123,6 @@ jobs:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
|
||||
Vendored
-40
@@ -43,46 +43,6 @@
|
||||
{
|
||||
"name": "packages/twenty-e2e-testing",
|
||||
"path": "../packages/twenty-e2e-testing"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-docs",
|
||||
"path": "../packages/twenty-docs"
|
||||
},
|
||||
{
|
||||
"name": "packages/create-twenty-app",
|
||||
"path": "../packages/create-twenty-app"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-apps",
|
||||
"path": "../packages/twenty-apps"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-claude-skills",
|
||||
"path": "../packages/twenty-claude-skills"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-cli",
|
||||
"path": "../packages/twenty-cli"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-client-sdk",
|
||||
"path": "../packages/twenty-client-sdk"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-companion",
|
||||
"path": "../packages/twenty-companion"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-front-component-renderer",
|
||||
"path": "../packages/twenty-front-component-renderer"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-sdk",
|
||||
"path": "../packages/twenty-sdk"
|
||||
},
|
||||
{
|
||||
"name": "packages/twenty-website-new",
|
||||
"path": "../packages/twenty-website-new"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
+1
-3
@@ -4,10 +4,8 @@ enableHardenedMode: true
|
||||
|
||||
enableInlineHunks: true
|
||||
|
||||
enableScripts: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
npmMinimalAgeGate: "3d"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||
|
||||
@@ -25,19 +25,18 @@ yarn twenty dev
|
||||
The scaffolder will:
|
||||
|
||||
1. Create a new project with TypeScript, linting, tests, and a preconfigured `twenty` CLI
|
||||
2. Start a local Twenty server via Docker (pulls the latest image automatically)
|
||||
3. Authenticate with the development API key
|
||||
2. Optionally start a local Twenty server (Docker)
|
||||
3. Open the browser for OAuth authentication
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description |
|
||||
| ---------------------------------- | --------------------------------------------------------------------- |
|
||||
| `--example <name>` | Initialize from an example |
|
||||
| `--name <name>` | Set the app name |
|
||||
| `--display-name <displayName>` | Set the display name |
|
||||
| `--description <description>` | Set the description |
|
||||
| `--api-url <url>` | Twenty instance URL (default: `http://localhost:2020`) |
|
||||
| `--authentication-method <method>` | `oauth` or `apiKey` (default: `apiKey` for local, `oauth` for remote) |
|
||||
| Flag | Description |
|
||||
| ------------------------------ | --------------------------------------- |
|
||||
| `--example <name>` | Initialize from an example |
|
||||
| `--name <name>` | Set the app name (skips the prompt) |
|
||||
| `--display-name <displayName>` | Set the display name (skips the prompt) |
|
||||
| `--description <description>` | Set the description (skips the prompt) |
|
||||
| `--skip-local-instance` | Skip the local server setup prompt |
|
||||
|
||||
By default (no flags), a minimal app is generated with core files and an integration test. Use `--example` to start from a richer example:
|
||||
|
||||
@@ -58,7 +57,7 @@ Full documentation is available at **[docs.twenty.com/developers/extend/apps](ht
|
||||
## Troubleshooting
|
||||
|
||||
- Server not starting: check Docker is running (`docker info`), then try `yarn twenty server logs`.
|
||||
- Auth not working: run `yarn twenty remote add --local` to re-authenticate.
|
||||
- Auth not working: make sure you are logged in to Twenty in the browser, then run `yarn twenty remote add`.
|
||||
- Types not generated: ensure `yarn twenty dev` is running — it auto-generates the typed client.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "2.5.0",
|
||||
"version": "2.4.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
@@ -32,6 +32,7 @@
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"inquirer": "^10.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
@@ -42,6 +43,7 @@
|
||||
"@swc/core": "^1.15.11",
|
||||
"@swc/jest": "^0.2.39",
|
||||
"@types/fs-extra": "^11.0.0",
|
||||
"@types/inquirer": "^9.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommanderError } from 'commander';
|
||||
import {
|
||||
type AuthenticationMethod,
|
||||
CreateAppCommand,
|
||||
} from '@/create-app.command';
|
||||
import { CreateAppCommand } from '@/create-app.command';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const program = new Command(packageJson.name)
|
||||
@@ -16,17 +13,20 @@ const program = new Command(packageJson.name)
|
||||
)
|
||||
.argument('[directory]')
|
||||
.option('--example <name>', 'Initialize from an example')
|
||||
.option('-n, --name <name>', 'Application name')
|
||||
.option('-d, --display-name <displayName>', 'Application display name')
|
||||
.option('--description <description>', 'Application description')
|
||||
.option('-n, --name <name>', 'Application name (skips prompt)')
|
||||
.option(
|
||||
'--api-url <apiUrl>',
|
||||
'Twenty instance URL (default: http://localhost:2020)',
|
||||
'-d, --display-name <displayName>',
|
||||
'Application display name (skips prompt)',
|
||||
)
|
||||
.option(
|
||||
'--authentication-method <method>',
|
||||
'Authentication method: oauth or apiKey (default: apiKey for local, oauth for remote)',
|
||||
'--description <description>',
|
||||
'Application description (skips prompt)',
|
||||
)
|
||||
.option(
|
||||
'--skip-local-instance',
|
||||
'Skip the local Twenty instance setup prompt',
|
||||
)
|
||||
.option('-y, --yes', 'Auto-confirm prompts (e.g. start existing container)')
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(
|
||||
async (
|
||||
@@ -36,8 +36,8 @@ const program = new Command(packageJson.name)
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
authenticationMethod?: AuthenticationMethod;
|
||||
skipLocalInstance?: boolean;
|
||||
yes?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
|
||||
@@ -54,26 +54,14 @@ const program = new Command(packageJson.name)
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
options?.authenticationMethod &&
|
||||
!['oauth', 'apiKey'].includes(options.authenticationMethod)
|
||||
) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: --authentication-method must be "oauth" or "apiKey".',
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await new CreateAppCommand().execute({
|
||||
directory,
|
||||
example: options?.example,
|
||||
name: options?.name,
|
||||
displayName: options?.displayName,
|
||||
description: options?.description,
|
||||
apiUrl: options?.apiUrl,
|
||||
authenticationMethod: options?.authenticationMethod,
|
||||
skipLocalInstance: options?.skipLocalInstance,
|
||||
yes: options?.yes,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,19 +2,8 @@ This is a [Twenty](https://twenty.com) application bootstrapped with [`create-tw
|
||||
|
||||
## Getting Started
|
||||
|
||||
This app was scaffolded with a local Twenty server running at [http://localhost:2020](http://localhost:2020).
|
||||
|
||||
Login with the default development credentials: `[email protected]` / `[email protected]`.
|
||||
|
||||
Run `yarn twenty help` to list all available commands.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
- `yarn twenty dev` - Start the development server and sync your app
|
||||
- `yarn twenty server status` - Check the local Twenty server status
|
||||
- `yarn twenty server start` - Start the local Twenty server
|
||||
- `yarn test` - Run integration tests
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Twenty Apps documentation](https://docs.twenty.com/developers/extend/apps/getting-started/quick-start)
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TWENTY_DEPLOY_URL: http://localhost:2020
|
||||
TWENTY_DEPLOY_URL: http://localhost:3000
|
||||
|
||||
concurrency:
|
||||
group: cd-${{ github.ref }}
|
||||
|
||||
@@ -5,29 +5,21 @@ import { install } from '@/utils/install';
|
||||
import { tryGitInit } from '@/utils/try-git-init';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import * as path from 'path';
|
||||
import { basename } from 'path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import {
|
||||
authLogin,
|
||||
authLoginOAuth,
|
||||
checkDockerRunning,
|
||||
ConfigService,
|
||||
DEV_API_KEY,
|
||||
DEV_API_URL,
|
||||
containerExists,
|
||||
detectLocalServer,
|
||||
serverStart,
|
||||
} from 'twenty-sdk/cli';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
getDockerInstallInstructions,
|
||||
isDockerInstalled,
|
||||
} from '@/utils/docker-install';
|
||||
|
||||
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
|
||||
const IMAGE = 'twentycrm/twenty-app-dev:latest';
|
||||
|
||||
export type AuthenticationMethod = 'oauth' | 'apiKey';
|
||||
|
||||
type CreateAppOptions = {
|
||||
directory?: string;
|
||||
@@ -35,62 +27,40 @@ type CreateAppOptions = {
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
apiUrl?: string;
|
||||
authenticationMethod?: AuthenticationMethod;
|
||||
skipLocalInstance?: boolean;
|
||||
yes?: boolean;
|
||||
};
|
||||
|
||||
export class CreateAppCommand {
|
||||
private stepCounter = 0;
|
||||
private totalSteps = 0;
|
||||
private static TOTAL_STEPS = 4;
|
||||
|
||||
async execute(options: CreateAppOptions = {}): Promise<void> {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
this.getAppInfos(options);
|
||||
|
||||
const apiUrl = options.apiUrl ?? DEV_API_URL;
|
||||
|
||||
const skipLocalInstance = apiUrl !== DEV_API_URL;
|
||||
|
||||
if (!skipLocalInstance && !isDockerInstalled()) {
|
||||
console.log(chalk.yellow('\n' + getDockerInstallInstructions() + '\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (skipLocalInstance && options.authenticationMethod === 'apiKey') {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'API key authentication is only supported on a local Docker instance. Ignoring and switching to OAuth authentication.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const authenticationMethod = skipLocalInstance
|
||||
? 'oauth'
|
||||
: (options.authenticationMethod ?? 'apiKey');
|
||||
await this.getAppInfos(options);
|
||||
|
||||
try {
|
||||
await this.validateDirectory(appDirectory);
|
||||
|
||||
this.totalSteps = this.computeTotalSteps({
|
||||
skipLocalInstance,
|
||||
const confirmed = await this.promptScaffoldConfirmation({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
autoConfirm: options.yes,
|
||||
});
|
||||
|
||||
this.stepCounter = 0;
|
||||
if (!confirmed) {
|
||||
console.log(chalk.gray('\nScaffolding cancelled.'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const dockerPullPromise =
|
||||
!skipLocalInstance && checkDockerRunning()
|
||||
? this.pullImageInBackground()
|
||||
: Promise.resolve(false);
|
||||
|
||||
this.logPlan({ appName, appDisplayName, appDescription, appDirectory });
|
||||
|
||||
this.logNextStep('Creating project directory');
|
||||
console.log('');
|
||||
|
||||
this.logStep(1, 'Creating project directory');
|
||||
await fs.ensureDir(appDirectory);
|
||||
|
||||
this.logDetail(appDirectory);
|
||||
|
||||
this.logNextStep('Scaffolding project files');
|
||||
this.logStep(2, 'Scaffolding project files');
|
||||
|
||||
if (options.example) {
|
||||
const exampleSucceeded = await this.tryDownloadExample(
|
||||
@@ -117,12 +87,10 @@ export class CreateAppCommand {
|
||||
});
|
||||
}
|
||||
|
||||
this.logNextStep('Installing dependencies');
|
||||
|
||||
this.logStep(3, 'Installing dependencies');
|
||||
await install(appDirectory, (message) => this.logDetail(message));
|
||||
|
||||
this.logNextStep('Initializing Git repository');
|
||||
|
||||
this.logStep(4, 'Initializing Git repository');
|
||||
const gitInitialized = await tryGitInit(appDirectory);
|
||||
|
||||
if (gitInitialized) {
|
||||
@@ -136,29 +104,38 @@ export class CreateAppCommand {
|
||||
|
||||
console.log('');
|
||||
|
||||
let hasLocalServer = false;
|
||||
let authSucceeded = false;
|
||||
let resolvedApiUrl = apiUrl;
|
||||
let serverReady = skipLocalInstance;
|
||||
|
||||
if (!skipLocalInstance) {
|
||||
this.logNextStep('Starting Twenty server');
|
||||
const serverResult = await this.ensureDockerServer(dockerPullPromise);
|
||||
if (!options.skipLocalInstance) {
|
||||
const existingServerUrl = await detectLocalServer();
|
||||
|
||||
if (isDefined(serverResult.url)) {
|
||||
resolvedApiUrl = serverResult.url;
|
||||
serverReady = true;
|
||||
if (existingServerUrl) {
|
||||
hasLocalServer = true;
|
||||
authSucceeded = await this.promptConnectToLocal(existingServerUrl);
|
||||
} else {
|
||||
const shouldStart = await this.shouldStartServer(options.yes);
|
||||
|
||||
if (shouldStart) {
|
||||
const startResult = await serverStart({
|
||||
onProgress: (message: string) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
if (startResult.success) {
|
||||
hasLocalServer = true;
|
||||
authSucceeded = await this.promptConnectToLocal(
|
||||
startResult.data.url,
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.yellow(`\n${startResult.error.message}`));
|
||||
}
|
||||
} else {
|
||||
this.logServerSkipped();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serverReady && authenticationMethod === 'oauth') {
|
||||
this.logNextStep('Authenticating via OAuth');
|
||||
authSucceeded = await this.authenticateWithOAuth(resolvedApiUrl);
|
||||
} else if (serverReady && authenticationMethod === 'apiKey') {
|
||||
this.logNextStep('Authenticating via API key');
|
||||
authSucceeded = await this.authenticateWithDevKey(resolvedApiUrl);
|
||||
}
|
||||
|
||||
this.logSuccess(appDirectory, resolvedApiUrl, authSucceeded);
|
||||
this.logSuccess(appDirectory, hasLocalServer, authSucceeded);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('\nCreate application failed:'),
|
||||
@@ -168,42 +145,66 @@ export class CreateAppCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private computeTotalSteps({
|
||||
skipLocalInstance,
|
||||
}: {
|
||||
skipLocalInstance: boolean;
|
||||
}): number {
|
||||
let steps = 4; // directory, scaffold, install, git
|
||||
|
||||
if (!skipLocalInstance) {
|
||||
steps += 1; // start server
|
||||
}
|
||||
|
||||
steps += 1; // authenticate (oauth or apiKey)
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
private getAppInfos(options: CreateAppOptions): {
|
||||
private async getAppInfos(options: CreateAppOptions): Promise<{
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
} {
|
||||
}> {
|
||||
const { directory } = options;
|
||||
|
||||
const hasName = isDefined(options.name) || isDefined(directory);
|
||||
const hasDisplayName = isDefined(options.displayName);
|
||||
const hasDescription = isDefined(options.description);
|
||||
const hasExample = isDefined(options.example);
|
||||
|
||||
const { name, displayName, description } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Application name:',
|
||||
when: () => !hasName && !hasExample,
|
||||
default: 'my-twenty-app',
|
||||
validate: (input) => {
|
||||
if (input.length === 0) return 'Application name is required';
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'displayName',
|
||||
message: 'Application display name:',
|
||||
when: () => !hasDisplayName && !hasExample,
|
||||
default: (answers: { name?: string }) => {
|
||||
return convertToLabel(
|
||||
answers?.name ?? options.name ?? directory ?? '',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'description',
|
||||
message: 'Application description (optional):',
|
||||
when: () => !hasDescription && !hasExample,
|
||||
default: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const appName = (
|
||||
options.name ??
|
||||
options.directory ??
|
||||
name ??
|
||||
directory ??
|
||||
options.example ??
|
||||
'my-twenty-app'
|
||||
).trim();
|
||||
|
||||
const appDisplayName =
|
||||
options.displayName?.trim() || convertToLabel(appName);
|
||||
(options.displayName ?? displayName)?.trim() || convertToLabel(appName);
|
||||
|
||||
const appDescription = (options.description ?? '').trim();
|
||||
const appDescription = (options.description ?? description ?? '').trim();
|
||||
|
||||
const appDirectory = options.directory
|
||||
? path.join(CURRENT_EXECUTION_DIRECTORY, options.directory)
|
||||
const appDirectory = directory
|
||||
? path.join(CURRENT_EXECUTION_DIRECTORY, directory)
|
||||
: path.join(CURRENT_EXECUTION_DIRECTORY, kebabCase(appName));
|
||||
|
||||
return { appName, appDisplayName, appDirectory, appDescription };
|
||||
@@ -231,30 +232,45 @@ export class CreateAppCommand {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
console.error(
|
||||
chalk.red(
|
||||
`\n${error instanceof Error ? error.message : 'Failed to download example.'}`,
|
||||
),
|
||||
);
|
||||
this.logDetail('Falling back to default template...');
|
||||
|
||||
const { useTemplate } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'useTemplate',
|
||||
message: 'Would you like to create a default template app instead?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!useTemplate) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Clean up any partial files from the failed download
|
||||
await fs.emptyDir(appDirectory);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private logPlan({
|
||||
private async promptScaffoldConfirmation({
|
||||
appName,
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
autoConfirm,
|
||||
}: {
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
}): void {
|
||||
autoConfirm?: boolean;
|
||||
}): Promise<boolean> {
|
||||
console.log(chalk.blue('\nCreating Twenty Application\n'));
|
||||
console.log(chalk.white(` Name: ${appName}`));
|
||||
console.log(chalk.white(` Display name: ${appDisplayName}`));
|
||||
@@ -264,13 +280,43 @@ export class CreateAppCommand {
|
||||
}
|
||||
|
||||
console.log(chalk.white(` Directory: ${appDirectory}`));
|
||||
|
||||
console.log(chalk.white('\nThe following steps will be performed:\n'));
|
||||
console.log(chalk.gray(' 1. Create project directory'));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
' 2. Scaffold project files from base template\n' +
|
||||
' - Copy template files\n' +
|
||||
' - Configure dotfiles (.gitignore, .github)\n' +
|
||||
' - Generate unique application identifiers\n' +
|
||||
' - Update package.json with app name and SDK versions',
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(' 3. Install dependencies (yarn)'));
|
||||
console.log(
|
||||
chalk.gray(' 4. Initialize Git repository with initial commit'),
|
||||
);
|
||||
console.log('');
|
||||
|
||||
if (autoConfirm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { proceed } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: 'Proceed?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
return proceed;
|
||||
}
|
||||
|
||||
private logNextStep(title: string): void {
|
||||
this.stepCounter++;
|
||||
private logStep(step: number, title: string): void {
|
||||
console.log(
|
||||
chalk.blue(`\n[${this.stepCounter}/${this.totalSteps}]`) +
|
||||
chalk.blue(`\n[${step}/${CreateAppCommand.TOTAL_STEPS}]`) +
|
||||
chalk.white(` ${title}...`),
|
||||
);
|
||||
}
|
||||
@@ -279,57 +325,100 @@ export class CreateAppCommand {
|
||||
console.log(chalk.gray(` → ${message}`));
|
||||
}
|
||||
|
||||
private pullImageInBackground(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('docker', ['pull', IMAGE], { stdio: 'ignore' });
|
||||
private async shouldStartServer(autoConfirm?: boolean): Promise<boolean> {
|
||||
console.log(
|
||||
chalk.white(
|
||||
'\n A local Twenty instance is required for app development.\n' +
|
||||
' It provides the API and schema your application connects to.\n',
|
||||
),
|
||||
);
|
||||
|
||||
child.on('close', (code) => resolve(code === 0));
|
||||
child.on('error', () => resolve(false));
|
||||
});
|
||||
if (checkDockerRunning() && containerExists()) {
|
||||
if (autoConfirm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { startExisting } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'startExisting',
|
||||
message:
|
||||
'An existing Twenty server container was found. Would you like to start it?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
return startExisting;
|
||||
}
|
||||
|
||||
if (autoConfirm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { startDocker } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'startDocker',
|
||||
message:
|
||||
'No running Twenty instance found. Would you like to start one using Docker?',
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
return startDocker;
|
||||
}
|
||||
|
||||
private async ensureDockerServer(
|
||||
dockerPullPromise: Promise<boolean>,
|
||||
): Promise<{ url?: string }> {
|
||||
if (!checkDockerRunning()) {
|
||||
private logServerSkipped(): void {
|
||||
console.log(
|
||||
chalk.gray(
|
||||
'\n To start a Twenty instance later:\n' +
|
||||
' yarn twenty server start\n\n' +
|
||||
' To connect to a remote instance instead:\n' +
|
||||
' yarn twenty remote add\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async promptConnectToLocal(serverUrl: string): Promise<boolean> {
|
||||
console.log(
|
||||
chalk.white(
|
||||
'\n Authentication links your app to a Twenty instance so you can\n' +
|
||||
' sync custom objects, fields, and roles during development.\n' +
|
||||
' This will open a browser window to complete the OAuth flow.\n',
|
||||
),
|
||||
);
|
||||
|
||||
const { shouldAuthenticate } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'shouldAuthenticate',
|
||||
message: `Authenticate to the local Twenty instance (${serverUrl})?`,
|
||||
default: true,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!shouldAuthenticate) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\n Docker is installed but not running.\n' +
|
||||
' Please start Docker and run this command again.\n',
|
||||
chalk.gray(
|
||||
'\n Authentication skipped. To authenticate later:\n' +
|
||||
` yarn twenty remote add --local\n`,
|
||||
),
|
||||
);
|
||||
|
||||
return {};
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logDetail('Ensuring latest Twenty server image...');
|
||||
await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'confirm',
|
||||
message: 'Press Enter to open the browser for authentication...',
|
||||
},
|
||||
]);
|
||||
|
||||
const pullSucceeded = await dockerPullPromise;
|
||||
|
||||
if (!pullSucceeded) {
|
||||
this.logDetail(
|
||||
'Image pull failed, continuing with cached image if available...',
|
||||
);
|
||||
}
|
||||
|
||||
const startResult = await serverStart({
|
||||
onProgress: (message: string) => this.logDetail(message),
|
||||
});
|
||||
|
||||
if (startResult.success) {
|
||||
return { url: startResult.data.url };
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`\n ${startResult.error.message}`));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private async authenticateWithDevKey(apiUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await authLogin({
|
||||
apiKey: DEV_API_KEY,
|
||||
apiUrl,
|
||||
const result = await authLoginOAuth({
|
||||
apiUrl: serverUrl,
|
||||
remote: 'local',
|
||||
});
|
||||
|
||||
@@ -337,68 +426,21 @@ export class CreateAppCommand {
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.setDefaultRemote('local');
|
||||
this.logDetail('Authenticated as [email protected] (development API key)');
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Authentication failed. Run `yarn twenty remote add --local` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
' Authentication failed. Run `yarn twenty remote add --local` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
' Authentication failed. Run `yarn twenty remote add --local` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private deriveRemoteName(url: string): string {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/\./g, '-');
|
||||
} catch {
|
||||
return 'remote';
|
||||
}
|
||||
}
|
||||
|
||||
private async authenticateWithOAuth(apiUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const remoteName = this.deriveRemoteName(apiUrl);
|
||||
|
||||
ConfigService.setActiveRemote(remoteName);
|
||||
|
||||
this.logDetail('Opening browser for OAuth...');
|
||||
|
||||
const result = await authLoginOAuth({ apiUrl });
|
||||
|
||||
if (result.success) {
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.setDefaultRemote(remoteName);
|
||||
this.logDetail(`Authenticated via OAuth to ${apiUrl}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` OAuth failed: ${result.error.message}\n` +
|
||||
` Run \`yarn twenty remote add --api-url ${apiUrl}\` manually.`,
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
` Authentication failed. Run \`yarn twenty remote add --api-url ${apiUrl}\` manually.`,
|
||||
'Authentication failed. Run `yarn twenty remote add` manually.',
|
||||
),
|
||||
);
|
||||
|
||||
@@ -408,13 +450,12 @@ export class CreateAppCommand {
|
||||
|
||||
private logSuccess(
|
||||
appDirectory: string,
|
||||
apiUrl: string,
|
||||
hasLocalServer: boolean,
|
||||
authSucceeded: boolean,
|
||||
): void {
|
||||
const dirName = basename(appDirectory);
|
||||
|
||||
console.log(chalk.green('\n✔ Application created successfully!\n'));
|
||||
|
||||
console.log(chalk.white(' Next steps:\n'));
|
||||
|
||||
let stepNumber = 1;
|
||||
@@ -424,21 +465,17 @@ export class CreateAppCommand {
|
||||
stepNumber++;
|
||||
|
||||
if (!authSucceeded) {
|
||||
const remoteCommand = hasLocalServer
|
||||
? 'yarn twenty remote add --local'
|
||||
: 'yarn twenty remote add';
|
||||
|
||||
console.log(chalk.white(` ${stepNumber}. Connect to a Twenty instance`));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
' yarn twenty remote add --api-url <your-instance-url>\n',
|
||||
),
|
||||
);
|
||||
console.log(chalk.cyan(` ${remoteCommand}\n`));
|
||||
stepNumber++;
|
||||
}
|
||||
|
||||
console.log(chalk.white(` ${stepNumber}. Start developing`));
|
||||
console.log(chalk.cyan(' yarn twenty dev\n'));
|
||||
stepNumber++;
|
||||
|
||||
console.log(chalk.white(` ${stepNumber}. Open your twenty instance`));
|
||||
console.log(chalk.cyan(` ${apiUrl}\n`));
|
||||
|
||||
console.log(
|
||||
chalk.gray(
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const DOCKER_DOWNLOAD_URLS: Record<string, string> = {
|
||||
darwin: 'https://docs.docker.com/desktop/setup/install/mac-install/',
|
||||
win32: 'https://docs.docker.com/desktop/setup/install/windows-install/',
|
||||
linux: 'https://docs.docker.com/engine/install/',
|
||||
};
|
||||
|
||||
export const isDockerInstalled = (): boolean => {
|
||||
try {
|
||||
execSync('docker --version', { stdio: 'ignore' });
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDockerInstallInstructions = (): string => {
|
||||
const url =
|
||||
DOCKER_DOWNLOAD_URLS[process.platform] ?? DOCKER_DOWNLOAD_URLS.linux;
|
||||
|
||||
return [
|
||||
' Docker is required but not installed.',
|
||||
'',
|
||||
` Install Docker: ${url}`,
|
||||
'',
|
||||
' Then run this command again.',
|
||||
'',
|
||||
' Alternatively, connect to an existing Twenty instance:',
|
||||
' npx create-twenty-app@latest my-twenty-app --api-url <your-instance-url>',
|
||||
].join('\n');
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
# Discord for Twenty
|
||||
|
||||
Connect Discord to your Twenty workflows. Post messages, edit them, react,
|
||||
delete, and browse channels — all from the workflow builder or the AI chat.
|
||||
|
||||
## What you can do
|
||||
|
||||
Once installed and the admin has provided the bot token, five tools become
|
||||
available in workflows and the AI chat:
|
||||
|
||||
- **Send Discord message** — *"post 'New deal closed 🎉' to #general"*. As a
|
||||
workflow step: provide `channelId` + `messageText`. Optional
|
||||
`replyToMessageId` turns it into a reply to a previous message.
|
||||
- **Update Discord message** — edit a message the bot already sent. Useful
|
||||
for status messages that change as a workflow progresses.
|
||||
- **Delete Discord message** — remove a bot message (for example after a
|
||||
mistake or when a workflow is cancelled).
|
||||
- **Add Discord reaction** — react with a unicode emoji (`👍`, `✅`, `🎉`)
|
||||
or a custom server emoji (`partyparrot:643452342342342342`). Great for
|
||||
signalling status at a glance.
|
||||
- **List Discord channels** — discovers the text-postable channels in a
|
||||
Discord server, useful as the first step in a workflow that picks a
|
||||
channel dynamically. **Leave `guildId` blank if your bot is in only one
|
||||
server — it auto-picks.** If the bot is in multiple servers, the error
|
||||
response lists each server's name and ID so you can copy the right one.
|
||||
|
||||
## Installing
|
||||
|
||||
1. Open **Settings → Applications** in your Twenty workspace.
|
||||
2. Find **Discord** in the available apps and click **Install**.
|
||||
3. Use any of the tools above in a workflow step or via the AI chat.
|
||||
|
||||
> **Heads up:** if you see *"Discord is not configured"* on the first run,
|
||||
> your Twenty admin needs to follow the [Self-hosting setup](#self-hosting-setup-admin-only)
|
||||
> section below — they need to create a Discord application and provide the
|
||||
> bot token before the tools work.
|
||||
|
||||
## Finding channel and message IDs
|
||||
|
||||
Discord IDs are long numeric strings (e.g. `1234567890123456789`). To copy
|
||||
them from the Discord client:
|
||||
|
||||
1. **Enable Developer Mode** once: **User Settings → Advanced → Developer
|
||||
Mode** (toggle on). Discord then shows extra "Copy ID" options on
|
||||
right-click.
|
||||
2. **Channel ID** — right-click any channel → *Copy Channel ID*.
|
||||
3. **Message ID** — right-click any message → *Copy Message ID*.
|
||||
4. **Server (guild) ID** — right-click the server icon → *Copy Server ID*.
|
||||
Only needed for `discord-list-channels` when your bot is in multiple
|
||||
servers (otherwise leave it blank for auto-pick).
|
||||
|
||||
> **Tip:** when chaining steps, pass `messageId` from a previous *Send
|
||||
> Discord message* step directly into a follow-up *Update / Delete / Add
|
||||
> Reaction* step. No copy/paste needed.
|
||||
|
||||
## Limitations
|
||||
|
||||
What this connector intentionally does **not** support in v1:
|
||||
|
||||
- **Direct messages to users.** Discord's DM API needs a different identity
|
||||
flow.
|
||||
- **Threads.** Replying inside a thread or starting a new one isn't exposed.
|
||||
- **Webhooks.** Uses the bot REST API only.
|
||||
- **Slash commands / interactions.** The bot doesn't register or respond to
|
||||
`/commands`.
|
||||
- **Per-workspace identity.** All Twenty workspaces in the same Twenty
|
||||
deployment share the same Discord bot — see
|
||||
[Why bot token instead of OAuth?](#why-bot-token-instead-of-oauth) below.
|
||||
- **2000-character message limit.** Discord rejects longer payloads with
|
||||
HTTP 400.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| `Discord is not configured` | `DISCORD_BOT_TOKEN` not set | Admin: paste the bot token in Settings → Applications → Discord → **Settings tab** |
|
||||
| `401 Unauthorized` | Token wrong, copied with whitespace, or rotated | Reset the token in the Discord Developer Portal and re-paste |
|
||||
| `Missing Access (50001)` | Bot wasn't invited to that server | Re-run the OAuth invite URL for that server (see admin setup) |
|
||||
| `Missing Permissions (50013)` | Bot is in the server but lacks Send / Manage / React permissions | In Discord: server settings → roles → grant the bot's role the missing permission |
|
||||
| `Unknown Channel (10003)` | `channelId` is wrong, or bot can't see that channel | Verify the ID; check the bot has *View Channel* permission |
|
||||
| `Unknown Message (10008)` | `messageId` is wrong, or message was deleted | Verify the ID is from a still-existing message |
|
||||
| `Cannot edit a message authored by another user (50005)` | Trying to update / delete a message the bot didn't send | The bot can only edit its own messages; it can delete others' messages only with the *Manage Messages* permission |
|
||||
| `Bot is in N Discord servers — please specify which one` (from list-channels) | Bot is in multiple servers and `guildId` was left blank | Copy the server ID from the error message and paste it into the `guildId` field |
|
||||
| `Discord API responded with 429` | Hit Discord's rate limit | Reduce workflow concurrency; this connector doesn't yet auto-retry |
|
||||
|
||||
---
|
||||
|
||||
## Self-hosting setup (admin-only)
|
||||
|
||||
This section is for Twenty server admins. If you're on Twenty Cloud, skip
|
||||
this — the bot credentials are already configured.
|
||||
|
||||
### 1. Create a Discord application
|
||||
|
||||
1. Visit https://discord.com/developers/applications and click
|
||||
**New Application**.
|
||||
2. Name it (e.g. *"Twenty Bot"*) and create. Use a dedicated app — don't
|
||||
reuse one configured for other bots.
|
||||
3. Sidebar → **Bot** tab → **Reset Token** → copy the token immediately
|
||||
(it's only shown once; if you lose it, reset again).
|
||||
4. *Recommended:* toggle **Public Bot** OFF so randoms can't invite the
|
||||
bot to their servers.
|
||||
|
||||
### 2. Generate the invite URL and add the bot to your server(s)
|
||||
|
||||
1. In the Discord Developer Portal: sidebar → **OAuth2** → **URL Generator**.
|
||||
2. **Scopes**: tick `bot`.
|
||||
3. **Bot Permissions**: tick `Send Messages`, `Manage Messages`,
|
||||
`Add Reactions`, `Read Message History`.
|
||||
4. Copy the generated URL.
|
||||
5. Open the URL in your browser, pick the Discord server, authorize.
|
||||
Repeat for each server you want workflows to post in.
|
||||
|
||||
### 3. Wire the bot token into Twenty
|
||||
|
||||
1. In Twenty: **Settings → Applications → Discord → Settings tab**
|
||||
(gear icon).
|
||||
2. Paste the bot token into the `DISCORD_BOT_TOKEN` row. Saves on blur.
|
||||
|
||||
Workspace users can now use the Discord workflow tools immediately — no
|
||||
further per-user configuration needed (which is unique vs Linear / Slack
|
||||
where each user connects their own account).
|
||||
|
||||
---
|
||||
|
||||
## Why bot token instead of OAuth?
|
||||
|
||||
Slack and Linear use OAuth-per-workspace
|
||||
(`defineConnectionProvider({ type: 'oauth' })`) so each Twenty workspace
|
||||
stores its own access token. Discord works differently:
|
||||
|
||||
- Discord's `bot` scope **does** have an OAuth flow, but the `access_token`
|
||||
it returns is a *user* bearer token — useless for bot actions like posting
|
||||
messages. To actually send messages as the bot you need the static
|
||||
**bot token** from the Developer Portal.
|
||||
- That bot token is global to the Discord application (and therefore to
|
||||
the Twenty deployment). Discord deprecated per-install bot tokens years
|
||||
ago.
|
||||
- Webhooks are a separate auth model but only support posting — no edit,
|
||||
delete, or reactions — so they don't cover Slack/Linear parity.
|
||||
|
||||
The result: this connector skips `defineConnectionProvider` entirely and
|
||||
reads `DISCORD_BOT_TOKEN` from an `applicationVariable` set once at
|
||||
deployment scope. See
|
||||
[Discord's OAuth2 docs](https://discord.com/developers/docs/topics/oauth2#bot-users)
|
||||
for the underlying reason bot users authenticate via static tokens.
|
||||
|
||||
---
|
||||
|
||||
## Developers only
|
||||
|
||||
If you're working on this app rather than installing the published version:
|
||||
|
||||
```bash
|
||||
cd packages/twenty-apps/internal/twenty-discord
|
||||
|
||||
# Day-to-day development (publish + install + watch in one):
|
||||
yarn twenty dev
|
||||
|
||||
# Run unit tests:
|
||||
yarn test
|
||||
|
||||
# Lint:
|
||||
yarn lint
|
||||
```
|
||||
|
||||
`twenty dev` is recommended for iteration — it publishes to your local
|
||||
Twenty server, installs the app, and watches for changes in one command.
|
||||
|
||||
The Discord REST API (v10) is called directly via `fetch` — no `discord.js`
|
||||
or other SDK dependency. See
|
||||
`src/logic-functions/utils/discord-api-request.ts` for the auth and
|
||||
error-handling wrapper that all handlers go through.
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "twenty-discord",
|
||||
"version": "0.1.0",
|
||||
"description": "Discord workflow connector for Twenty",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"keywords": [
|
||||
"twenty-app"
|
||||
],
|
||||
"packageManager": "[email protected]",
|
||||
"scripts": {
|
||||
"twenty": "twenty",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"oxlint": "^0.16.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 127 127"
|
||||
fill="none"
|
||||
role="img"
|
||||
>
|
||||
<title>Discord</title>
|
||||
<path
|
||||
fill="#5865F2"
|
||||
d="M107.7 26.4A104.6 104.6 0 0 0 81.6 18a0.4 0.4 0 0 0-0.4 0.2 73 73 0 0 0-3.2 6.6 96.6 96.6 0 0 0-29 0A66.8 66.8 0 0 0 45.6 18.2a0.4 0.4 0 0 0-0.4-0.2 104.2 104.2 0 0 0-26.1 8.4 0.4 0.4 0 0 0-0.2 0.2C2.3 51.4-2.2 75.7 0 99.7a0.4 0.4 0 0 0 0.2 0.3 105.2 105.2 0 0 0 31.6 16 0.4 0.4 0 0 0 0.5-0.1 75.6 75.6 0 0 0 6.5-10.6 0.4 0.4 0 0 0-0.2-0.5 69.4 69.4 0 0 1-9.9-4.7 0.4 0.4 0 0 1 0-0.7 54.6 54.6 0 0 0 2-1.6 0.4 0.4 0 0 1 0.4-0.1 75 75 0 0 0 63.9 0 0.4 0.4 0 0 1 0.4 0.1c0.6 0.6 1.3 1.1 2 1.6a0.4 0.4 0 0 1 0 0.7 65.2 65.2 0 0 1-9.9 4.7 0.4 0.4 0 0 0-0.2 0.5 84.8 84.8 0 0 0 6.5 10.6 0.4 0.4 0 0 0 0.5 0.1 104.8 104.8 0 0 0 31.6-16 0.4 0.4 0 0 0 0.2-0.3c2.7-27.8-4.5-51.9-18.9-73.1a0.4 0.4 0 0 0-0.2-0.2zM42.5 85a12.3 12.3 0 0 1-11.5-12.9c0-7.1 5-12.9 11.5-12.9 6.6 0 11.7 5.8 11.6 12.9 0 7-5.1 12.9-11.6 12.9zm42.8 0a12.3 12.3 0 0 1-11.5-12.9c0-7.1 5-12.9 11.5-12.9 6.6 0 11.6 5.8 11.5 12.9 0 7-5 12.9-11.5 12.9z"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,30 +0,0 @@
|
||||
import { defineApplication } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
DISCORD_BOT_TOKEN_VARIABLE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
displayName: 'Twenty Discord',
|
||||
description:
|
||||
'Connect Discord to Twenty. Workflow steps post, update, and delete bot messages and add reactions using a Discord bot token shared across the deployment.',
|
||||
logoUrl: 'public/twenty-discord.svg',
|
||||
author: 'Twenty',
|
||||
category: 'Communication',
|
||||
aboutDescription:
|
||||
'Official Discord connector for Twenty CRM. Create a Discord application at https://discord.com/developers/applications, copy its bot token into the DISCORD_BOT_TOKEN application variable, then invite the bot to each server you want workflows to post in. Use workflow actions to post, update, or delete bot messages and add reactions.',
|
||||
websiteUrl: 'https://docs.twenty.com/developers/extend/apps/getting-started',
|
||||
termsUrl: 'https://www.twenty.com/terms',
|
||||
emailSupport: '[email protected]',
|
||||
issueReportUrl: 'https://github.com/twentyhq/twenty/issues',
|
||||
applicationVariables: {
|
||||
DISCORD_BOT_TOKEN: {
|
||||
universalIdentifier: DISCORD_BOT_TOKEN_VARIABLE_UNIVERSAL_IDENTIFIER,
|
||||
description:
|
||||
'Bot token from your Discord application (Developer Portal → Bot tab → Reset Token). Used with the `Bot` auth prefix to call the Discord REST API. The same token authenticates the bot across every guild it has been invited to.',
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { defineCommandMenuItem } from 'twenty-sdk/define';
|
||||
|
||||
import {
|
||||
SEND_DISCORD_MESSAGE_COMMAND_UNIVERSAL_IDENTIFIER,
|
||||
SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineCommandMenuItem({
|
||||
universalIdentifier: SEND_DISCORD_MESSAGE_COMMAND_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Send Discord message',
|
||||
shortLabel: 'Discord message',
|
||||
icon: 'IconBrandDiscord',
|
||||
isPinned: false,
|
||||
availabilityType: 'GLOBAL',
|
||||
frontComponentUniversalIdentifier:
|
||||
SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
-670
@@ -1,670 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
closeSidePanel,
|
||||
enqueueSnackbar,
|
||||
unmountFrontComponent,
|
||||
} from 'twenty-sdk/front-component';
|
||||
|
||||
import { SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
type DiscordGuild = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type DiscordChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
parentId: string | null;
|
||||
position: number;
|
||||
};
|
||||
|
||||
const DISCORD_MESSAGE_MAX_LENGTH = 2000;
|
||||
|
||||
// The front component sandbox dispatches events with a non-standard shape.
|
||||
// Values may live on e.detail.value, e.value, or e.target.value.
|
||||
const readSerializedValue = (
|
||||
e: React.SyntheticEvent<HTMLElement>,
|
||||
): string | undefined => {
|
||||
const obj = e as {
|
||||
detail?: { value?: string };
|
||||
value?: string;
|
||||
target?: { value?: string };
|
||||
};
|
||||
|
||||
if (typeof obj.detail?.value === 'string') return obj.detail.value;
|
||||
if (typeof obj.value === 'string') return obj.value;
|
||||
if (typeof obj.target?.value === 'string') return obj.target.value;
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const onValueChange =
|
||||
(fn: (value: string) => void) => (e: React.SyntheticEvent<HTMLElement>) => {
|
||||
const v = readSerializedValue(e);
|
||||
|
||||
if (typeof v === 'string') fn(v);
|
||||
};
|
||||
|
||||
const callAppRoute = async (
|
||||
path: string,
|
||||
method: 'GET' | 'POST',
|
||||
body?: Record<string, unknown>,
|
||||
) => {
|
||||
const apiBaseUrl = process.env.TWENTY_API_URL;
|
||||
const token =
|
||||
process.env.TWENTY_APP_ACCESS_TOKEN ?? process.env.TWENTY_API_KEY;
|
||||
|
||||
if (!apiBaseUrl || !token) {
|
||||
throw new Error('API configuration missing');
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiBaseUrl}/s${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
|
||||
throw new Error(
|
||||
`Request failed (${response.status}): ${text.slice(0, 200)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const COLOR = {
|
||||
bg: '#f1f1f1',
|
||||
card: '#ffffff',
|
||||
surface: '#fcfcfc',
|
||||
border: '#ebebeb',
|
||||
borderStrong: '#d6d6d6',
|
||||
text: '#333333',
|
||||
textSecondary: '#666666',
|
||||
textTertiary: '#999999',
|
||||
placeholder: '#cccccc',
|
||||
accent: '#5865f2',
|
||||
error: '#e05252',
|
||||
};
|
||||
|
||||
const INLINE_SELECT: React.CSSProperties = {
|
||||
appearance: 'none',
|
||||
WebkitAppearance: 'none' as const,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
color: 'inherit',
|
||||
font: 'inherit',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
const STYLES = {
|
||||
outer: {
|
||||
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||
fontSize: '13px',
|
||||
backgroundColor: COLOR.bg,
|
||||
padding: '12px',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
},
|
||||
container: {
|
||||
backgroundColor: COLOR.card,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
color: COLOR.text,
|
||||
},
|
||||
topBar: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 12px',
|
||||
borderBottom: `1px solid ${COLOR.border}`,
|
||||
flexShrink: 0,
|
||||
},
|
||||
guildSelect: {
|
||||
appearance: 'none' as const,
|
||||
WebkitAppearance: 'none' as const,
|
||||
background: COLOR.surface,
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
borderRadius: '6px',
|
||||
padding: '3px 8px',
|
||||
color: COLOR.text,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
},
|
||||
channelPicker: {
|
||||
flexShrink: 0,
|
||||
padding: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
borderBottom: `1px solid ${COLOR.border}`,
|
||||
},
|
||||
channelChip: {
|
||||
appearance: 'none' as const,
|
||||
WebkitAppearance: 'none' as const,
|
||||
background: 'transparent',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
borderRadius: '20px',
|
||||
padding: '3px 9px',
|
||||
color: COLOR.textSecondary,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
display: 'inline-flex' as const,
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
whiteSpace: 'nowrap' as const,
|
||||
lineHeight: '16px',
|
||||
},
|
||||
chipIcon: {
|
||||
fontSize: '12px',
|
||||
lineHeight: 1,
|
||||
flexShrink: 0,
|
||||
color: COLOR.textTertiary,
|
||||
},
|
||||
optionsLoadingIndicator: {
|
||||
marginLeft: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
spinner: {
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
border: `2px solid ${COLOR.border}`,
|
||||
borderTopColor: COLOR.textSecondary,
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 0.6s linear infinite',
|
||||
},
|
||||
body: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
padding: '24px',
|
||||
overflow: 'auto',
|
||||
minHeight: 0,
|
||||
gap: '6px',
|
||||
},
|
||||
messageInput: {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: COLOR.text,
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit',
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
resize: 'none' as const,
|
||||
flex: 1,
|
||||
minHeight: '120px',
|
||||
},
|
||||
charCount: {
|
||||
fontSize: '11px',
|
||||
textAlign: 'right' as const,
|
||||
color: COLOR.textTertiary,
|
||||
flexShrink: 0,
|
||||
},
|
||||
charCountOverLimit: {
|
||||
color: COLOR.error,
|
||||
},
|
||||
actionBar: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '6px',
|
||||
padding: '6px 12px',
|
||||
borderTop: `1px solid ${COLOR.border}`,
|
||||
flexShrink: 0,
|
||||
},
|
||||
cancelButton: {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: COLOR.textSecondary,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 10px',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
sendButton: {
|
||||
background: COLOR.accent,
|
||||
border: 'none',
|
||||
color: '#ffffff',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 14px',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
sendButtonDisabled: {
|
||||
background: COLOR.borderStrong,
|
||||
color: COLOR.textTertiary,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
successOuter: {
|
||||
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||
backgroundColor: COLOR.bg,
|
||||
padding: '12px',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
},
|
||||
successContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '10px 14px',
|
||||
boxSizing: 'border-box' as const,
|
||||
backgroundColor: COLOR.card,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
color: COLOR.text,
|
||||
},
|
||||
successCheck: {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#2ea043',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '11px',
|
||||
color: '#fff',
|
||||
flexShrink: 0,
|
||||
},
|
||||
successText: {
|
||||
color: COLOR.text,
|
||||
fontWeight: 500,
|
||||
fontSize: '13px',
|
||||
flex: 1,
|
||||
overflow: 'hidden' as const,
|
||||
textOverflow: 'ellipsis' as const,
|
||||
whiteSpace: 'nowrap' as const,
|
||||
},
|
||||
closeButton: {
|
||||
background: 'transparent',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
color: COLOR.textSecondary,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 14px',
|
||||
borderRadius: '6px',
|
||||
flexShrink: 0,
|
||||
},
|
||||
loadingOuter: {
|
||||
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||
backgroundColor: COLOR.bg,
|
||||
padding: '12px',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
},
|
||||
loadingContainer: {
|
||||
color: COLOR.textSecondary,
|
||||
fontSize: '13px',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: COLOR.card,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
errorOuter: {
|
||||
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
||||
backgroundColor: COLOR.bg,
|
||||
padding: '12px',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
},
|
||||
errorContainer: {
|
||||
backgroundColor: COLOR.card,
|
||||
borderRadius: '8px',
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
padding: '24px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
},
|
||||
errorText: {
|
||||
color: COLOR.error,
|
||||
fontSize: '12px',
|
||||
textAlign: 'center' as const,
|
||||
},
|
||||
retryButton: {
|
||||
background: COLOR.surface,
|
||||
border: `1px solid ${COLOR.border}`,
|
||||
color: COLOR.text,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'inherit',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 12px',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
};
|
||||
|
||||
const SendMessageForm = () => {
|
||||
const [guilds, setGuilds] = useState<DiscordGuild[]>([]);
|
||||
const [guildsLoading, setGuildsLoading] = useState(true);
|
||||
const [guildsError, setGuildsError] = useState<string | null>(null);
|
||||
|
||||
const [channels, setChannels] = useState<DiscordChannel[]>([]);
|
||||
const [channelsLoading, setChannelsLoading] = useState(false);
|
||||
|
||||
const [selectedGuildId, setSelectedGuildId] = useState('');
|
||||
const [selectedChannelId, setSelectedChannelId] = useState('');
|
||||
const [messageText, setMessageText] = useState('');
|
||||
|
||||
const [sending, setSending] = useState(false);
|
||||
const [sentChannelName, setSentChannelName] = useState<string | null>(null);
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
const fetchChannels = useCallback(async (guildId: string) => {
|
||||
if (!guildId) {
|
||||
setChannels([]);
|
||||
setSelectedChannelId('');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setChannelsLoading(true);
|
||||
|
||||
try {
|
||||
const result = await callAppRoute(
|
||||
`/discord/channels?guildId=${encodeURIComponent(guildId)}`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
setChannels([]);
|
||||
await enqueueSnackbar({
|
||||
message: result.error ?? 'Failed to load channels',
|
||||
variant: 'error',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setChannels(result.channels ?? []);
|
||||
} catch (error) {
|
||||
setChannels([]);
|
||||
await enqueueSnackbar({
|
||||
message:
|
||||
error instanceof Error ? error.message : 'Failed to load channels',
|
||||
variant: 'error',
|
||||
});
|
||||
} finally {
|
||||
setChannelsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleGuildChange = useCallback(
|
||||
(newGuildId: string) => {
|
||||
setSelectedGuildId(newGuildId);
|
||||
setSelectedChannelId('');
|
||||
fetchChannels(newGuildId);
|
||||
},
|
||||
[fetchChannels],
|
||||
);
|
||||
|
||||
const fetchGuilds = useCallback(async () => {
|
||||
try {
|
||||
setGuildsError(null);
|
||||
const result = await callAppRoute('/discord/guilds', 'GET');
|
||||
|
||||
if (!result.success) {
|
||||
setGuildsError(result.error ?? 'Failed to load Discord servers');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchedGuilds: DiscordGuild[] = result.guilds ?? [];
|
||||
|
||||
setGuilds(fetchedGuilds);
|
||||
|
||||
// Auto-select when the bot is in exactly one server to skip a needless step.
|
||||
if (fetchedGuilds.length === 1) {
|
||||
setSelectedGuildId(fetchedGuilds[0].id);
|
||||
fetchChannels(fetchedGuilds[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
setGuildsError(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to load Discord servers',
|
||||
);
|
||||
} finally {
|
||||
setGuildsLoading(false);
|
||||
}
|
||||
}, [fetchChannels]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGuilds();
|
||||
}, [fetchGuilds]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const trimmedMessage = (messageText ?? '').trim();
|
||||
|
||||
if (
|
||||
!selectedGuildId ||
|
||||
!selectedChannelId ||
|
||||
trimmedMessage.length === 0 ||
|
||||
trimmedMessage.length > DISCORD_MESSAGE_MAX_LENGTH
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSending(true);
|
||||
|
||||
try {
|
||||
const result = await callAppRoute('/discord/messages', 'POST', {
|
||||
channelId: selectedChannelId,
|
||||
messageText: trimmedMessage,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
await enqueueSnackbar({
|
||||
message: result.error ?? 'Failed to send message',
|
||||
variant: 'error',
|
||||
});
|
||||
setSending(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const channelName =
|
||||
channels.find((channel) => channel.id === selectedChannelId)?.name ??
|
||||
'channel';
|
||||
|
||||
setSentChannelName(channelName);
|
||||
await enqueueSnackbar({
|
||||
message: `Message sent to #${channelName}`,
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
await enqueueSnackbar({
|
||||
message:
|
||||
error instanceof Error ? error.message : 'Failed to send message',
|
||||
variant: 'error',
|
||||
});
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setClosing(true);
|
||||
unmountFrontComponent();
|
||||
closeSidePanel();
|
||||
};
|
||||
|
||||
if (closing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sentChannelName) {
|
||||
return (
|
||||
<div style={STYLES.successOuter}>
|
||||
<div style={STYLES.successContainer}>
|
||||
<div style={STYLES.successCheck}>{'✓'}</div>
|
||||
<span style={STYLES.successText}>
|
||||
Message sent to #{sentChannelName}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
style={STYLES.closeButton}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (guildsLoading) {
|
||||
return (
|
||||
<div style={STYLES.loadingOuter}>
|
||||
<div style={STYLES.loadingContainer}>Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (guildsError) {
|
||||
return (
|
||||
<div style={STYLES.errorOuter}>
|
||||
<div style={STYLES.errorContainer}>
|
||||
<p style={STYLES.errorText}>{guildsError}</p>
|
||||
<button
|
||||
type="button"
|
||||
style={STYLES.retryButton}
|
||||
onClick={fetchGuilds}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const trimmedLength = (messageText ?? '').trim().length;
|
||||
const isOverLimit = (messageText ?? '').length > DISCORD_MESSAGE_MAX_LENGTH;
|
||||
const canSubmit =
|
||||
!!selectedGuildId &&
|
||||
!!selectedChannelId &&
|
||||
trimmedLength > 0 &&
|
||||
!isOverLimit &&
|
||||
!sending;
|
||||
|
||||
return (
|
||||
<div style={STYLES.outer}>
|
||||
<style>{'@keyframes spin{to{transform:rotate(360deg)}}'}</style>
|
||||
<div style={STYLES.container}>
|
||||
<div style={STYLES.topBar}>
|
||||
<select
|
||||
value={selectedGuildId}
|
||||
onChange={onValueChange(handleGuildChange)}
|
||||
style={STYLES.guildSelect}
|
||||
>
|
||||
<option value="">Select server...</option>
|
||||
{guilds.map((guild) => (
|
||||
<option key={guild.id} value={guild.id}>
|
||||
{guild.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{channelsLoading && (
|
||||
<div style={STYLES.optionsLoadingIndicator}>
|
||||
<div style={STYLES.spinner} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={STYLES.channelPicker}>
|
||||
<label style={STYLES.channelChip}>
|
||||
<span style={STYLES.chipIcon}>{'#'}</span>
|
||||
<select
|
||||
value={selectedChannelId}
|
||||
onChange={onValueChange(setSelectedChannelId)}
|
||||
style={INLINE_SELECT}
|
||||
disabled={!selectedGuildId || channelsLoading}
|
||||
>
|
||||
<option value="">
|
||||
{channels.length === 0 ? 'No channels' : 'Select channel'}
|
||||
</option>
|
||||
{channels.map((channel) => (
|
||||
<option key={channel.id} value={channel.id}>
|
||||
{channel.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style={STYLES.body}>
|
||||
<textarea
|
||||
value={messageText}
|
||||
onInput={onValueChange(setMessageText)}
|
||||
onChange={onValueChange(setMessageText)}
|
||||
style={STYLES.messageInput}
|
||||
placeholder="Type a message..."
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
...STYLES.charCount,
|
||||
...(isOverLimit ? STYLES.charCountOverLimit : {}),
|
||||
}}
|
||||
>
|
||||
{(messageText ?? '').length} / {DISCORD_MESSAGE_MAX_LENGTH}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={STYLES.actionBar}>
|
||||
<button
|
||||
type="button"
|
||||
style={STYLES.cancelButton}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
...STYLES.sendButton,
|
||||
...(!canSubmit ? STYLES.sendButtonDisabled : {}),
|
||||
}}
|
||||
disabled={!canSubmit}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{sending ? 'Sending...' : 'Send'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'send-discord-message-form',
|
||||
description:
|
||||
'Form to send a message to a Discord channel: pick the server, pick the channel, type the message, send.',
|
||||
component: SendMessageForm,
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
export const APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'9b1b7628-ac98-4e44-8e1e-91b401378a5f';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'6aa50eb2-e8c2-470f-8c4d-09debbf6e051';
|
||||
|
||||
export const DISCORD_BOT_TOKEN_VARIABLE_UNIVERSAL_IDENTIFIER =
|
||||
'ac986a3b-19a5-4cee-a87a-2f8d3885ad84';
|
||||
|
||||
export const DISCORD_POST_MESSAGE_UNIVERSAL_IDENTIFIER =
|
||||
'6c03d456-c1fa-4acc-9928-d97ced0925fe';
|
||||
|
||||
export const DISCORD_UPDATE_MESSAGE_UNIVERSAL_IDENTIFIER =
|
||||
'b37285d9-cb11-4fe5-963d-0434780122ba';
|
||||
|
||||
export const DISCORD_DELETE_MESSAGE_UNIVERSAL_IDENTIFIER =
|
||||
'a2efc27c-eae0-4713-b327-bdb1aaeff95f';
|
||||
|
||||
export const DISCORD_ADD_REACTION_UNIVERSAL_IDENTIFIER =
|
||||
'32c77c8a-ebd0-4308-aa1b-d21bb94422ac';
|
||||
|
||||
export const DISCORD_LIST_CHANNELS_UNIVERSAL_IDENTIFIER =
|
||||
'ef2296f6-1b48-4edd-81ac-cafa4fe73f99';
|
||||
|
||||
export const DISCORD_LIST_CHANNELS_ROUTE_UNIVERSAL_IDENTIFIER =
|
||||
'550cd993-af6e-46ae-b697-f838f34cfe39';
|
||||
|
||||
export const DISCORD_LIST_GUILDS_UNIVERSAL_IDENTIFIER =
|
||||
'0aaaca4f-3156-4b7c-a9f5-ee6a2e17cbf1';
|
||||
|
||||
export const DISCORD_LIST_GUILDS_ROUTE_UNIVERSAL_IDENTIFIER =
|
||||
'3cfa36d9-3c04-495c-bc09-f8019ad85dce';
|
||||
|
||||
export const DISCORD_POST_MESSAGE_ROUTE_UNIVERSAL_IDENTIFIER =
|
||||
'eda60c95-450b-4a86-b5a0-6b85f488ae81';
|
||||
|
||||
export const SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
|
||||
'ee8d8efa-2f9d-4d2c-b759-bb3cafc7797d';
|
||||
|
||||
export const SEND_DISCORD_MESSAGE_COMMAND_UNIVERSAL_IDENTIFIER =
|
||||
'8c80b9d8-2f18-4b6c-8933-30d170790685';
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_ADD_REACTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordAddReactionHandler } from 'src/logic-functions/handlers/discord-add-reaction-handler';
|
||||
import { discordAddReactionInputSchema } from './schemas/discord-add-reaction-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_ADD_REACTION_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-add-reaction',
|
||||
description:
|
||||
'Add an emoji reaction to a Discord message (for example a checkmark ✅) so the channel can see status at a glance.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordAddReactionInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Add Discord Reaction',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordAddReactionInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
error: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordAddReactionHandler,
|
||||
});
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_DELETE_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordDeleteMessageHandler } from 'src/logic-functions/handlers/discord-delete-message-handler';
|
||||
import { discordDeleteMessageInputSchema } from './schemas/discord-delete-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_DELETE_MESSAGE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-delete-message',
|
||||
description:
|
||||
'Remove a Discord message this bot sent (for example after a mistake or when a workflow is cancelled).',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordDeleteMessageInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Delete Discord Message',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordDeleteMessageInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
error: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordDeleteMessageHandler,
|
||||
});
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { DISCORD_LIST_CHANNELS_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordListChannelsHandler } from 'src/logic-functions/handlers/discord-list-channels-handler';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const params = event.queryStringParameters ?? {};
|
||||
const guildId =
|
||||
typeof params.guildId === 'string' && params.guildId.length > 0
|
||||
? params.guildId
|
||||
: undefined;
|
||||
|
||||
return discordListChannelsHandler({ guildId });
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_LIST_CHANNELS_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-list-channels-route',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/discord/channels',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_LIST_CHANNELS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordListChannelsHandler } from 'src/logic-functions/handlers/discord-list-channels-handler';
|
||||
import { discordListChannelsInputSchema } from './schemas/discord-list-channels-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_LIST_CHANNELS_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-list-channels',
|
||||
description:
|
||||
'List the text-postable channels (types `GUILD_TEXT` and `GUILD_ANNOUNCEMENT`) in a Discord server, sorted by Discord position. Each entry exposes `id`, `name`, `type`, `parentId` (category), and `position`. Pass the server (guild) ID to scope the listing; if omitted and the bot is in exactly one server, that server is auto-selected. If the bot is in multiple servers, the error response lists each server name and ID for explicit selection. Voice channels, threads, forums, and categories are excluded.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordListChannelsInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'List Discord Channels',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordListChannelsInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
channels: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
type: { type: 'number' },
|
||||
parentId: { type: 'string' },
|
||||
position: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: { type: 'number' },
|
||||
error: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordListChannelsHandler,
|
||||
});
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
import { DISCORD_LIST_GUILDS_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordListGuildsHandler } from 'src/logic-functions/handlers/discord-list-guilds-handler';
|
||||
|
||||
const handler = async (_event: RoutePayload) => {
|
||||
return discordListGuildsHandler();
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_LIST_GUILDS_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-list-guilds-route',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/discord/guilds',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_LIST_GUILDS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordListGuildsHandler } from 'src/logic-functions/handlers/discord-list-guilds-handler';
|
||||
import { discordListGuildsInputSchema } from './schemas/discord-list-guilds-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_LIST_GUILDS_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-list-guilds',
|
||||
description:
|
||||
'List the Discord servers (guilds) the bot has been invited to. Each entry exposes `id` and `name`. Use this to discover which servers the bot can post in, then pass the chosen server ID to `discord-list-channels` or other server-scoped actions.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordListGuildsInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'List Discord Servers',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordListGuildsInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
guilds: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: { type: 'number' },
|
||||
error: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordListGuildsHandler,
|
||||
});
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { DISCORD_POST_MESSAGE_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordPostMessageHandler } from 'src/logic-functions/handlers/discord-post-message-handler';
|
||||
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const body = event.body as Record<string, unknown> | null;
|
||||
const rawChannelId = body?.channelId;
|
||||
const rawMessageText = body?.messageText;
|
||||
|
||||
if (typeof rawChannelId !== 'string' || typeof rawMessageText !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to post Discord message',
|
||||
error: '`channelId` and `messageText` must be strings.',
|
||||
};
|
||||
}
|
||||
|
||||
const channelId = rawChannelId.trim();
|
||||
const messageText = rawMessageText.trim();
|
||||
|
||||
if (!isNonEmptyString(channelId) || !isNonEmptyString(messageText)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to post Discord message',
|
||||
error: '`channelId` and `messageText` are required.',
|
||||
};
|
||||
}
|
||||
|
||||
return discordPostMessageHandler({ channelId, messageText });
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_POST_MESSAGE_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-post-message-route',
|
||||
timeoutSeconds: 30,
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/discord/messages',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
});
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_POST_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordPostMessageHandler } from 'src/logic-functions/handlers/discord-post-message-handler';
|
||||
import { discordPostMessageInputSchema } from './schemas/discord-post-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_POST_MESSAGE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-post-message',
|
||||
description:
|
||||
'Send a message to a Discord channel as the bot. Optionally reply to an existing message using its message ID from a previous step.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordPostMessageInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Send Discord Message',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordPostMessageInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
error: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordPostMessageHandler,
|
||||
});
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { DISCORD_UPDATE_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { discordUpdateMessageHandler } from 'src/logic-functions/handlers/discord-update-message-handler';
|
||||
import { discordUpdateMessageInputSchema } from './schemas/discord-update-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: DISCORD_UPDATE_MESSAGE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'discord-update-message',
|
||||
description:
|
||||
'Change the text of a Discord message this bot already sent. You need the channel ID and the message ID from when it was posted.',
|
||||
timeoutSeconds: 30,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: discordUpdateMessageInputSchema,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Update Discord Message',
|
||||
icon: 'IconBrandDiscord',
|
||||
inputSchema: jsonSchemaToInputSchema(discordUpdateMessageInputSchema),
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
error: { type: 'string' },
|
||||
messageId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
handler: discordUpdateMessageHandler,
|
||||
});
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { type DiscordAddReactionInput } from 'src/logic-functions/types/discord-add-reaction-input.type';
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
import { buildDiscordFailureResult } from 'src/logic-functions/utils/build-discord-failure-result';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
export const discordAddReactionHandler = async (
|
||||
parameters: DiscordAddReactionInput,
|
||||
): Promise<DiscordToolResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Discord is not configured',
|
||||
error: tokenResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
const channelId = parameters.channelId.trim();
|
||||
const messageId = parameters.messageId.trim();
|
||||
const emoji = parameters.emoji.trim();
|
||||
|
||||
const encodedChannelId = encodeURIComponent(channelId);
|
||||
const encodedMessageId = encodeURIComponent(messageId);
|
||||
const encodedEmoji = encodeURIComponent(emoji);
|
||||
|
||||
try {
|
||||
const result = await discordApiRequest({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'PUT',
|
||||
path: `/channels/${encodedChannelId}/messages/${encodedMessageId}/reactions/${encodedEmoji}/@me`,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to add Discord reaction',
|
||||
error: result.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Reaction "${emoji}" added to the message.`,
|
||||
messageId,
|
||||
channelId,
|
||||
};
|
||||
} catch (error) {
|
||||
return buildDiscordFailureResult('Failed to add Discord reaction', error);
|
||||
}
|
||||
};
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { type DiscordDeleteMessageInput } from 'src/logic-functions/types/discord-delete-message-input.type';
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
import { buildDiscordFailureResult } from 'src/logic-functions/utils/build-discord-failure-result';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
export const discordDeleteMessageHandler = async (
|
||||
parameters: DiscordDeleteMessageInput,
|
||||
): Promise<DiscordToolResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Discord is not configured',
|
||||
error: tokenResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await discordApiRequest({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'DELETE',
|
||||
path: `/channels/${encodeURIComponent(
|
||||
parameters.channelId,
|
||||
)}/messages/${encodeURIComponent(parameters.messageId)}`,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to delete Discord message',
|
||||
error: result.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Discord message deleted.',
|
||||
messageId: parameters.messageId,
|
||||
channelId: parameters.channelId,
|
||||
};
|
||||
} catch (error) {
|
||||
return buildDiscordFailureResult('Failed to delete Discord message', error);
|
||||
}
|
||||
};
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type DiscordListChannelsInput } from 'src/logic-functions/types/discord-list-channels-input.type';
|
||||
import {
|
||||
type DiscordListChannelsResult,
|
||||
type DiscordListChannelsResultChannel,
|
||||
} from 'src/logic-functions/types/discord-list-channels-output.type';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { fetchDiscordGuilds } from 'src/logic-functions/utils/fetch-discord-guilds';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
const GUILD_TEXT_CHANNEL_TYPE = 0;
|
||||
const GUILD_ANNOUNCEMENT_CHANNEL_TYPE = 5;
|
||||
|
||||
const POSTABLE_CHANNEL_TYPES = new Set<number>([
|
||||
GUILD_TEXT_CHANNEL_TYPE,
|
||||
GUILD_ANNOUNCEMENT_CHANNEL_TYPE,
|
||||
]);
|
||||
|
||||
type DiscordChannelResponse = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
type: number;
|
||||
parent_id: string | null;
|
||||
position: number;
|
||||
};
|
||||
|
||||
const buildFailureResult = (error: string): DiscordListChannelsResult => ({
|
||||
success: false,
|
||||
channels: [],
|
||||
count: 0,
|
||||
error,
|
||||
});
|
||||
|
||||
const resolveGuildId = async ({
|
||||
botToken,
|
||||
providedGuildId,
|
||||
}: {
|
||||
botToken: string;
|
||||
providedGuildId: string | undefined;
|
||||
}): Promise<{ ok: true; guildId: string } | { ok: false; error: string }> => {
|
||||
const trimmed = providedGuildId?.trim();
|
||||
|
||||
if (isDefined(trimmed) && trimmed.length > 0) {
|
||||
return { ok: true, guildId: trimmed };
|
||||
}
|
||||
|
||||
const guildsResult = await fetchDiscordGuilds(botToken);
|
||||
|
||||
if (!guildsResult.ok) {
|
||||
return { ok: false, error: guildsResult.errorMessage };
|
||||
}
|
||||
|
||||
const guilds = guildsResult.data;
|
||||
|
||||
if (guilds.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
'Bot has not been invited to any Discord server yet. Use the OAuth2 → URL Generator in the Discord Developer Portal (scope `bot`) to invite it to a server, then retry.',
|
||||
};
|
||||
}
|
||||
|
||||
if (guilds.length > 1) {
|
||||
const guildList = guilds
|
||||
.map((guild) => `"${guild.name}" (${guild.id})`)
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: `Bot is in ${guilds.length} Discord servers — please specify which one via the guildId input. Available: ${guildList}.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, guildId: guilds[0].id };
|
||||
};
|
||||
|
||||
export const discordListChannelsHandler = async (
|
||||
parameters: DiscordListChannelsInput,
|
||||
): Promise<DiscordListChannelsResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return buildFailureResult(tokenResult.error);
|
||||
}
|
||||
|
||||
const guildIdResult = await resolveGuildId({
|
||||
botToken: tokenResult.botToken,
|
||||
providedGuildId: parameters.guildId,
|
||||
});
|
||||
|
||||
if (!guildIdResult.ok) {
|
||||
return buildFailureResult(guildIdResult.error);
|
||||
}
|
||||
|
||||
const result = await discordApiRequest<DiscordChannelResponse[]>({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'GET',
|
||||
path: `/guilds/${encodeURIComponent(guildIdResult.guildId)}/channels`,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return buildFailureResult(result.errorMessage);
|
||||
}
|
||||
|
||||
const channels: DiscordListChannelsResultChannel[] = result.data
|
||||
.filter(
|
||||
(channel) =>
|
||||
POSTABLE_CHANNEL_TYPES.has(channel.type) && isDefined(channel.name),
|
||||
)
|
||||
.map((channel) => ({
|
||||
id: channel.id,
|
||||
name: channel.name as string,
|
||||
type: channel.type,
|
||||
parentId: channel.parent_id,
|
||||
position: channel.position,
|
||||
}))
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
channels,
|
||||
count: channels.length,
|
||||
};
|
||||
};
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { type DiscordListGuildsResult } from 'src/logic-functions/types/discord-list-guilds-output.type';
|
||||
import { fetchDiscordGuilds } from 'src/logic-functions/utils/fetch-discord-guilds';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
const buildFailureResult = (error: string): DiscordListGuildsResult => ({
|
||||
success: false,
|
||||
guilds: [],
|
||||
count: 0,
|
||||
error,
|
||||
});
|
||||
|
||||
export const discordListGuildsHandler =
|
||||
async (): Promise<DiscordListGuildsResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return buildFailureResult(tokenResult.error);
|
||||
}
|
||||
|
||||
const result = await fetchDiscordGuilds(tokenResult.botToken);
|
||||
|
||||
if (!result.ok) {
|
||||
return buildFailureResult(result.errorMessage);
|
||||
}
|
||||
|
||||
const guilds = result.data.map((guild) => ({
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
}));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
guilds,
|
||||
count: guilds.length,
|
||||
};
|
||||
};
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type DiscordPostMessageInput } from 'src/logic-functions/types/discord-post-message-input.type';
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
import { buildDiscordFailureResult } from 'src/logic-functions/utils/build-discord-failure-result';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
type DiscordMessageResponse = {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
};
|
||||
|
||||
export const discordPostMessageHandler = async (
|
||||
parameters: DiscordPostMessageInput,
|
||||
): Promise<DiscordToolResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Discord is not configured',
|
||||
error: tokenResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
const replyToMessageId = parameters.replyToMessageId?.trim();
|
||||
const body: Record<string, unknown> = {
|
||||
content: parameters.messageText,
|
||||
};
|
||||
|
||||
if (isDefined(replyToMessageId) && replyToMessageId.length > 0) {
|
||||
body.message_reference = { message_id: replyToMessageId };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await discordApiRequest<DiscordMessageResponse>({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'POST',
|
||||
path: `/channels/${encodeURIComponent(parameters.channelId)}/messages`,
|
||||
body,
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to post Discord message',
|
||||
error: result.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Message posted to Discord (id=${result.data.id}).`,
|
||||
messageId: result.data.id,
|
||||
channelId: result.data.channel_id,
|
||||
};
|
||||
} catch (error) {
|
||||
return buildDiscordFailureResult('Failed to post Discord message', error);
|
||||
}
|
||||
};
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
import { type DiscordUpdateMessageInput } from 'src/logic-functions/types/discord-update-message-input.type';
|
||||
import { buildDiscordFailureResult } from 'src/logic-functions/utils/build-discord-failure-result';
|
||||
import { discordApiRequest } from 'src/logic-functions/utils/discord-api-request';
|
||||
import { getDiscordBotToken } from 'src/logic-functions/utils/get-discord-bot-token';
|
||||
|
||||
type DiscordMessageResponse = {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
};
|
||||
|
||||
export const discordUpdateMessageHandler = async (
|
||||
parameters: DiscordUpdateMessageInput,
|
||||
): Promise<DiscordToolResult> => {
|
||||
const tokenResult = getDiscordBotToken();
|
||||
|
||||
if (!tokenResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Discord is not configured',
|
||||
error: tokenResult.error,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await discordApiRequest<DiscordMessageResponse>({
|
||||
botToken: tokenResult.botToken,
|
||||
method: 'PATCH',
|
||||
path: `/channels/${encodeURIComponent(
|
||||
parameters.channelId,
|
||||
)}/messages/${encodeURIComponent(parameters.messageId)}`,
|
||||
body: { content: parameters.newMessageText },
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to update Discord message',
|
||||
error: result.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Discord message updated.',
|
||||
messageId: result.data.id,
|
||||
channelId: result.data.channel_id,
|
||||
};
|
||||
} catch (error) {
|
||||
return buildDiscordFailureResult('Failed to update Discord message', error);
|
||||
}
|
||||
};
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordAddReactionInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
label: 'Discord channel ID',
|
||||
description: 'Channel where the message lives (Discord channel ID).',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
label: 'Message ID',
|
||||
description:
|
||||
'The message to react to: its **message ID** (same value as `messageId` returned when the bot posted it, or right-click the message → "Copy Message ID").',
|
||||
},
|
||||
emoji: {
|
||||
type: 'string',
|
||||
label: 'Emoji',
|
||||
description:
|
||||
'Emoji to add. For a standard unicode emoji, paste the emoji directly (e.g. `👍`, `✅`, `🎉`). For a custom server emoji, use the format `name:id` (e.g. `partyparrot:643452342342342342`). The bot must have **Add Reactions** permission in the channel.',
|
||||
},
|
||||
},
|
||||
required: ['channelId', 'messageId', 'emoji'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordDeleteMessageInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
label: 'Discord channel ID',
|
||||
description: 'Channel that contains the message (Discord channel ID).',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
label: 'Message ID',
|
||||
description:
|
||||
'Which message to remove: its **message ID** (same value as `messageId` returned when the bot posted it). The bot can delete its own messages without extra permissions; deleting others\' messages requires the **Manage Messages** permission.',
|
||||
},
|
||||
},
|
||||
required: ['channelId', 'messageId'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordListChannelsInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
guildId: {
|
||||
type: 'string',
|
||||
label: 'Discord server (guild) ID',
|
||||
description:
|
||||
'Optional. The Discord server ID to list channels for (a long number like `1234567890123456789`). In Discord with Developer Mode on: right-click the server icon → "Copy Server ID". If omitted and the bot is in exactly one server, that server is used automatically; if the bot is in multiple servers, the error response lists each server name and ID so you can pick one.',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordListGuildsInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordPostMessageInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
label: 'Discord channel ID',
|
||||
description:
|
||||
'Where to post: the Discord channel ID (a long number like `1234567890123456789`). In Discord with Developer Mode on: right-click the channel → "Copy Channel ID". The bot must be a member of the server and have permission to send messages in this channel.',
|
||||
},
|
||||
messageText: {
|
||||
type: 'string',
|
||||
label: 'Message',
|
||||
multiline: true,
|
||||
description:
|
||||
'The message text. Supports Discord-flavoured markdown (**bold**, *italics*, ```code blocks```, lists, links). Max 2000 characters — Discord rejects longer payloads.',
|
||||
},
|
||||
replyToMessageId: {
|
||||
type: 'string',
|
||||
label: 'Reply to message ID',
|
||||
description:
|
||||
'Optional. To reply to an existing message, paste its ID (from a previous step\'s `messageId` output, or right-click the message → "Copy Message ID"). Leave empty for a normal new message.',
|
||||
},
|
||||
},
|
||||
required: ['channelId', 'messageText'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const discordUpdateMessageInputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channelId: {
|
||||
type: 'string',
|
||||
label: 'Discord channel ID',
|
||||
description: 'Channel that contains the message (Discord channel ID).',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
label: 'Message ID',
|
||||
description:
|
||||
'Which message to edit: its **message ID** (same value as `messageId` returned when the bot posted the message). You can only edit messages this bot sent.',
|
||||
},
|
||||
newMessageText: {
|
||||
type: 'string',
|
||||
label: 'New message',
|
||||
multiline: true,
|
||||
description:
|
||||
'Replacement text shown in Discord instead of the old content. Supports Discord-flavoured markdown. Max 2000 characters.',
|
||||
},
|
||||
},
|
||||
required: ['channelId', 'messageId', 'newMessageText'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
export type DiscordAddReactionInput = {
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
emoji: string;
|
||||
};
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
export type DiscordDeleteMessageInput = {
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
};
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export type DiscordListChannelsInput = {
|
||||
guildId?: string;
|
||||
};
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
export type DiscordListChannelsResultChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
parentId: string | null;
|
||||
position: number;
|
||||
};
|
||||
|
||||
export type DiscordListChannelsResult = {
|
||||
success: boolean;
|
||||
channels: DiscordListChannelsResultChannel[];
|
||||
count: number;
|
||||
error?: string;
|
||||
};
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
export type DiscordListGuildsResultGuild = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type DiscordListGuildsResult = {
|
||||
success: boolean;
|
||||
guilds: DiscordListGuildsResultGuild[];
|
||||
count: number;
|
||||
error?: string;
|
||||
};
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
export type DiscordPostMessageInput = {
|
||||
channelId: string;
|
||||
messageText: string;
|
||||
replyToMessageId?: string;
|
||||
};
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
export type DiscordToolResult = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
error?: string;
|
||||
messageId?: string;
|
||||
channelId?: string;
|
||||
};
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
export type DiscordUpdateMessageInput = {
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
newMessageText: string;
|
||||
};
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { type DiscordToolResult } from 'src/logic-functions/types/discord-tool-result.type';
|
||||
|
||||
export const buildDiscordFailureResult = (
|
||||
message: string,
|
||||
error: unknown,
|
||||
): DiscordToolResult => ({
|
||||
success: false,
|
||||
message,
|
||||
error: error instanceof Error ? error.message : 'Discord request failed',
|
||||
});
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const DISCORD_API_BASE_URL = 'https://discord.com/api/v10';
|
||||
|
||||
type DiscordApiSuccess<TData> = {
|
||||
ok: true;
|
||||
status: number;
|
||||
data: TData;
|
||||
};
|
||||
|
||||
type DiscordApiFailure = {
|
||||
ok: false;
|
||||
status: number;
|
||||
errorMessage: string;
|
||||
};
|
||||
|
||||
export type DiscordApiResult<TData> =
|
||||
| DiscordApiSuccess<TData>
|
||||
| DiscordApiFailure;
|
||||
|
||||
type DiscordErrorBody = {
|
||||
code?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type DiscordApiRequestParams = {
|
||||
botToken: string;
|
||||
path: string;
|
||||
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||
body?: unknown;
|
||||
};
|
||||
|
||||
export const discordApiRequest = async <TData = unknown>({
|
||||
botToken,
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
}: DiscordApiRequestParams): Promise<DiscordApiResult<TData>> => {
|
||||
const url = `${DISCORD_API_BASE_URL}${path}`;
|
||||
const hasBody = isDefined(body);
|
||||
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bot ${botToken}`,
|
||||
...(hasBody ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
...(hasBody ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 0,
|
||||
errorMessage: `Discord API request failed: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return { ok: true, status: 204, data: undefined as TData };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = await extractDiscordErrorMessage(response);
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const data = (await response.json()) as TData;
|
||||
|
||||
return { ok: true, status: response.status, data };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
errorMessage: `Discord API returned a non-JSON response: ${
|
||||
(error as Error).message
|
||||
}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const extractDiscordErrorMessage = async (
|
||||
response: Response,
|
||||
): Promise<string> => {
|
||||
const fallback = `Discord API responded with ${response.status}`;
|
||||
|
||||
try {
|
||||
const errorBody = (await response.json()) as DiscordErrorBody;
|
||||
|
||||
if (isDefined(errorBody.message)) {
|
||||
return isDefined(errorBody.code)
|
||||
? `${errorBody.message} (Discord error code ${errorBody.code})`
|
||||
: errorBody.message;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import {
|
||||
discordApiRequest,
|
||||
type DiscordApiResult,
|
||||
} from 'src/logic-functions/utils/discord-api-request';
|
||||
|
||||
export type DiscordGuildResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const fetchDiscordGuilds = (
|
||||
botToken: string,
|
||||
): Promise<DiscordApiResult<DiscordGuildResponse[]>> =>
|
||||
discordApiRequest<DiscordGuildResponse[]>({
|
||||
botToken,
|
||||
method: 'GET',
|
||||
path: '/users/@me/guilds',
|
||||
});
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const DISCORD_BOT_TOKEN_ENV_VAR = 'DISCORD_BOT_TOKEN';
|
||||
|
||||
export const getDiscordBotToken = ():
|
||||
| { success: true; botToken: string }
|
||||
| { success: false; error: string } => {
|
||||
const botToken = process.env[DISCORD_BOT_TOKEN_ENV_VAR];
|
||||
|
||||
if (!isDefined(botToken) || botToken.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
'Discord is not configured. Open the Twenty Discord app settings and set the DISCORD_BOT_TOKEN application variable (Developer Portal → Bot tab → Reset Token).',
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, botToken };
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { defineApplicationRole } from 'twenty-sdk/define';
|
||||
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
label: 'Twenty Discord tools role',
|
||||
description:
|
||||
'No CRM data access — tools only forward requests to Discord using the configured bot token.',
|
||||
canReadAllObjectRecords: false,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
canUpdateAllSettings: false,
|
||||
canBeAssignedToAgents: false,
|
||||
canBeAssignedToUsers: false,
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlags: [],
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"allowUnreachableCode": false,
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": false,
|
||||
"target": "es2020",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"src/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "2.5.0",
|
||||
"version": "2.4.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -244,7 +244,7 @@ type FieldPermission {
|
||||
canUpdateFieldValue: Boolean
|
||||
}
|
||||
|
||||
type RolePermissionFlag {
|
||||
type PermissionFlag {
|
||||
id: UUID!
|
||||
roleId: UUID!
|
||||
flag: PermissionFlagType!
|
||||
@@ -276,7 +276,7 @@ type Role {
|
||||
canUpdateAllObjectRecords: Boolean!
|
||||
canSoftDeleteAllObjectRecords: Boolean!
|
||||
canDestroyAllObjectRecords: Boolean!
|
||||
permissionFlags: [RolePermissionFlag!]
|
||||
permissionFlags: [PermissionFlag!]
|
||||
objectPermissions: [ObjectPermission!]
|
||||
fieldPermissions: [FieldPermission!]
|
||||
rowLevelPermissionPredicates: [RowLevelPermissionPredicate!]
|
||||
@@ -323,7 +323,6 @@ type FrontComponent {
|
||||
isHeadless: Boolean!
|
||||
usesSdkClient: Boolean!
|
||||
applicationTokenPair: ApplicationTokenPair
|
||||
applicationVariables: JSON
|
||||
}
|
||||
|
||||
type CommandMenuItem {
|
||||
@@ -771,7 +770,6 @@ type ViewFilter {
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
relationTargetFieldMetadataId: UUID
|
||||
viewId: UUID!
|
||||
workspaceId: UUID!
|
||||
createdAt: DateTime!
|
||||
@@ -814,7 +812,6 @@ type ViewSort {
|
||||
id: UUID!
|
||||
fieldMetadataId: UUID!
|
||||
direction: ViewSortDirection!
|
||||
subFieldName: String
|
||||
viewId: UUID!
|
||||
workspaceId: UUID!
|
||||
createdAt: DateTime!
|
||||
@@ -1489,6 +1486,7 @@ enum BillingUsageType {
|
||||
enum BillingProductKey {
|
||||
BASE_PRODUCT
|
||||
RESOURCE_CREDIT
|
||||
WORKFLOW_NODE_EXECUTION
|
||||
}
|
||||
|
||||
type BillingPriceLicensed {
|
||||
@@ -1582,7 +1580,7 @@ type BillingEndTrialPeriod {
|
||||
billingPortalUrl: String
|
||||
}
|
||||
|
||||
type BillingResourceCreditUsage {
|
||||
type BillingMeteredProductUsage {
|
||||
productKey: BillingProductKey!
|
||||
periodStart: DateTime!
|
||||
periodEnd: DateTime!
|
||||
@@ -1751,11 +1749,13 @@ enum FeatureFlagKey {
|
||||
IS_UNIQUE_INDEXES_ENABLED
|
||||
IS_JSON_FILTER_ENABLED
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED
|
||||
IS_PUBLIC_DOMAIN_ENABLED
|
||||
IS_EMAILING_DOMAIN_ENABLED
|
||||
IS_EMAIL_GROUP_ENABLED
|
||||
IS_JUNCTION_RELATIONS_ENABLED
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED
|
||||
IS_BILLING_V2_ENABLED
|
||||
}
|
||||
|
||||
type WorkspaceUrls {
|
||||
@@ -2444,6 +2444,13 @@ type ImapSmtpCaldavConnectionSuccess {
|
||||
connectedAccountId: String!
|
||||
}
|
||||
|
||||
type PostgresCredentials {
|
||||
id: UUID!
|
||||
user: String!
|
||||
password: String!
|
||||
workspaceId: UUID!
|
||||
}
|
||||
|
||||
type ToolIndexEntry {
|
||||
name: String!
|
||||
description: String!
|
||||
@@ -2884,7 +2891,6 @@ enum AllMetadataName {
|
||||
pageLayoutTab
|
||||
commandMenuItem
|
||||
navigationMenuItem
|
||||
rolePermissionFlag
|
||||
permissionFlag
|
||||
objectPermission
|
||||
fieldPermission
|
||||
@@ -2955,7 +2961,7 @@ type Query {
|
||||
enterpriseSubscriptionStatus: EnterpriseSubscriptionStatusDTO
|
||||
billingPortalSession(returnUrlPath: String): BillingSession!
|
||||
listPlans: [BillingPlan!]!
|
||||
getResourceCreditUsage: [BillingResourceCreditUsage!]!
|
||||
getMeteredProductsUsage: [BillingMeteredProductUsage!]!
|
||||
findWorkspaceInvitations: [WorkspaceInvitation!]!
|
||||
getApprovedAccessDomains: [ApprovedAccessDomain!]!
|
||||
getPageLayoutTabs(pageLayoutId: String!): [PageLayoutTab!]!
|
||||
@@ -3057,6 +3063,7 @@ type Query {
|
||||
getAutoCompleteAddress(address: String!, token: String!, country: String, isFieldCity: Boolean): [AutocompleteResult!]!
|
||||
getAddressDetails(placeId: String!, token: String!): PlaceDetailsResult!
|
||||
getUsageAnalytics(input: UsageAnalyticsInput): UsageAnalytics!
|
||||
getPostgresCredentials: PostgresCredentials
|
||||
findManyPublicDomains: [PublicDomain!]!
|
||||
getEmailingDomains: [EmailingDomain!]!
|
||||
findManyMarketplaceApps: [MarketplaceApp!]!
|
||||
@@ -3194,9 +3201,9 @@ type Mutation {
|
||||
switchBillingPlan: BillingUpdate!
|
||||
cancelSwitchBillingPlan: BillingUpdate!
|
||||
cancelSwitchBillingInterval: BillingUpdate!
|
||||
setResourceCreditSubscriptionPrice(priceId: String!): BillingUpdate!
|
||||
setMeteredSubscriptionPrice(priceId: String!): BillingUpdate!
|
||||
endSubscriptionTrialPeriod: BillingEndTrialPeriod!
|
||||
cancelSwitchResourceCreditPrice: BillingUpdate!
|
||||
cancelSwitchMeteredPrice: BillingUpdate!
|
||||
deleteWorkspaceInvitation(appTokenId: String!): String!
|
||||
resendWorkspaceInvitation(appTokenId: String!): SendInvitations!
|
||||
sendInvitations(emails: [String!]!, roleId: UUID): SendInvitations!
|
||||
@@ -3238,7 +3245,7 @@ type Mutation {
|
||||
updateOneRole(updateRoleInput: UpdateRoleInput!): Role!
|
||||
deleteOneRole(roleId: UUID!): String!
|
||||
upsertObjectPermissions(upsertObjectPermissionsInput: UpsertObjectPermissionsInput!): [ObjectPermission!]!
|
||||
upsertPermissionFlags(upsertPermissionFlagsInput: UpsertPermissionFlagsInput!): [RolePermissionFlag!]!
|
||||
upsertPermissionFlags(upsertPermissionFlagsInput: UpsertPermissionFlagsInput!): [PermissionFlag!]!
|
||||
upsertFieldPermissions(upsertFieldPermissionsInput: UpsertFieldPermissionsInput!): [FieldPermission!]!
|
||||
upsertRowLevelPermissionPredicates(input: UpsertRowLevelPermissionPredicatesInput!): UpsertRowLevelPermissionPredicatesResult!
|
||||
assignRoleToAgent(agentId: UUID!, roleId: UUID!): Boolean!
|
||||
@@ -3263,7 +3270,7 @@ type Mutation {
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
createChatThread: AgentChatThread!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileIds: [UUID!]): SendChatMessageResult!
|
||||
stopAgentChatStream(threadId: UUID!): Boolean!
|
||||
renameChatThread(id: UUID!, title: String!): AgentChatThread!
|
||||
archiveChatThread(id: UUID!): AgentChatThread!
|
||||
@@ -3328,6 +3335,8 @@ type Mutation {
|
||||
startChannelSync(connectedAccountId: UUID!): ChannelSyncSuccess!
|
||||
saveImapSmtpCaldavAccount(accountOwnerId: UUID!, handle: String!, connectionParameters: EmailAccountConnectionParameters!, id: UUID): ImapSmtpCaldavConnectionSuccess!
|
||||
updateLabPublicFeatureFlag(input: UpdateLabPublicFeatureFlagInput!): FeatureFlag!
|
||||
enablePostgresProxy: PostgresCredentials!
|
||||
disablePostgresProxy: PostgresCredentials!
|
||||
createPublicDomain(domain: String!, applicationId: String): PublicDomain!
|
||||
updatePublicDomain(domain: String!, applicationId: String): PublicDomain!
|
||||
deletePublicDomain(domain: String!): Boolean!
|
||||
@@ -3418,7 +3427,6 @@ input CreateViewFilterInput {
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
relationTargetFieldMetadataId: UUID
|
||||
viewId: UUID!
|
||||
}
|
||||
|
||||
@@ -3437,7 +3445,6 @@ input UpdateViewFilterInputUpdates {
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
relationTargetFieldMetadataId: UUID
|
||||
}
|
||||
|
||||
input DeleteViewFilterInput {
|
||||
@@ -3526,7 +3533,6 @@ input UpsertViewWidgetViewFilterInput {
|
||||
viewFilterGroupId: UUID
|
||||
positionInViewFilterGroup: Float
|
||||
subFieldName: String
|
||||
relationTargetFieldMetadataId: UUID
|
||||
}
|
||||
|
||||
input UpsertViewWidgetViewFilterGroupInput {
|
||||
@@ -3546,7 +3552,6 @@ input CreateViewSortInput {
|
||||
id: UUID
|
||||
fieldMetadataId: UUID!
|
||||
direction: ViewSortDirection = ASC
|
||||
subFieldName: String
|
||||
viewId: UUID!
|
||||
}
|
||||
|
||||
@@ -3560,7 +3565,6 @@ input UpdateViewSortInput {
|
||||
|
||||
input UpdateViewSortInputUpdates {
|
||||
direction: ViewSortDirection
|
||||
subFieldName: String
|
||||
}
|
||||
|
||||
input DeleteViewSortInput {
|
||||
@@ -4228,11 +4232,6 @@ input UpdateWebhookInputUpdates {
|
||||
secret: String
|
||||
}
|
||||
|
||||
input FileAttachmentInput {
|
||||
id: UUID!
|
||||
filename: String!
|
||||
}
|
||||
|
||||
input CreateSkillInput {
|
||||
id: UUID
|
||||
name: String!
|
||||
|
||||
@@ -190,11 +190,11 @@ export interface FieldPermission {
|
||||
__typename: 'FieldPermission'
|
||||
}
|
||||
|
||||
export interface RolePermissionFlag {
|
||||
export interface PermissionFlag {
|
||||
id: Scalars['UUID']
|
||||
roleId: Scalars['UUID']
|
||||
flag: PermissionFlagType
|
||||
__typename: 'RolePermissionFlag'
|
||||
__typename: 'PermissionFlag'
|
||||
}
|
||||
|
||||
export interface ApiKeyForRole {
|
||||
@@ -224,7 +224,7 @@ export interface Role {
|
||||
canUpdateAllObjectRecords: Scalars['Boolean']
|
||||
canSoftDeleteAllObjectRecords: Scalars['Boolean']
|
||||
canDestroyAllObjectRecords: Scalars['Boolean']
|
||||
permissionFlags?: RolePermissionFlag[]
|
||||
permissionFlags?: PermissionFlag[]
|
||||
objectPermissions?: ObjectPermission[]
|
||||
fieldPermissions?: FieldPermission[]
|
||||
rowLevelPermissionPredicates?: RowLevelPermissionPredicate[]
|
||||
@@ -276,7 +276,6 @@ export interface FrontComponent {
|
||||
isHeadless: Scalars['Boolean']
|
||||
usesSdkClient: Scalars['Boolean']
|
||||
applicationTokenPair?: ApplicationTokenPair
|
||||
applicationVariables?: Scalars['JSON']
|
||||
__typename: 'FrontComponent'
|
||||
}
|
||||
|
||||
@@ -523,7 +522,6 @@ export interface ViewFilter {
|
||||
viewFilterGroupId?: Scalars['UUID']
|
||||
positionInViewFilterGroup?: Scalars['Float']
|
||||
subFieldName?: Scalars['String']
|
||||
relationTargetFieldMetadataId?: Scalars['UUID']
|
||||
viewId: Scalars['UUID']
|
||||
workspaceId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
@@ -551,7 +549,6 @@ export interface ViewSort {
|
||||
id: Scalars['UUID']
|
||||
fieldMetadataId: Scalars['UUID']
|
||||
direction: ViewSortDirection
|
||||
subFieldName?: Scalars['String']
|
||||
viewId: Scalars['UUID']
|
||||
workspaceId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
@@ -1149,7 +1146,7 @@ export type BillingUsageType = 'METERED' | 'LICENSED'
|
||||
|
||||
|
||||
/** The different billing products available */
|
||||
export type BillingProductKey = 'BASE_PRODUCT' | 'RESOURCE_CREDIT'
|
||||
export type BillingProductKey = 'BASE_PRODUCT' | 'RESOURCE_CREDIT' | 'WORKFLOW_NODE_EXECUTION'
|
||||
|
||||
export interface BillingPriceLicensed {
|
||||
recurringInterval: SubscriptionInterval
|
||||
@@ -1235,7 +1232,7 @@ export interface BillingEndTrialPeriod {
|
||||
__typename: 'BillingEndTrialPeriod'
|
||||
}
|
||||
|
||||
export interface BillingResourceCreditUsage {
|
||||
export interface BillingMeteredProductUsage {
|
||||
productKey: BillingProductKey
|
||||
periodStart: Scalars['DateTime']
|
||||
periodEnd: Scalars['DateTime']
|
||||
@@ -1244,7 +1241,7 @@ export interface BillingResourceCreditUsage {
|
||||
rolloverCredits: Scalars['Float']
|
||||
totalGrantedCredits: Scalars['Float']
|
||||
unitPriceCents: Scalars['Float']
|
||||
__typename: 'BillingResourceCreditUsage'
|
||||
__typename: 'BillingMeteredProductUsage'
|
||||
}
|
||||
|
||||
export interface BillingPlan {
|
||||
@@ -1392,7 +1389,7 @@ export interface FeatureFlag {
|
||||
__typename: 'FeatureFlag'
|
||||
}
|
||||
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT'
|
||||
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAILING_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' | 'IS_BILLING_V2_ENABLED'
|
||||
|
||||
export interface WorkspaceUrls {
|
||||
customUrl?: Scalars['String']
|
||||
@@ -2125,6 +2122,14 @@ export interface ImapSmtpCaldavConnectionSuccess {
|
||||
__typename: 'ImapSmtpCaldavConnectionSuccess'
|
||||
}
|
||||
|
||||
export interface PostgresCredentials {
|
||||
id: Scalars['UUID']
|
||||
user: Scalars['String']
|
||||
password: Scalars['String']
|
||||
workspaceId: Scalars['UUID']
|
||||
__typename: 'PostgresCredentials'
|
||||
}
|
||||
|
||||
export interface ToolIndexEntry {
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
@@ -2516,7 +2521,7 @@ export interface CollectionHash {
|
||||
__typename: 'CollectionHash'
|
||||
}
|
||||
|
||||
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'rolePermissionFlag' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
|
||||
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
|
||||
|
||||
export interface MinimalObjectMetadata {
|
||||
id: Scalars['UUID']
|
||||
@@ -2583,7 +2588,7 @@ export interface Query {
|
||||
enterpriseSubscriptionStatus?: EnterpriseSubscriptionStatusDTO
|
||||
billingPortalSession: BillingSession
|
||||
listPlans: BillingPlan[]
|
||||
getResourceCreditUsage: BillingResourceCreditUsage[]
|
||||
getMeteredProductsUsage: BillingMeteredProductUsage[]
|
||||
findWorkspaceInvitations: WorkspaceInvitation[]
|
||||
getApprovedAccessDomains: ApprovedAccessDomain[]
|
||||
getPageLayoutTabs: PageLayoutTab[]
|
||||
@@ -2658,6 +2663,7 @@ export interface Query {
|
||||
getAutoCompleteAddress: AutocompleteResult[]
|
||||
getAddressDetails: PlaceDetailsResult
|
||||
getUsageAnalytics: UsageAnalytics
|
||||
getPostgresCredentials?: PostgresCredentials
|
||||
findManyPublicDomains: PublicDomain[]
|
||||
getEmailingDomains: EmailingDomain[]
|
||||
findManyMarketplaceApps: MarketplaceApp[]
|
||||
@@ -2728,9 +2734,9 @@ export interface Mutation {
|
||||
switchBillingPlan: BillingUpdate
|
||||
cancelSwitchBillingPlan: BillingUpdate
|
||||
cancelSwitchBillingInterval: BillingUpdate
|
||||
setResourceCreditSubscriptionPrice: BillingUpdate
|
||||
setMeteredSubscriptionPrice: BillingUpdate
|
||||
endSubscriptionTrialPeriod: BillingEndTrialPeriod
|
||||
cancelSwitchResourceCreditPrice: BillingUpdate
|
||||
cancelSwitchMeteredPrice: BillingUpdate
|
||||
deleteWorkspaceInvitation: Scalars['String']
|
||||
resendWorkspaceInvitation: SendInvitations
|
||||
sendInvitations: SendInvitations
|
||||
@@ -2772,7 +2778,7 @@ export interface Mutation {
|
||||
updateOneRole: Role
|
||||
deleteOneRole: Scalars['String']
|
||||
upsertObjectPermissions: ObjectPermission[]
|
||||
upsertPermissionFlags: RolePermissionFlag[]
|
||||
upsertPermissionFlags: PermissionFlag[]
|
||||
upsertFieldPermissions: FieldPermission[]
|
||||
upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult
|
||||
assignRoleToAgent: Scalars['Boolean']
|
||||
@@ -2862,6 +2868,8 @@ export interface Mutation {
|
||||
startChannelSync: ChannelSyncSuccess
|
||||
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess
|
||||
updateLabPublicFeatureFlag: FeatureFlag
|
||||
enablePostgresProxy: PostgresCredentials
|
||||
disablePostgresProxy: PostgresCredentials
|
||||
createPublicDomain: PublicDomain
|
||||
updatePublicDomain: PublicDomain
|
||||
deletePublicDomain: Scalars['Boolean']
|
||||
@@ -3072,7 +3080,7 @@ export interface FieldPermissionGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface RolePermissionFlagGenqlSelection{
|
||||
export interface PermissionFlagGenqlSelection{
|
||||
id?: boolean | number
|
||||
roleId?: boolean | number
|
||||
flag?: boolean | number
|
||||
@@ -3108,7 +3116,7 @@ export interface RoleGenqlSelection{
|
||||
canUpdateAllObjectRecords?: boolean | number
|
||||
canSoftDeleteAllObjectRecords?: boolean | number
|
||||
canDestroyAllObjectRecords?: boolean | number
|
||||
permissionFlags?: RolePermissionFlagGenqlSelection
|
||||
permissionFlags?: PermissionFlagGenqlSelection
|
||||
objectPermissions?: ObjectPermissionGenqlSelection
|
||||
fieldPermissions?: FieldPermissionGenqlSelection
|
||||
rowLevelPermissionPredicates?: RowLevelPermissionPredicateGenqlSelection
|
||||
@@ -3165,7 +3173,6 @@ export interface FrontComponentGenqlSelection{
|
||||
isHeadless?: boolean | number
|
||||
usesSdkClient?: boolean | number
|
||||
applicationTokenPair?: ApplicationTokenPairGenqlSelection
|
||||
applicationVariables?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -3452,7 +3459,6 @@ export interface ViewFilterGenqlSelection{
|
||||
viewFilterGroupId?: boolean | number
|
||||
positionInViewFilterGroup?: boolean | number
|
||||
subFieldName?: boolean | number
|
||||
relationTargetFieldMetadataId?: boolean | number
|
||||
viewId?: boolean | number
|
||||
workspaceId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
@@ -3480,7 +3486,6 @@ export interface ViewSortGenqlSelection{
|
||||
id?: boolean | number
|
||||
fieldMetadataId?: boolean | number
|
||||
direction?: boolean | number
|
||||
subFieldName?: boolean | number
|
||||
viewId?: boolean | number
|
||||
workspaceId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
@@ -4187,7 +4192,7 @@ export interface BillingEndTrialPeriodGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface BillingResourceCreditUsageGenqlSelection{
|
||||
export interface BillingMeteredProductUsageGenqlSelection{
|
||||
productKey?: boolean | number
|
||||
periodStart?: boolean | number
|
||||
periodEnd?: boolean | number
|
||||
@@ -5149,6 +5154,15 @@ export interface ImapSmtpCaldavConnectionSuccessGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface PostgresCredentialsGenqlSelection{
|
||||
id?: boolean | number
|
||||
user?: boolean | number
|
||||
password?: boolean | number
|
||||
workspaceId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ToolIndexEntryGenqlSelection{
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
@@ -5619,7 +5633,7 @@ export interface QueryGenqlSelection{
|
||||
enterpriseSubscriptionStatus?: EnterpriseSubscriptionStatusDTOGenqlSelection
|
||||
billingPortalSession?: (BillingSessionGenqlSelection & { __args?: {returnUrlPath?: (Scalars['String'] | null)} })
|
||||
listPlans?: BillingPlanGenqlSelection
|
||||
getResourceCreditUsage?: BillingResourceCreditUsageGenqlSelection
|
||||
getMeteredProductsUsage?: BillingMeteredProductUsageGenqlSelection
|
||||
findWorkspaceInvitations?: WorkspaceInvitationGenqlSelection
|
||||
getApprovedAccessDomains?: ApprovedAccessDomainGenqlSelection
|
||||
getPageLayoutTabs?: (PageLayoutTabGenqlSelection & { __args: {pageLayoutId: Scalars['String']} })
|
||||
@@ -5712,6 +5726,7 @@ export interface QueryGenqlSelection{
|
||||
getAutoCompleteAddress?: (AutocompleteResultGenqlSelection & { __args: {address: Scalars['String'], token: Scalars['String'], country?: (Scalars['String'] | null), isFieldCity?: (Scalars['Boolean'] | null)} })
|
||||
getAddressDetails?: (PlaceDetailsResultGenqlSelection & { __args: {placeId: Scalars['String'], token: Scalars['String']} })
|
||||
getUsageAnalytics?: (UsageAnalyticsGenqlSelection & { __args?: {input?: (UsageAnalyticsInput | null)} })
|
||||
getPostgresCredentials?: PostgresCredentialsGenqlSelection
|
||||
findManyPublicDomains?: PublicDomainGenqlSelection
|
||||
getEmailingDomains?: EmailingDomainGenqlSelection
|
||||
findManyMarketplaceApps?: MarketplaceAppGenqlSelection
|
||||
@@ -5803,9 +5818,9 @@ export interface MutationGenqlSelection{
|
||||
switchBillingPlan?: BillingUpdateGenqlSelection
|
||||
cancelSwitchBillingPlan?: BillingUpdateGenqlSelection
|
||||
cancelSwitchBillingInterval?: BillingUpdateGenqlSelection
|
||||
setResourceCreditSubscriptionPrice?: (BillingUpdateGenqlSelection & { __args: {priceId: Scalars['String']} })
|
||||
setMeteredSubscriptionPrice?: (BillingUpdateGenqlSelection & { __args: {priceId: Scalars['String']} })
|
||||
endSubscriptionTrialPeriod?: BillingEndTrialPeriodGenqlSelection
|
||||
cancelSwitchResourceCreditPrice?: BillingUpdateGenqlSelection
|
||||
cancelSwitchMeteredPrice?: BillingUpdateGenqlSelection
|
||||
deleteWorkspaceInvitation?: { __args: {appTokenId: Scalars['String']} }
|
||||
resendWorkspaceInvitation?: (SendInvitationsGenqlSelection & { __args: {appTokenId: Scalars['String']} })
|
||||
sendInvitations?: (SendInvitationsGenqlSelection & { __args: {emails: Scalars['String'][], roleId?: (Scalars['UUID'] | null)} })
|
||||
@@ -5847,7 +5862,7 @@ export interface MutationGenqlSelection{
|
||||
updateOneRole?: (RoleGenqlSelection & { __args: {updateRoleInput: UpdateRoleInput} })
|
||||
deleteOneRole?: { __args: {roleId: Scalars['UUID']} }
|
||||
upsertObjectPermissions?: (ObjectPermissionGenqlSelection & { __args: {upsertObjectPermissionsInput: UpsertObjectPermissionsInput} })
|
||||
upsertPermissionFlags?: (RolePermissionFlagGenqlSelection & { __args: {upsertPermissionFlagsInput: UpsertPermissionFlagsInput} })
|
||||
upsertPermissionFlags?: (PermissionFlagGenqlSelection & { __args: {upsertPermissionFlagsInput: UpsertPermissionFlagsInput} })
|
||||
upsertFieldPermissions?: (FieldPermissionGenqlSelection & { __args: {upsertFieldPermissionsInput: UpsertFieldPermissionsInput} })
|
||||
upsertRowLevelPermissionPredicates?: (UpsertRowLevelPermissionPredicatesResultGenqlSelection & { __args: {input: UpsertRowLevelPermissionPredicatesInput} })
|
||||
assignRoleToAgent?: { __args: {agentId: Scalars['UUID'], roleId: Scalars['UUID']} }
|
||||
@@ -5872,7 +5887,7 @@ export interface MutationGenqlSelection{
|
||||
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
|
||||
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createChatThread?: AgentChatThreadGenqlSelection
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileAttachments?: (FileAttachmentInput[] | null)} })
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileIds?: (Scalars['UUID'][] | null)} })
|
||||
stopAgentChatStream?: { __args: {threadId: Scalars['UUID']} }
|
||||
renameChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID'], title: Scalars['String']} })
|
||||
archiveChatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -5937,6 +5952,8 @@ export interface MutationGenqlSelection{
|
||||
startChannelSync?: (ChannelSyncSuccessGenqlSelection & { __args: {connectedAccountId: Scalars['UUID']} })
|
||||
saveImapSmtpCaldavAccount?: (ImapSmtpCaldavConnectionSuccessGenqlSelection & { __args: {accountOwnerId: Scalars['UUID'], handle: Scalars['String'], connectionParameters: EmailAccountConnectionParameters, id?: (Scalars['UUID'] | null)} })
|
||||
updateLabPublicFeatureFlag?: (FeatureFlagGenqlSelection & { __args: {input: UpdateLabPublicFeatureFlagInput} })
|
||||
enablePostgresProxy?: PostgresCredentialsGenqlSelection
|
||||
disablePostgresProxy?: PostgresCredentialsGenqlSelection
|
||||
createPublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId?: (Scalars['String'] | null)} })
|
||||
updatePublicDomain?: (PublicDomainGenqlSelection & { __args: {domain: Scalars['String'], applicationId?: (Scalars['String'] | null)} })
|
||||
deletePublicDomain?: { __args: {domain: Scalars['String']} }
|
||||
@@ -5975,7 +5992,7 @@ export interface CreateViewFilterGroupInput {id?: (Scalars['UUID'] | null),paren
|
||||
|
||||
export interface UpdateViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null),viewId?: (Scalars['UUID'] | null)}
|
||||
|
||||
export interface CreateViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null),viewId: Scalars['UUID']}
|
||||
export interface CreateViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),viewId: Scalars['UUID']}
|
||||
|
||||
export interface UpdateViewFilterInput {
|
||||
/** The id of the view filter to update */
|
||||
@@ -5983,7 +6000,7 @@ id: Scalars['UUID'],
|
||||
/** The view filter to update */
|
||||
update: UpdateViewFilterInputUpdates}
|
||||
|
||||
export interface UpdateViewFilterInputUpdates {fieldMetadataId?: (Scalars['UUID'] | null),operand?: (ViewFilterOperand | null),value?: (Scalars['JSON'] | null),viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null)}
|
||||
export interface UpdateViewFilterInputUpdates {fieldMetadataId?: (Scalars['UUID'] | null),operand?: (ViewFilterOperand | null),value?: (Scalars['JSON'] | null),viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
|
||||
|
||||
export interface DeleteViewFilterInput {
|
||||
/** The id of the view filter to delete. */
|
||||
@@ -6015,13 +6032,13 @@ viewFieldId?: (Scalars['UUID'] | null),
|
||||
/** The field metadata id. Used to create a new view field when viewFieldId is not provided. */
|
||||
fieldMetadataId?: (Scalars['UUID'] | null),isVisible: Scalars['Boolean'],position: Scalars['Float'],size?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null),relationTargetFieldMetadataId?: (Scalars['UUID'] | null)}
|
||||
export interface UpsertViewWidgetViewFilterInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],operand?: (ViewFilterOperand | null),value: Scalars['JSON'],viewFilterGroupId?: (Scalars['UUID'] | null),positionInViewFilterGroup?: (Scalars['Float'] | null),subFieldName?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewFilterGroupInput {id?: (Scalars['UUID'] | null),parentViewFilterGroupId?: (Scalars['UUID'] | null),logicalOperator?: (ViewFilterGroupLogicalOperator | null),positionInViewFilterGroup?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpsertViewWidgetViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null)}
|
||||
|
||||
export interface CreateViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null),subFieldName?: (Scalars['String'] | null),viewId: Scalars['UUID']}
|
||||
export interface CreateViewSortInput {id?: (Scalars['UUID'] | null),fieldMetadataId: Scalars['UUID'],direction?: (ViewSortDirection | null),viewId: Scalars['UUID']}
|
||||
|
||||
export interface UpdateViewSortInput {
|
||||
/** The id of the view sort to update */
|
||||
@@ -6029,7 +6046,7 @@ id: Scalars['UUID'],
|
||||
/** The view sort to update */
|
||||
update: UpdateViewSortInputUpdates}
|
||||
|
||||
export interface UpdateViewSortInputUpdates {direction?: (ViewSortDirection | null),subFieldName?: (Scalars['String'] | null)}
|
||||
export interface UpdateViewSortInputUpdates {direction?: (ViewSortDirection | null)}
|
||||
|
||||
export interface DeleteViewSortInput {
|
||||
/** The id of the view sort to delete. */
|
||||
@@ -6259,8 +6276,6 @@ update: UpdateWebhookInputUpdates}
|
||||
|
||||
export interface UpdateWebhookInputUpdates {targetUrl?: (Scalars['String'] | null),operations?: (Scalars['String'][] | null),description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
export interface FileAttachmentInput {id: Scalars['UUID'],filename: Scalars['String']}
|
||||
|
||||
export interface CreateSkillInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],label: Scalars['String'],icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content: Scalars['String']}
|
||||
|
||||
export interface UpdateSkillInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),content?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null)}
|
||||
@@ -6428,10 +6443,10 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const RolePermissionFlag_possibleTypes: string[] = ['RolePermissionFlag']
|
||||
export const isRolePermissionFlag = (obj?: { __typename?: any } | null): obj is RolePermissionFlag => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isRolePermissionFlag"')
|
||||
return RolePermissionFlag_possibleTypes.includes(obj.__typename)
|
||||
const PermissionFlag_possibleTypes: string[] = ['PermissionFlag']
|
||||
export const isPermissionFlag = (obj?: { __typename?: any } | null): obj is PermissionFlag => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPermissionFlag"')
|
||||
return PermissionFlag_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
@@ -7092,10 +7107,10 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const BillingResourceCreditUsage_possibleTypes: string[] = ['BillingResourceCreditUsage']
|
||||
export const isBillingResourceCreditUsage = (obj?: { __typename?: any } | null): obj is BillingResourceCreditUsage => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingResourceCreditUsage"')
|
||||
return BillingResourceCreditUsage_possibleTypes.includes(obj.__typename)
|
||||
const BillingMeteredProductUsage_possibleTypes: string[] = ['BillingMeteredProductUsage']
|
||||
export const isBillingMeteredProductUsage = (obj?: { __typename?: any } | null): obj is BillingMeteredProductUsage => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingMeteredProductUsage"')
|
||||
return BillingMeteredProductUsage_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
@@ -7956,6 +7971,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const PostgresCredentials_possibleTypes: string[] = ['PostgresCredentials']
|
||||
export const isPostgresCredentials = (obj?: { __typename?: any } | null): obj is PostgresCredentials => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPostgresCredentials"')
|
||||
return PostgresCredentials_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ToolIndexEntry_possibleTypes: string[] = ['ToolIndexEntry']
|
||||
export const isToolIndexEntry = (obj?: { __typename?: any } | null): obj is ToolIndexEntry => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isToolIndexEntry"')
|
||||
@@ -8682,7 +8705,8 @@ export const enumBillingUsageType = {
|
||||
|
||||
export const enumBillingProductKey = {
|
||||
BASE_PRODUCT: 'BASE_PRODUCT' as const,
|
||||
RESOURCE_CREDIT: 'RESOURCE_CREDIT' as const
|
||||
RESOURCE_CREDIT: 'RESOURCE_CREDIT' as const,
|
||||
WORKFLOW_NODE_EXECUTION: 'WORKFLOW_NODE_EXECUTION' as const
|
||||
}
|
||||
|
||||
export const enumSubscriptionInterval = {
|
||||
@@ -8735,11 +8759,13 @@ export const enumFeatureFlagKey = {
|
||||
IS_UNIQUE_INDEXES_ENABLED: 'IS_UNIQUE_INDEXES_ENABLED' as const,
|
||||
IS_JSON_FILTER_ENABLED: 'IS_JSON_FILTER_ENABLED' as const,
|
||||
IS_MARKETPLACE_SETTING_TAB_VISIBLE: 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' as const,
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED' as const,
|
||||
IS_PUBLIC_DOMAIN_ENABLED: 'IS_PUBLIC_DOMAIN_ENABLED' as const,
|
||||
IS_EMAILING_DOMAIN_ENABLED: 'IS_EMAILING_DOMAIN_ENABLED' as const,
|
||||
IS_EMAIL_GROUP_ENABLED: 'IS_EMAIL_GROUP_ENABLED' as const,
|
||||
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
|
||||
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const
|
||||
IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED: 'IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED' as const,
|
||||
IS_BILLING_V2_ENABLED: 'IS_BILLING_V2_ENABLED' as const
|
||||
}
|
||||
|
||||
export const enumIdentityProviderType = {
|
||||
@@ -8901,7 +8927,6 @@ export const enumAllMetadataName = {
|
||||
pageLayoutTab: 'pageLayoutTab' as const,
|
||||
commandMenuItem: 'commandMenuItem' as const,
|
||||
navigationMenuItem: 'navigationMenuItem' as const,
|
||||
rolePermissionFlag: 'rolePermissionFlag' as const,
|
||||
permissionFlag: 'permissionFlag' as const,
|
||||
objectPermission: 'objectPermission' as const,
|
||||
fieldPermission: 'fieldPermission' as const,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
FROM node:24.15.0-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS twenty-website-new-build
|
||||
FROM node:24-alpine AS twenty-website-new-build
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
@@ -20,7 +20,7 @@ COPY ./packages/twenty-shared /app/packages/twenty-shared
|
||||
COPY ./packages/twenty-website-new /app/packages/twenty-website-new
|
||||
RUN npx nx build twenty-website-new
|
||||
|
||||
FROM node:24.15.0-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS twenty-website-new
|
||||
FROM node:24-alpine AS twenty-website-new
|
||||
|
||||
WORKDIR /app/packages/twenty-website-new
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Dependency stages
|
||||
# ===========================================================================
|
||||
|
||||
FROM node:24.15.0-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS front-deps
|
||||
FROM node:24-alpine AS front-deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -20,7 +20,7 @@ COPY ./packages/twenty-client-sdk/package.json /app/packages/twenty-client-sdk/
|
||||
RUN yarn workspaces focus twenty twenty-front twenty-front-component-renderer twenty-ui twenty-shared twenty-sdk twenty-client-sdk && yarn cache clean && npx nx reset
|
||||
|
||||
|
||||
FROM node:24.15.0-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS server-deps
|
||||
FROM node:24-alpine AS server-deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -84,7 +84,7 @@ RUN if [ -d /app/packages/twenty-front/build ]; then \
|
||||
# docker build --target twenty-server -f packages/twenty-docker/twenty/Dockerfile .
|
||||
# ===========================================================================
|
||||
|
||||
FROM node:24.15.0-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS twenty-server
|
||||
FROM node:24-alpine AS twenty-server
|
||||
|
||||
RUN apk add --no-cache curl jq postgresql-client
|
||||
|
||||
@@ -187,7 +187,7 @@ RUN S6_ARCH=$(cat /tmp/s6arch) && \
|
||||
echo "$NOARCH_SUM s6-overlay-noarch.tar.xz" | sha256sum -c - && \
|
||||
echo "$ARCH_SUM s6-overlay-arch.tar.xz" | sha256sum -c -
|
||||
|
||||
FROM node:24.15.0-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS twenty-app-dev
|
||||
FROM node:24-alpine AS twenty-app-dev
|
||||
|
||||
# s6-overlay
|
||||
COPY --from=s6-fetch /tmp/s6-overlay-noarch.tar.xz /tmp/
|
||||
@@ -197,7 +197,7 @@ RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \
|
||||
&& rm /tmp/s6-overlay-*.tar.xz
|
||||
|
||||
RUN apk add --no-cache \
|
||||
postgresql18 postgresql18-contrib \
|
||||
postgresql16 postgresql16-contrib \
|
||||
redis \
|
||||
curl jq su-exec
|
||||
|
||||
|
||||
@@ -72,11 +72,11 @@ Your content here...
|
||||
- `package.json` - Package dependencies and scripts (`docs:generate`, `docs:generate-navigation-template`, …).
|
||||
- `project.json` - Nx workspace configuration
|
||||
|
||||
## 📦 Validation
|
||||
## 📦 Building
|
||||
|
||||
```bash
|
||||
# Validate the documentation build
|
||||
npx nx run twenty-docs:validate
|
||||
# Build the documentation
|
||||
npx nx run twenty-docs:build
|
||||
```
|
||||
|
||||
## 🔗 Links
|
||||
@@ -97,3 +97,4 @@ To contribute to the documentation:
|
||||
## 📄 License
|
||||
|
||||
This documentation is part of the Twenty project and is licensed under [AGPL-3.0](../../LICENSE).
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export default defineApplication({
|
||||
|
||||
Notes:
|
||||
- `universalIdentifier` fields are deterministic IDs you own. Generate them once and keep them stable across syncs.
|
||||
- `applicationVariables` become environment variables for your functions and front components. In logic functions (server-side), they are available as `process.env.VARIABLE_NAME`. In front components, use `getApplicationVariable('VARIABLE_NAME')` from `twenty-sdk/front-component`. Variables marked with `isSecret: true` are only injected into logic functions. Front components receive only non-secret variables.
|
||||
- `applicationVariables` become environment variables for your functions and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
- The default role is detected automatically from the role file marked with [`defineApplicationRole()`](/developers/extend/apps/config/roles) — you do not need to reference it from `defineApplication()`.
|
||||
- Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`.
|
||||
- Passing `defaultRoleUniversalIdentifier` explicitly is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Roles & Permissions
|
||||
description: Declare what objects and fields your app's logic functions and front components can read and write.
|
||||
icon: 'shield-halved'
|
||||
icon: "shield-halved"
|
||||
---
|
||||
|
||||
A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role marked with `defineApplicationRole()` (see [The default function role](#the-default-function-role) below).
|
||||
@@ -40,8 +40,7 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
@@ -81,7 +80,6 @@ export default defineApplicationRole({
|
||||
`defineApplicationRole()` is a thin wrapper around `defineRole()` that flags **the** role used as your application's default at install time. Validation is identical to `defineRole`, but the build pipeline auto-wires its `universalIdentifier` into the application manifest's `defaultRoleUniversalIdentifier` — so you do not need to reference it from [`defineApplication`](/developers/extend/apps/config/application) yourself.
|
||||
|
||||
Notes:
|
||||
|
||||
- Exactly **one** `defineApplicationRole(...)` is allowed per app — the manifest build will fail if it finds more than one.
|
||||
- Use `defineRole()` (not `defineApplicationRole()`) for any **additional** roles your app ships.
|
||||
- Setting `defaultRoleUniversalIdentifier` explicitly on `defineApplication()` is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
|
||||
|
||||
@@ -87,7 +87,7 @@ cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a few seconds the server reflects the change. You'll see a live status panel in your terminal.
|
||||
This watches `src/`, rebuilds on every change, and syncs the result to the server. Edit a file, save, and within a second the server reflects the change. You'll see a live status panel in your terminal.
|
||||
|
||||
For more detailed output (build logs, sync requests, error traces), add `--verbose`.
|
||||
|
||||
@@ -128,15 +128,11 @@ yarn twenty dev --once
|
||||
| `yarn twenty dev` | Watches and re-syncs on every change. Runs until you stop it. | Interactive local development. |
|
||||
| `yarn twenty dev --once` | Single build + sync, exits `0` on success, `1` on failure. | CI, pre-commit hooks, AI agents, scripted workflows. |
|
||||
|
||||
Both modes need an authenticated remote.
|
||||
Both modes need a server in development mode and an authenticated remote.
|
||||
|
||||
### Dev mode options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--once` | Build and sync once, then exit. |
|
||||
| `--debounceMs <ms>` | Set the file-change debounce delay in milliseconds (default: `2000`). |
|
||||
| `--verbose` / `--debug` | Show detailed build logs, sync requests, and error traces. |
|
||||
<Warning>
|
||||
Dev mode is only available on Twenty instances running in development (`NODE_ENV=development`). Production instances reject dev sync requests — use `yarn twenty deploy` to deploy to production servers. See [Publishing](/developers/extend/apps/operations/publishing).
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -239,38 +239,6 @@ Available hooks:
|
||||
| `useFrontComponentId()` | `string` | This component instance's ID |
|
||||
| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function |
|
||||
|
||||
## Application variables
|
||||
|
||||
Application variables defined in [`defineApplication()`](/developers/extend/apps/config/application) with `isSecret: false` are available inside front components via the `getApplicationVariable` utility:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Secret variables (`isSecret: true`) are **not** exposed to front components. They are only available in [logic functions](/developers/extend/apps/logic/logic-functions), which run server-side. This prevents sensitive values like API keys from being sent to the browser.
|
||||
</Warning>
|
||||
|
||||
The following system variables are always available via `process.env`:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `TWENTY_API_URL` | Base URL of the Twenty API |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | Short-lived token scoped to your app's role |
|
||||
|
||||
## Host communication API
|
||||
|
||||
Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`:
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineApplication({
|
||||
Notizen:
|
||||
|
||||
* `universalIdentifier`-Felder sind deterministische IDs, die Ihnen gehören. Erzeugen Sie sie einmal und halten Sie sie über Synchronisierungen hinweg stabil.
|
||||
* `applicationVariables` werden zu Umgebungsvariablen für Ihre Funktionen und Frontend-Komponenten. In Logikfunktionen (serverseitig) sind sie als `process.env.VARIABLE_NAME` verfügbar. In Frontend-Komponenten verwenden Sie `getApplicationVariable('VARIABLE_NAME')` aus `twenty-sdk/front-component`. Variablen, die mit `isSecret: true` gekennzeichnet sind, werden nur in Logikfunktionen injiziert. Frontend-Komponenten erhalten nur nicht-geheime Variablen.
|
||||
* `applicationVariables` werden zu Umgebungsvariablen für Ihre Funktionen und Frontend-Komponenten (z. B. ist `DEFAULT_RECIPIENT_NAME` als `process.env.DEFAULT_RECIPIENT_NAME` verfügbar).
|
||||
* Die Standardrolle wird automatisch aus der Rollen-Datei erkannt, die mit [`defineApplicationRole()`](/l/de/developers/extend/apps/config/roles) markiert ist – Sie müssen sie nicht aus `defineApplication()` referenzieren.
|
||||
* Pre- und Post-Installationsfunktionen werden während des Manifest-Builds automatisch erkannt — Sie müssen sie in `defineApplication()` nicht referenzieren.
|
||||
* Die explizite Übergabe von `defaultRoleUniversalIdentifier` wird für die Abwärtskompatibilität weiterhin unterstützt, ist jedoch zugunsten von `defineApplicationRole()` veraltet.
|
||||
|
||||
@@ -40,8 +40,7 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Dies überwacht `src/`, baut bei jeder Änderung neu und synchronisiert das Ergebnis mit dem Server. Bearbeiten Sie eine Datei, speichern Sie, und innerhalb weniger Sekunden spiegelt der Server die Änderung wider. Sie sehen eine Live-Statusanzeige in Ihrem Terminal.
|
||||
Dies überwacht `src/`, baut bei jeder Änderung neu und synchronisiert das Ergebnis mit dem Server. Bearbeiten Sie eine Datei, speichern Sie, und innerhalb einer Sekunde spiegelt der Server die Änderung wider. Sie sehen eine Live-Statusanzeige in Ihrem Terminal.
|
||||
|
||||
Für ausführlichere Ausgaben (Build-Protokolle, Sync-Anfragen, Fehlerspuren) fügen Sie `--verbose` hinzu.
|
||||
|
||||
@@ -128,15 +128,11 @@ yarn twenty dev --once
|
||||
| `yarn twenty dev` | Überwacht und synchronisiert bei jeder Änderung erneut. Läuft, bis Sie es stoppen. | Interaktive lokale Entwicklung. |
|
||||
| `yarn twenty dev --once` | Einmaliger Build + Sync, beendet sich mit `0` bei Erfolg, mit `1` bei Fehler. | CI, Pre-Commit-Hooks, KI-Agenten, skriptgesteuerte Workflows. |
|
||||
|
||||
Beide Modi benötigen ein authentifiziertes Remote-Repository.
|
||||
Beide Modi benötigen einen Server im Entwicklungsmodus und eine authentifizierte Remote-Verbindung.
|
||||
|
||||
### Dev-Modus-Optionen
|
||||
|
||||
| Flag | Beschreibung |
|
||||
| ------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `--once` | Einmal erstellen und synchronisieren, dann beenden. |
|
||||
| `--debounceMs \<ms>` | Legt die Entprellzeit für Dateiänderungen in Millisekunden fest (Standard: `2000`). |
|
||||
| `--verbose` / `--debug` | Zeigt ausführliche Build-Protokolle, Sync-Anfragen und Fehler-Traces an. |
|
||||
<Warning>
|
||||
Der Dev-Modus ist nur auf Twenty-Instanzen verfügbar, die im Entwicklungsmodus laufen (`NODE_ENV=development`). Produktionsinstanzen lehnen Dev-Sync-Anfragen ab — verwenden Sie `yarn twenty deploy`, um auf Produktionsserver bereitzustellen. Siehe [Apps veröffentlichen](/l/de/developers/extend/apps/operations/publishing).
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -239,38 +239,6 @@ Verfügbare Hooks:
|
||||
| `useFrontComponentId()` | `string` | Die ID dieser Komponenteninstanz |
|
||||
| `useFrontComponentExecutionContext(selector)` | variiert | Zugriff auf den vollständigen Ausführungskontext mit einer Selektorfunktion |
|
||||
|
||||
## Anwendungsvariablen
|
||||
|
||||
In [`defineApplication()`](/l/de/developers/extend/apps/config/application) mit `isSecret: false` definierte Anwendungsvariablen sind in Front-Komponenten über das Hilfsprogramm `getApplicationVariable` verfügbar:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Geheime Variablen (`isSecret: true`) werden **nicht** in Front-Komponenten offengelegt. Sie sind nur in [Logikfunktionen](/l/de/developers/extend/apps/logic/logic-functions) verfügbar, die serverseitig ausgeführt werden. Dadurch wird verhindert, dass sensible Werte wie API-Schlüssel an den Browser gesendet werden.
|
||||
</Warning>
|
||||
|
||||
Die folgenden Systemvariablen sind immer über `process.env` verfügbar:
|
||||
|
||||
| Variable | Beschreibung |
|
||||
| ------------------------- | ------------------------------------------------------------- |
|
||||
| `TWENTY_API_URL` | Basis-URL der Twenty API |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | Kurzlebiges Token mit dem Geltungsbereich der Rolle Ihrer App |
|
||||
|
||||
## Host-Kommunikations-API
|
||||
|
||||
Front-Komponenten können Navigation, Modals und Benachrichtigungen mittels Funktionen aus `twenty-sdk` auslösen:
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineApplication({
|
||||
Notas:
|
||||
|
||||
* Os campos `universalIdentifier` são IDs determinísticos que você controla. Gere-os uma vez e mantenha-os estáveis entre sincronizações.
|
||||
* `applicationVariables` tornam-se variáveis de ambiente para suas funções e componentes de front-end. Em funções lógicas (no lado do servidor), elas ficam disponíveis como `process.env.VARIABLE_NAME`. Em componentes de front-end, use `getApplicationVariable('VARIABLE_NAME')` de `twenty-sdk/front-component`. Variáveis marcadas com `isSecret: true` são injetadas apenas em funções lógicas. Componentes de front-end recebem apenas variáveis não secretas.
|
||||
* `applicationVariables` tornam-se variáveis de ambiente para suas funções e componentes de front-end (por exemplo, `DEFAULT_RECIPIENT_NAME` fica disponível como `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* O papel padrão é detectado automaticamente a partir do arquivo de definição de papel marcado com [`defineApplicationRole()`](/l/pt/developers/extend/apps/config/roles) — você não precisa referenciá-lo em `defineApplication()`.
|
||||
* As funções de pré-instalação e pós-instalação são detectadas automaticamente durante a construção do manifesto — você não precisa referenciá-las em `defineApplication()`.
|
||||
* Passar `defaultRoleUniversalIdentifier` explicitamente ainda é compatível para retrocompatibilidade, mas foi preterido em favor de `defineApplicationRole()`.
|
||||
|
||||
@@ -40,8 +40,7 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Isso monitora `src/`, recompila a cada alteração e sincroniza o resultado com o servidor. Edite um arquivo, salve e, em alguns segundos, o servidor refletirá a alteração. Você verá um painel de status em tempo real no seu terminal.
|
||||
Isso monitora `src/`, recompila a cada alteração e sincroniza o resultado com o servidor. Edite um arquivo, salve e, em um segundo, o servidor refletirá a alteração. Você verá um painel de status em tempo real no seu terminal.
|
||||
|
||||
Para uma saída mais detalhada (logs de build, solicitações de sincronização, rastros de erro), adicione `--verbose`.
|
||||
|
||||
@@ -128,15 +128,11 @@ yarn twenty dev --once
|
||||
| `yarn twenty dev` | Monitora e ressincroniza a cada alteração. Fica em execução até você interrompê-lo. | Desenvolvimento local interativo. |
|
||||
| `yarn twenty dev --once` | Executa uma única compilação + sincronização e, em seguida, encerra com o código `0` em caso de sucesso ou `1` em caso de falha. | Scripts, CI, hooks de pre-commit, agentes de IA e fluxos de trabalho com script. |
|
||||
|
||||
Ambos os modos precisam de um remoto autenticado.
|
||||
Ambos os modos precisam de um servidor em modo de desenvolvimento e de um remoto autenticado.
|
||||
|
||||
### Opções do modo de desenvolvimento
|
||||
|
||||
| Opção | Descrição |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `--once` | Compila e sincroniza uma vez e, em seguida, sai. |
|
||||
| `--debounceMs \<ms>` | Define o atraso de debounce para alterações de arquivo em milissegundos (padrão: `2000`). |
|
||||
| `--verbose` / `--debug` | Mostra registros detalhados de compilação, solicitações de sincronização e rastreamentos de erro. |
|
||||
<Warning>
|
||||
O modo de desenvolvimento só está disponível em instâncias do Twenty em modo de desenvolvimento (`NODE_ENV=development`). Instâncias de produção rejeitam solicitações de sincronização de desenvolvimento — use `yarn twenty deploy` para implantar em servidores de produção. Veja [Publicação](/l/pt/developers/extend/apps/operations/publishing).
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -239,38 +239,6 @@ Hooks disponíveis:
|
||||
| `useFrontComponentId()` | `string` | O ID desta instância do componente |
|
||||
| `useFrontComponentExecutionContext(selector)` | varia | Acesse o contexto de execução completo com uma função seletora |
|
||||
|
||||
## Variáveis de aplicação
|
||||
|
||||
Variáveis de aplicação definidas em [`defineApplication()`](/l/pt/developers/extend/apps/config/application) com `isSecret: false` estão disponíveis nos componentes de front por meio do utilitário `getApplicationVariable`:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Variáveis secretas (`isSecret: true`) **não** são expostas aos componentes de front. Elas estão disponíveis apenas em [funções de lógica](/l/pt/developers/extend/apps/logic/logic-functions), que são executadas no lado do servidor. Isso impede que valores sigilosos, como chaves de API, sejam enviados para o navegador.
|
||||
</Warning>
|
||||
|
||||
As seguintes variáveis de sistema estão sempre disponíveis via `process.env`:
|
||||
|
||||
| Variável | Descrição |
|
||||
| ------------------------- | ------------------------------------------------------------- |
|
||||
| `TWENTY_API_URL` | URL base da API da Twenty |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | Token de curta duração limitado ao escopo do papel do seu app |
|
||||
|
||||
## API de comunicação do host
|
||||
|
||||
Componentes de front-end podem acionar navegação, modais e notificações usando funções de `twenty-sdk`:
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineApplication({
|
||||
Notițe:
|
||||
|
||||
* Câmpurile `universalIdentifier` sunt ID-uri deterministe pe care le dețineți. Generați-le o singură dată și mențineți-le stabile între sincronizări.
|
||||
* `applicationVariables` devin variabile de mediu pentru funcțiile și componentele front-end. În funcțiile de logică (server-side), acestea sunt disponibile ca `process.env.VARIABLE_NAME`. În componentele front-end, folosește `getApplicationVariable('VARIABLE_NAME')` din `twenty-sdk/front-component`. Variabilele marcate cu `isSecret: true` sunt injectate doar în funcțiile de logică. Componentele front-end primesc doar variabile non-secrete.
|
||||
* `applicationVariables` devin variabile de mediu pentru funcțiile și componentele front-end (de exemplu, `DEFAULT_RECIPIENT_NAME` este disponibil ca `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* Rolul implicit este detectat automat din fișierul de rol marcat cu [`defineApplicationRole()`](/l/ro/developers/extend/apps/config/roles) — nu este necesar să faci referire la el în `defineApplication()`.
|
||||
* Funcțiile de pre-instalare și post-instalare sunt detectate automat în timpul construirii manifestului — nu trebuie să le referiți în `defineApplication()`.
|
||||
* Transmiterea explicită a `defaultRoleUniversalIdentifier` este în continuare acceptată pentru compatibilitate retroactivă, dar este considerată învechită în favoarea `defineApplicationRole()`.
|
||||
|
||||
@@ -40,8 +40,7 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Aceasta monitorizează `src/`, reconstruiește la fiecare modificare și sincronizează rezultatul pe server. Editați un fișier, salvați, iar în câteva secunde serverul reflectă modificarea. Veți vedea în terminal un panou de stare în timp real.
|
||||
Aceasta monitorizează `src/`, reconstruiește la fiecare modificare și sincronizează rezultatul pe server. Editați un fișier, salvați, iar în decurs de o secundă serverul reflectă modificarea. Veți vedea în terminal un panou de stare în timp real.
|
||||
|
||||
Pentru o ieșire mai detaliată (jurnale de build, cereri de sincronizare, urme ale erorilor), adăugați `--verbose`.
|
||||
|
||||
@@ -128,15 +128,11 @@ yarn twenty dev --once
|
||||
| `yarn twenty dev` | Monitorizează și resincronizează la fiecare modificare. Rulează până când îl opriți. | Dezvoltare locală interactivă. |
|
||||
| `yarn twenty dev --once` | Un singur build + sync, iese cu `0` la succes, `1` la eșec. | CI, hook-uri pre-commit, agenți AI, fluxuri de lucru scriptate. |
|
||||
|
||||
Ambele moduri necesită o conexiune la distanță autentificată.
|
||||
Ambele moduri necesită un server în modul de dezvoltare și un remote autentificat.
|
||||
|
||||
### Opțiuni pentru modul de dezvoltare
|
||||
|
||||
| Opțiune | Descriere |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `--once` | Construiește și sincronizează o singură dată, apoi iese. |
|
||||
| `--debounceMs \<ms>` | Setează întârzierea de debounce pentru modificarea fișierului în milisecunde (implicit: `2000`). |
|
||||
| `--verbose` / `--debug` | Afișează jurnale detaliate de construire, cereri de sincronizare și urme ale erorilor. |
|
||||
<Warning>
|
||||
Modul de dezvoltare este disponibil doar pe instanțele Twenty care rulează în modul development (`NODE_ENV=development`). Instanțele de producție resping cererile de sincronizare din modul de dezvoltare — folosiți `yarn twenty deploy` pentru a implementa pe serverele de producție. Consultați [Publicarea](/l/ro/developers/extend/apps/operations/publishing).
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -239,38 +239,6 @@ Hook-uri disponibile:
|
||||
| `useFrontComponentId()` | `string` | ID-ul acestei instanțe de componentă |
|
||||
| `useFrontComponentExecutionContext(selector)` | variază | Accesați întregul context de execuție cu o funcție selector |
|
||||
|
||||
## Variabile de aplicație
|
||||
|
||||
Variabilele de aplicație definite în [`defineApplication()`](/l/ro/developers/extend/apps/config/application) cu `isSecret: false` sunt disponibile în componentele de interfață prin utilitarul `getApplicationVariable`:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Variabilele secrete (`isSecret: true`) **nu** sunt expuse componentelor de interfață. Acestea sunt disponibile doar în [funcțiile logice](/l/ro/developers/extend/apps/logic/logic-functions), care rulează pe server. Acest lucru împiedică trimiterea către browser a valorilor sensibile, cum ar fi cheile API.
|
||||
</Warning>
|
||||
|
||||
Următoarele variabile de sistem sunt întotdeauna disponibile prin `process.env`:
|
||||
|
||||
| Variabilă | Descriere |
|
||||
| ------------------------- | -------------------------------------------------------- |
|
||||
| `TWENTY_API_URL` | URL de bază al API-ului Twenty |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | Token cu durată scurtă, limitat la rolul aplicației dvs. |
|
||||
|
||||
## API-ul de comunicare cu gazda
|
||||
|
||||
Componentele front-end pot declanșa navigare, ferestre modale și notificări folosind funcții din `twenty-sdk`:
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineApplication({
|
||||
Заметки:
|
||||
|
||||
* Поля `universalIdentifier` — это детерминированные идентификаторы, которые принадлежат вам. Сгенерируйте их один раз и сохраняйте неизменными между синхронизациями.
|
||||
* `applicationVariables` становятся переменными окружения для ваших функций и фронтенд-компонентов. В логических функциях (на стороне сервера) они доступны как `process.env.VARIABLE_NAME`. Во фронтенд-компонентах используйте `getApplicationVariable('VARIABLE_NAME')` из `twenty-sdk/front-component`. Переменные, помеченные как `isSecret: true`, внедряются только в логические функции. Фронтенд-компоненты получают только несекретные переменные.
|
||||
* `applicationVariables` становятся переменными окружения для ваших функций и фронтенд-компонентов (например, `DEFAULT_RECIPIENT_NAME` доступна как `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* Роль по умолчанию автоматически определяется из файла роли, помеченного с помощью [`defineApplicationRole()`](/l/ru/developers/extend/apps/config/roles) — вам не нужно ссылаться на неё из `defineApplication()`.
|
||||
* Предустановочные и постустановочные функции обнаруживаются автоматически во время сборки манифеста — вам не нужно указывать их в `defineApplication()`.
|
||||
* Явная передача `defaultRoleUniversalIdentifier` по-прежнему поддерживается для обратной совместимости, но считается устаревшей и вместо неё рекомендуется использовать `defineApplicationRole()`.
|
||||
|
||||
@@ -40,8 +40,7 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Эта команда отслеживает `src/`, пересобирает при каждом изменении и синхронизирует результат с сервером. Отредактируйте файл, сохраните — и через несколько секунд сервер отразит изменения. В терминале появится панель текущего статуса.
|
||||
Эта команда отслеживает `src/`, пересобирает при каждом изменении и синхронизирует результат с сервером. Отредактируйте файл, сохраните — и через секунду сервер отразит изменения. В терминале появится панель текущего статуса.
|
||||
|
||||
Для более подробного вывода (журналы сборки, запросы синхронизации, трассировки ошибок) добавьте `--verbose`.
|
||||
|
||||
@@ -128,15 +128,11 @@ yarn twenty dev --once
|
||||
| `yarn twenty dev` | Отслеживает и повторно синхронизирует при каждом изменении. Продолжает работать, пока вы его не остановите. | Интерактивная локальная разработка. |
|
||||
| `yarn twenty dev --once` | Одна сборка и синхронизация, завершает работу с кодом `0` при успехе и `1` при ошибке. | CI, хуки pre-commit, AI-агенты, скриптовые рабочие процессы. |
|
||||
|
||||
Оба режима требуют аутентифицированного удалённого репозитория.
|
||||
Оба режима требуют сервер в режиме разработки и аутентифицированный удалённый сервер.
|
||||
|
||||
### Параметры режима разработки
|
||||
|
||||
| Флаг | Описание |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `--once` | Выполнить сборку и синхронизацию один раз, затем завершить работу. |
|
||||
| `--debounceMs \<ms>` | Установить задержку дебаунса изменений файлов в миллисекундах (по умолчанию: `2000`). |
|
||||
| `--verbose` / `--debug` | Показывать подробные журналы сборки, запросы синхронизации и трассировки ошибок. |
|
||||
<Warning>
|
||||
Режим разработки доступен только на экземплярах Twenty, запущенных в режиме разработки (`NODE_ENV=development`). Экземпляры в продакшене отклоняют запросы синхронизации из режима разработки — используйте `yarn twenty deploy` для развёртывания на производственные серверы. См. [Публикация](/l/ru/developers/extend/apps/operations/publishing).
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -239,38 +239,6 @@ export default defineFrontComponent({
|
||||
| `useFrontComponentId()` | `string` | ID этого экземпляра компонента |
|
||||
| `useFrontComponentExecutionContext(selector)` | различается | Доступ к полному контексту выполнения с помощью функции-селектора |
|
||||
|
||||
## Переменные приложения
|
||||
|
||||
Переменные приложения, определенные в [`defineApplication()`](/l/ru/developers/extend/apps/config/application) с `isSecret: false`, доступны внутри фронтенд-компонентов через утилиту `getApplicationVariable`:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Секретные переменные (`isSecret: true`) **не** доступны фронтенд-компонентам. Они доступны только в [логических функциях](/l/ru/developers/extend/apps/logic/logic-functions), которые выполняются на стороне сервера. Это предотвращает отправку в браузер конфиденциальных значений, таких как ключи API.
|
||||
</Warning>
|
||||
|
||||
Следующие системные переменные всегда доступны через `process.env`:
|
||||
|
||||
| Переменная | Описание |
|
||||
| ------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `TWENTY_API_URL` | Базовый URL API Twenty |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | Краткоживущий токен с областью действия, ограниченной ролью вашего приложения |
|
||||
|
||||
## API взаимодействия с хостом
|
||||
|
||||
Компоненты фронтенда могут вызывать навигацию, модальные окна и уведомления с помощью функций из `twenty-sdk`:
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineApplication({
|
||||
Notlar:
|
||||
|
||||
* `universalIdentifier` alanları, size ait deterministik kimliklerdir. Bunları bir kez oluşturun ve senkronizasyonlar boyunca kararlı tutun.
|
||||
* `applicationVariables`, fonksiyonlarınız ve ön bileşenleriniz için ortam değişkenlerine dönüşür. Mantık fonksiyonlarında (sunucu tarafında), `process.env.VARIABLE_NAME` olarak kullanılabilirler. Ön bileşenlerde, `twenty-sdk/front-component` içindeki `getApplicationVariable('VARIABLE_NAME')` fonksiyonunu kullanın. `isSecret: true` ile işaretlenen değişkenler yalnızca mantık fonksiyonlarına enjekte edilir. Ön bileşenler yalnızca gizli olmayan değişkenleri alır.
|
||||
* `applicationVariables`, fonksiyonlarınız ve ön uç bileşenleriniz için ortam değişkenlerine dönüşür (örn. `DEFAULT_RECIPIENT_NAME`, `process.env.DEFAULT_RECIPIENT_NAME` olarak kullanılabilir).
|
||||
* Varsayılan rol, [`defineApplicationRole()`](/l/tr/developers/extend/apps/config/roles) ile işaretlenmiş rol dosyasından otomatik olarak algılanır — onu `defineApplication()` içinden belirtmenize gerek yoktur.
|
||||
* Kurulum öncesi ve kurulum sonrası fonksiyonlar manifest derlemesi sırasında otomatik olarak algılanır — bunlara `defineApplication()` içinde referans vermeniz gerekmez.
|
||||
* Geriye dönük uyumluluk için `defaultRoleUniversalIdentifier` değerini açıkça geçmek hâlâ desteklenmektedir, ancak `defineApplicationRole()` lehine kullanımdan kaldırılmıştır.
|
||||
|
||||
@@ -40,8 +40,7 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
Bu, `src/` dizinini izler, her değişiklikte yeniden oluşturur ve sonucu sunucuya eşitler. Bir dosyayı düzenleyin, kaydedin ve birkaç saniye içinde sunucu değişikliği yansıtır. Terminalinizde canlı bir durum paneli göreceksiniz.
|
||||
Bu, `src/` dizinini izler, her değişiklikte yeniden oluşturur ve sonucu sunucuya eşitler. Bir dosyayı düzenleyin, kaydedin; bir saniye içinde sunucu değişikliği yansıtır. Terminalinizde canlı bir durum paneli göreceksiniz.
|
||||
|
||||
Daha ayrıntılı çıktı (derleme günlükleri, eşitleme istekleri, hata izleri) için `--verbose` ekleyin.
|
||||
|
||||
@@ -128,15 +128,11 @@ yarn twenty dev --once
|
||||
| `yarn twenty dev` | Her değişiklikte izler ve yeniden eşitler. Siz durdurana kadar çalışır. | Etkileşimli yerel geliştirme. |
|
||||
| `yarn twenty dev --once` | Tek derleme + eşitleme; başarıda `0`, başarısızlıkta `1` ile çıkar. | CI, pre-commit kancaları, AI ajanları, betiklenmiş iş akışları. |
|
||||
|
||||
Her iki kipin de kimliği doğrulanmış bir uzak sunucuya ihtiyaç duyar.
|
||||
Her iki mod da geliştirme modunda bir sunucuya ve kimliği doğrulanmış bir uzak uca ihtiyaç duyar.
|
||||
|
||||
### Geliştirme kipi seçenekleri
|
||||
|
||||
| Bayrak | Açıklama |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `--once` | Bir kez derleyip eşitleyin, ardından çıkın. |
|
||||
| `--debounceMs \<ms>` | Dosya değişikliği geciktirme süresini milisaniye cinsinden ayarlayın (varsayılan: `2000`). |
|
||||
| `--verbose` / `--debug` | Ayrıntılı derleme günlüklerini, eşitleme isteklerini ve hata izlerini gösterin. |
|
||||
<Warning>
|
||||
Geliştirme modu yalnızca geliştirme ortamında (`NODE_ENV=development`) çalışan Twenty örneklerinde kullanılabilir. Üretim örnekleri, geliştirme eşitleme isteklerini reddeder — üretim sunucularına dağıtmak için `yarn twenty deploy` kullanın. Bkz. [Yayımlama](/l/tr/developers/extend/apps/operations/publishing).
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -239,38 +239,6 @@ Kullanılabilir hook'lar:
|
||||
| `useFrontComponentId()` | `string` | Bu bileşen örneğinin ID'si |
|
||||
| `useFrontComponentExecutionContext(selector)` | değişir | Bir seçici işlevle tam yürütme bağlamına erişin |
|
||||
|
||||
## Uygulama değişkenleri
|
||||
|
||||
`isSecret: false` ile [`defineApplication()`](/l/tr/developers/extend/apps/config/application) içinde tanımlanan uygulama değişkenleri, `getApplicationVariable` yardımcı işlevi aracılığıyla ön uç bileşenleri içinde kullanılabilir:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Gizli değişkenler (`isSecret: true`) ön uç bileşenlere açıklanmaz. Bunlar yalnızca sunucu tarafında çalışan [mantık işlevlerinde](/l/tr/developers/extend/apps/logic/logic-functions) kullanılabilir. Bu, API anahtarları gibi hassas değerlerin tarayıcıya gönderilmesini engeller.
|
||||
</Warning>
|
||||
|
||||
Aşağıdaki sistem değişkenleri her zaman `process.env` aracılığıyla kullanılabilir:
|
||||
|
||||
| Değişken | Açıklama |
|
||||
| ------------------------- | --------------------------------------------------------- |
|
||||
| `TWENTY_API_URL` | Twenty API'nin temel URL'si |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | Uygulamanızın rolüyle sınırlanan kısa ömürlü bir belirteç |
|
||||
|
||||
## Host iletişim API'si
|
||||
|
||||
Ön uç bileşenleri, `twenty-sdk`'deki işlevleri kullanarak gezinmeyi, modalları ve bildirimleri tetikleyebilir:
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineApplication({
|
||||
备注:
|
||||
|
||||
* `universalIdentifier` 字段是你拥有的确定性 ID。 只需生成一次,并在多次同步过程中保持稳定不变。
|
||||
* `applicationVariables` 会变成你的函数和前端组件可用的环境变量。 在逻辑函数(服务端)中,可以通过 `process.env.VARIABLE_NAME` 使用它们。 在前端组件中,使用 `twenty-sdk/front-component` 中的 `getApplicationVariable('VARIABLE_NAME')`。 标记为 `isSecret: true` 的变量只会注入到逻辑函数中。 前端组件只会接收非机密变量。
|
||||
* `applicationVariables` 会变成你的函数和前端组件可用的环境变量(例如,`DEFAULT_RECIPIENT_NAME` 可作为 `process.env.DEFAULT_RECIPIENT_NAME` 使用)。
|
||||
* 默认角色会根据使用 [`defineApplicationRole()`](/l/zh/developers/extend/apps/config/roles) 标记的角色文件自动检测——你不需要在 `defineApplication()` 中引用它。
|
||||
* 在构建清单时会自动检测安装前/安装后函数——无需在 `defineApplication()` 中引用它们。
|
||||
* 显式传递 `defaultRoleUniversalIdentifier` 仍然受支持以保持向后兼容性,但已弃用,推荐改用 `defineApplicationRole()`。
|
||||
|
||||
@@ -40,8 +40,7 @@ export default defineRole({
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
||||
.universalIdentifier,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name.universalIdentifier,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ cd my-twenty-app
|
||||
yarn twenty dev
|
||||
```
|
||||
|
||||
它会监视 `src/`,在每次更改时重新构建,并将结果同步到服务器。 编辑文件、保存,服务器会在几秒内反映出更改。 你会在终端中看到一个实时状态面板。
|
||||
它会监视 `src/`,在每次更改时重新构建,并将结果同步到服务器。 编辑文件、保存,服务器会在一秒内反映出更改。 你会在终端中看到一个实时状态面板。
|
||||
|
||||
如需更详细的输出(构建日志、同步请求、错误跟踪),请添加 `--verbose`。
|
||||
|
||||
@@ -128,15 +128,11 @@ yarn twenty dev --once
|
||||
| `yarn twenty dev` | 监视并在每次更改时重新同步。 持续运行,直到你将其停止。 | 交互式本地开发。 |
|
||||
| `yarn twenty dev --once` | 单次构建与同步,成功时以 `0` 退出,失败时以 `1` 退出。 | CI、pre-commit 钩子、AI 智能体、脚本化工作流。 |
|
||||
|
||||
两种模式都需要经过身份验证的远程仓库。
|
||||
两种模式都需要处于开发模式的服务器和已认证的远程。
|
||||
|
||||
### 开发模式选项
|
||||
|
||||
| 标志 | 描述 |
|
||||
| ------------------------------------- | ------------------------------ |
|
||||
| `--once` | 仅构建并同步一次,然后退出。 |
|
||||
| `--debounceMs \<ms>` | 以毫秒为单位设置文件更改的防抖延迟(默认值:`2000`)。 |
|
||||
| `--verbose` / `--debug` | 显示详细的构建日志、同步请求和错误跟踪。 |
|
||||
<Warning>
|
||||
开发模式仅适用于以开发模式运行的 Twenty 实例(`NODE_ENV=development`)。 生产实例会拒绝开发同步请求——请使用 `yarn twenty deploy` 部署到生产服务器。 参见[发布](/l/zh/developers/extend/apps/operations/publishing)。
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -239,38 +239,6 @@ export default defineFrontComponent({
|
||||
| `useFrontComponentId()` | `string` | 此组件实例的 ID |
|
||||
| `useFrontComponentExecutionContext(selector)` | 因情况而异 | 使用选择器函数访问完整的执行上下文 |
|
||||
|
||||
## 应用程序变量
|
||||
|
||||
在 [`defineApplication()`](/l/zh/developers/extend/apps/config/application) 中定义、且 `isSecret: false` 的应用程序变量,可以通过 `getApplicationVariable` 实用工具在前端组件中使用:
|
||||
|
||||
```tsx src/front-components/greeting.tsx
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import { getApplicationVariable } from 'twenty-sdk/front-component';
|
||||
|
||||
const Greeting = () => {
|
||||
const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
|
||||
|
||||
return <p>Hello, {recipientName}!</p>;
|
||||
};
|
||||
|
||||
export default defineFrontComponent({
|
||||
universalIdentifier: '...',
|
||||
name: 'greeting',
|
||||
component: Greeting,
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
机密变量(`isSecret: true`)**不会**暴露给前端组件。 它们仅在服务器端运行的 [逻辑函数](/l/zh/developers/extend/apps/logic/logic-functions) 中可用。 这可以防止诸如 API 密钥之类的敏感值被发送到浏览器。
|
||||
</Warning>
|
||||
|
||||
以下系统变量始终可以通过 `process.env` 获取:
|
||||
|
||||
| 变量 | 描述 |
|
||||
| ------------------------- | ------------------ |
|
||||
| `TWENTY_API_URL` | Twenty API 的基础 URL |
|
||||
| `TWENTY_APP_ACCESS_TOKEN` | 限定在你的应用角色范围内的短期令牌 |
|
||||
|
||||
## 宿主通信 API
|
||||
|
||||
前端组件可以使用来自 `twenty-sdk` 的函数触发导航、模态框和通知:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "mintlify dev",
|
||||
"validate": "mintlify validate",
|
||||
"build": "mintlify build",
|
||||
"fix-links": "bash scripts/fix-translated-links.sh",
|
||||
"lint": "npx oxlint -c .oxlintrc.json ."
|
||||
},
|
||||
|
||||
@@ -12,11 +12,12 @@
|
||||
"command": "mintlify dev"
|
||||
}
|
||||
},
|
||||
"validate": {
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"outputs": ["{projectRoot}/.mintlify"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "mintlify validate"
|
||||
"command": "mintlify build"
|
||||
}
|
||||
},
|
||||
"lint": {},
|
||||
|
||||
-4
@@ -23,7 +23,6 @@ type FrontComponentContentProps = {
|
||||
applicationAccessToken?: string;
|
||||
apiUrl?: string;
|
||||
sdkClientUrls?: SdkClientUrls;
|
||||
applicationVariables?: Record<string, string>;
|
||||
executionContext: FrontComponentExecutionContext;
|
||||
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
|
||||
onError: (error?: Error) => void;
|
||||
@@ -35,7 +34,6 @@ export const FrontComponentRenderer = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
executionContext,
|
||||
frontComponentHostCommunicationApi,
|
||||
onError,
|
||||
@@ -57,7 +55,6 @@ export const FrontComponentRenderer = ({
|
||||
applicationAccessToken={applicationAccessToken}
|
||||
apiUrl={apiUrl}
|
||||
sdkClientUrls={sdkClientUrls}
|
||||
applicationVariables={applicationVariables}
|
||||
frontComponentId={executionContext.frontComponentId}
|
||||
setReceiver={setReceiver}
|
||||
setThread={setThread}
|
||||
@@ -72,7 +69,6 @@ export const FrontComponentRenderer = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
executionContext.frontComponentId,
|
||||
]);
|
||||
|
||||
|
||||
-4
@@ -36,7 +36,6 @@ type FrontComponentWorkerEffectProps = {
|
||||
applicationAccessToken?: string;
|
||||
apiUrl?: string;
|
||||
sdkClientUrls?: SdkClientUrls;
|
||||
applicationVariables?: Record<string, string>;
|
||||
frontComponentId: string;
|
||||
setReceiver: React.Dispatch<React.SetStateAction<RemoteReceiver | null>>;
|
||||
setThread: React.Dispatch<
|
||||
@@ -53,7 +52,6 @@ export const FrontComponentWorkerEffect = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
frontComponentId,
|
||||
setReceiver,
|
||||
setThread,
|
||||
@@ -123,7 +121,6 @@ export const FrontComponentWorkerEffect = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
setError(error);
|
||||
@@ -146,7 +143,6 @@ export const FrontComponentWorkerEffect = ({
|
||||
applicationAccessToken,
|
||||
apiUrl,
|
||||
sdkClientUrls,
|
||||
applicationVariables,
|
||||
frontComponentId,
|
||||
setError,
|
||||
setReceiver,
|
||||
|
||||
@@ -91,13 +91,6 @@ const render: WorkerExports['render'] = async (
|
||||
document.body.append(root);
|
||||
installStyleBridge(root);
|
||||
|
||||
if (isDefined(renderContext.applicationVariables)) {
|
||||
setWorkerEnv({
|
||||
applicationVariables: JSON.stringify(renderContext.applicationVariables),
|
||||
});
|
||||
}
|
||||
|
||||
// System variables are set after application variables so they cannot be overridden
|
||||
if (isDefined(renderContext.apiUrl)) {
|
||||
setWorkerEnv({
|
||||
TWENTY_API_URL: renderContext.apiUrl,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const setWorkerEnv = (variables: Record<string, string>) => {
|
||||
export const setWorkerEnv = (environmentVariables: Record<string, string>) => {
|
||||
const globalObject = globalThis as Record<string, unknown>;
|
||||
const processObject =
|
||||
(globalObject['process'] as Record<string, unknown> | undefined) ?? {};
|
||||
@@ -7,7 +7,7 @@ export const setWorkerEnv = (variables: Record<string, string>) => {
|
||||
|
||||
processObject['env'] = {
|
||||
...processEnvironment,
|
||||
...variables,
|
||||
...environmentVariables,
|
||||
};
|
||||
|
||||
globalObject['process'] = processObject;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user