Add twenty slack to internal application ci (#21849)
- adds `twenty-slack` to internal application ci - unify config with twenty-last-contact app - add base oxlint config to show error twenty-shared is used in internal app <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21849?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -66,7 +66,7 @@ jobs:
|
||||
// keep this PR small while the remaining apps are made CI-ready. Remove an
|
||||
// app from this list once its checks pass, and delete the list entirely
|
||||
// once every application is covered.
|
||||
const CI_EXCLUDED_APPLICATIONS = ['call-recording', 'exa', 'people-data-labs', 'self-hosting', 'twenty-discord', 'twenty-fireflies', 'twenty-for-twenty', 'twenty-linear', 'twenty-meeting-bot', 'twenty-partners', 'twenty-slack'];
|
||||
const CI_EXCLUDED_APPLICATIONS = ['call-recording', 'exa', 'people-data-labs', 'self-hosting', 'twenty-discord', 'twenty-fireflies', 'twenty-for-twenty', 'twenty-linear', 'twenty-meeting-bot', 'twenty-partners'];
|
||||
const eventName = process.env.EVENT_NAME;
|
||||
const changedFiles = JSON.parse(process.env.CHANGED_FILES || '[]');
|
||||
const changedApps = new Set();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["twenty-shared", "twenty-shared/*"],
|
||||
"message": "twenty-apps applications must not import from twenty-shared directly. Import the runtime types and helpers you need from twenty-sdk instead so the app bundle stays minimal."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
@@ -44,23 +45,5 @@
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.logic-function.ts", "**/logic-functions/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["twenty-shared", "twenty-shared/*"],
|
||||
"message": "Logic functions must not import from twenty-shared directly. Import runtime types and helpers from `twenty-sdk/logic-function` instead so the logic-function bundle stays minimal."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["react", "typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
@@ -15,23 +16,5 @@
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.logic-function.ts", "**/logic-functions/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["twenty-shared", "twenty-shared/*"],
|
||||
"message": "Logic functions must not import from twenty-shared directly. Import runtime types and helpers from `twenty-sdk/logic-function` instead so the logic-function bundle stays minimal."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
@@ -15,23 +16,5 @@
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.logic-function.ts", "**/logic-functions/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["twenty-shared", "twenty-shared/*"],
|
||||
"message": "Logic functions must not import from twenty-shared directly. Import runtime types and helpers from `twenty-sdk/logic-function` instead so the logic-function bundle stays minimal."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn
|
||||
|
||||
# codegen
|
||||
generated
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# dev
|
||||
/dist/
|
||||
|
||||
.twenty
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
*.d.ts
|
||||
@@ -0,0 +1 @@
|
||||
24.5.0
|
||||
@@ -1,18 +1,58 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"extends": ["../../.oxlintrc.base.json"],
|
||||
"plugins": ["react", "typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
"import/no-duplicates": "error",
|
||||
"typescript/no-redeclare": "error",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "inline-type-imports"
|
||||
}
|
||||
],
|
||||
"typescript/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"typescript/no-empty-object-type": [
|
||||
"error",
|
||||
{
|
||||
"allowInterfaces": "with-single-extends"
|
||||
}
|
||||
],
|
||||
"typescript/no-empty-function": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-key": "off",
|
||||
"react/display-name": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/jsx-no-useless-fragment": "off",
|
||||
"react/jsx-props-no-spreading": ["error", { "explicitSpread": "ignore" }],
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
@@ -16,20 +16,25 @@
|
||||
"twenty": "twenty",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"typecheck": "tsgo --noEmit -p tsconfig.spec.json",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"test:unit": "vitest run --config vitest.unit.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@slack/web-api": "^7.8.0",
|
||||
"twenty-sdk": "2.13.0"
|
||||
"@slack/web-api": "^7.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"twenty-client-sdk": "^2.14.0",
|
||||
"twenty-sdk": "^2.14.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { appDevOnce, appUninstall } from 'twenty-sdk/cli';
|
||||
|
||||
const APP_PATH = process.cwd();
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.twenty');
|
||||
|
||||
function validateEnv(): { apiUrl: string; apiKey: string } {
|
||||
const apiUrl = process.env.TWENTY_API_URL;
|
||||
const apiKey = process.env.TWENTY_API_KEY;
|
||||
|
||||
if (!apiUrl || !apiKey) {
|
||||
throw new Error(
|
||||
'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' +
|
||||
'Start a local server: yarn twenty docker:start\n' +
|
||||
'Or set them in vitest env config.',
|
||||
);
|
||||
}
|
||||
|
||||
return { apiUrl, apiKey };
|
||||
}
|
||||
|
||||
async function checkServer(apiUrl: string) {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
response = await fetch(`${apiUrl}/healthz`);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${apiUrl}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server at ${apiUrl} returned ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfig(apiUrl: string, apiKey: string) {
|
||||
const payload = JSON.stringify(
|
||||
{
|
||||
remotes: {
|
||||
local: { apiUrl, apiKey, accessToken: apiKey },
|
||||
},
|
||||
defaultRemote: 'local',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload);
|
||||
}
|
||||
|
||||
export async function setup() {
|
||||
const { apiUrl, apiKey } = validateEnv();
|
||||
|
||||
await checkServer(apiUrl);
|
||||
|
||||
writeConfig(apiUrl, apiKey);
|
||||
|
||||
await appUninstall({ appPath: APP_PATH }).catch(() => {});
|
||||
|
||||
const result = await appDevOnce({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(`[dev] ${message}`),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Dev sync failed: ${result.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
describe('App installation', () => {
|
||||
it('should find the installed Slack app in the applications list', async () => {
|
||||
const client = new MetadataApiClient();
|
||||
|
||||
const result = await client.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
const matchingApplication = result.findManyApplications.find(
|
||||
(application: { universalIdentifier: string }) =>
|
||||
application.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(matchingApplication).toBeDefined();
|
||||
});
|
||||
});
|
||||
+56
-13
@@ -1,20 +1,63 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type SyntheticEvent,
|
||||
} from 'react';
|
||||
import { type CSSProperties, type SyntheticEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { defineFrontComponent } from 'twenty-sdk/define';
|
||||
import {
|
||||
closeSidePanel,
|
||||
enqueueSnackbar,
|
||||
unmountFrontComponent,
|
||||
} from 'twenty-sdk/front-component';
|
||||
import { themeCssVariables } from 'twenty-sdk/ui';
|
||||
import { closeSidePanel, enqueueSnackbar, unmountFrontComponent } from 'twenty-sdk/front-component';
|
||||
|
||||
import { SEND_MESSAGE_FORM_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
|
||||
// Workaround: 'twenty-sdk/ui' currently fails typecheck because it re-exports
|
||||
// from the unresolvable 'twenty-ui-deprecated'. Inline only the theme tokens
|
||||
// this component uses, keeping the same runtime CSS-variable values. Revert to
|
||||
// `import { themeCssVariables } from 'twenty-sdk/ui'` once the SDK export is fixed.
|
||||
const themeCssVariables = {
|
||||
spacing: {
|
||||
'1': 'var(--t-spacing-1)',
|
||||
'2': 'var(--t-spacing-2)',
|
||||
'3': 'var(--t-spacing-3)',
|
||||
'4': 'var(--t-spacing-4)',
|
||||
'8': 'var(--t-spacing-8)',
|
||||
'20': 'var(--t-spacing-20)',
|
||||
},
|
||||
accent: {
|
||||
accent4060: 'var(--t-accent-accent4060)',
|
||||
},
|
||||
background: {
|
||||
primary: 'var(--t-background-primary)',
|
||||
secondary: 'var(--t-background-secondary)',
|
||||
},
|
||||
border: {
|
||||
color: {
|
||||
medium: 'var(--t-border-color-medium)',
|
||||
light: 'var(--t-border-color-light)',
|
||||
},
|
||||
radius: {
|
||||
sm: 'var(--t-border-radius-sm)',
|
||||
},
|
||||
},
|
||||
font: {
|
||||
color: {
|
||||
primary: 'var(--t-font-color-primary)',
|
||||
secondary: 'var(--t-font-color-secondary)',
|
||||
tertiary: 'var(--t-font-color-tertiary)',
|
||||
inverted: 'var(--t-font-color-inverted)',
|
||||
danger: 'var(--t-font-color-danger)',
|
||||
},
|
||||
size: {
|
||||
xs: 'var(--t-font-size-xs)',
|
||||
sm: 'var(--t-font-size-sm)',
|
||||
md: 'var(--t-font-size-md)',
|
||||
},
|
||||
weight: {
|
||||
regular: 'var(--t-font-weight-regular)',
|
||||
medium: 'var(--t-font-weight-medium)',
|
||||
semiBold: 'var(--t-font-weight-semi-bold)',
|
||||
},
|
||||
family: 'var(--t-font-family)',
|
||||
},
|
||||
color: {
|
||||
blue: 'var(--t-color-blue)',
|
||||
},
|
||||
};
|
||||
|
||||
type SlackChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { slackAddReactionHandler } from 'src/logic-functions/handlers/slack-add-reaction-handler';
|
||||
|
||||
const { getSlackClientMock, addReactionMock } = vi.hoisted(() => ({
|
||||
getSlackClientMock: vi.fn(),
|
||||
addReactionMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('src/logic-functions/utils/get-slack-client', () => ({
|
||||
getSlackClient: getSlackClientMock,
|
||||
}));
|
||||
|
||||
const CHANNEL_ID = 'C0123456789';
|
||||
const MESSAGE_TS = '1700000000.000100';
|
||||
|
||||
describe('slackAddReactionHandler', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
getSlackClientMock.mockResolvedValue({
|
||||
success: true,
|
||||
client: { reactions: { add: addReactionMock } },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a failure result and skip reacting when Slack is not connected', async () => {
|
||||
getSlackClientMock.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Slack is not connected.',
|
||||
});
|
||||
|
||||
const result = await slackAddReactionHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageTimestamp: MESSAGE_TS,
|
||||
emojiName: 'thumbsup',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(addReactionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add the reaction with a colon-stripped, trimmed emoji name', async () => {
|
||||
addReactionMock.mockResolvedValue({ ok: true });
|
||||
|
||||
const result = await slackAddReactionHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageTimestamp: MESSAGE_TS,
|
||||
emojiName: ' white_check_mark ',
|
||||
});
|
||||
|
||||
expect(addReactionMock).toHaveBeenCalledWith({
|
||||
channel: CHANNEL_ID,
|
||||
timestamp: MESSAGE_TS,
|
||||
name: 'white_check_mark',
|
||||
});
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: 'Reaction "white_check_mark" added to the message.',
|
||||
slackTs: MESSAGE_TS,
|
||||
channel: CHANNEL_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a failure result when the Slack API throws', async () => {
|
||||
addReactionMock.mockRejectedValue(new Error('already_reacted'));
|
||||
|
||||
const result = await slackAddReactionHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageTimestamp: MESSAGE_TS,
|
||||
emojiName: 'eyes',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
message: 'Failed to add Slack reaction',
|
||||
error: 'already_reacted',
|
||||
});
|
||||
});
|
||||
});
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { slackPostMessageHandler } from 'src/logic-functions/handlers/slack-post-message-handler';
|
||||
|
||||
const { getSlackClientMock, postMessageMock } = vi.hoisted(() => ({
|
||||
getSlackClientMock: vi.fn(),
|
||||
postMessageMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('src/logic-functions/utils/get-slack-client', () => ({
|
||||
getSlackClient: getSlackClientMock,
|
||||
}));
|
||||
|
||||
const CHANNEL_ID = 'C0123456789';
|
||||
|
||||
describe('slackPostMessageHandler', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
getSlackClientMock.mockResolvedValue({
|
||||
success: true,
|
||||
client: { chat: { postMessage: postMessageMock } },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a failure result and skip posting when Slack is not connected', async () => {
|
||||
getSlackClientMock.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Slack is not connected.',
|
||||
});
|
||||
|
||||
const result = await slackPostMessageHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageText: 'hello',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('Slack is not connected.');
|
||||
expect(postMessageMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should post the message and return the Slack timestamp and channel', async () => {
|
||||
postMessageMock.mockResolvedValue({
|
||||
ts: '1700000000.000100',
|
||||
channel: CHANNEL_ID,
|
||||
});
|
||||
|
||||
const result = await slackPostMessageHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageText: 'hello',
|
||||
messageFormat: 'markdown',
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledWith({
|
||||
channel: CHANNEL_ID,
|
||||
thread_ts: undefined,
|
||||
markdown_text: 'hello',
|
||||
});
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: 'Message posted to Slack (ts=1700000000.000100).',
|
||||
slackTs: '1700000000.000100',
|
||||
channel: CHANNEL_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reply inside a thread with a trimmed parent timestamp', async () => {
|
||||
postMessageMock.mockResolvedValue({
|
||||
ts: '1700000000.000200',
|
||||
channel: CHANNEL_ID,
|
||||
});
|
||||
|
||||
await slackPostMessageHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageText: 'in thread',
|
||||
parentMessageTimestamp: ' 1699999999.000100 ',
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ thread_ts: '1699999999.000100' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not set thread_ts when the parent timestamp is blank', async () => {
|
||||
postMessageMock.mockResolvedValue({ ts: '1700000000.000300' });
|
||||
|
||||
await slackPostMessageHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageText: 'standalone',
|
||||
parentMessageTimestamp: ' ',
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ thread_ts: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should treat a runtime null parent timestamp as no thread without throwing', async () => {
|
||||
postMessageMock.mockResolvedValue({ ts: '1700000000.000400' });
|
||||
|
||||
await slackPostMessageHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageText: 'standalone',
|
||||
parentMessageTimestamp: null as unknown as undefined,
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ thread_ts: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a failure result when the Slack API throws', async () => {
|
||||
postMessageMock.mockRejectedValue(new Error('channel_not_found'));
|
||||
|
||||
const result = await slackPostMessageHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageText: 'hello',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
message: 'Failed to post Slack message',
|
||||
error: 'channel_not_found',
|
||||
});
|
||||
});
|
||||
});
|
||||
+4
-6
@@ -1,12 +1,10 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type SlackChannelType,
|
||||
type SlackListChannelsInput,
|
||||
type SlackListChannelsInput
|
||||
} from 'src/logic-functions/types/slack-list-channels-input.type';
|
||||
import {
|
||||
type SlackListChannelsResult,
|
||||
type SlackListChannelsResultChannel,
|
||||
type SlackListChannelsResultChannel
|
||||
} from 'src/logic-functions/types/slack-list-channels-result.type';
|
||||
import { getSlackClient } from 'src/logic-functions/utils/get-slack-client';
|
||||
|
||||
@@ -63,7 +61,7 @@ export const slackListChannelsHandler = async (
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isDefined(channel.id) || !isDefined(channel.name)) {
|
||||
if (channel.id == null || channel.name == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -81,7 +79,7 @@ export const slackListChannelsHandler = async (
|
||||
|
||||
const nextCursor = response.response_metadata?.next_cursor;
|
||||
|
||||
if (!isDefined(nextCursor) || nextCursor.length === 0) {
|
||||
if (nextCursor == null || nextCursor.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type SlackPostMessageInput } from 'src/logic-functions/types/slack-post-message-input.type';
|
||||
import { type SlackToolResult } from 'src/logic-functions/types/slack-tool-result.type';
|
||||
import { getSlackChatMessageBodyFields } from 'src/logic-functions/utils/get-slack-chat-message-body-fields';
|
||||
@@ -32,7 +30,7 @@ export const slackPostMessageHandler = async (
|
||||
const data = await client.chat.postMessage({
|
||||
channel: parameters.slackChannelId,
|
||||
thread_ts:
|
||||
isDefined(parentTimestamp) && parentTimestamp.trim().length > 0
|
||||
parentTimestamp != null && parentTimestamp.trim().length > 0
|
||||
? parentTimestamp.trim()
|
||||
: undefined,
|
||||
...bodyFields,
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { SLACK_ADD_REACTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { slackAddReactionHandler } from 'src/logic-functions/handlers/slack-add-reaction-handler';
|
||||
import { jsonSchemaToInputSchema } from 'src/logic-functions/utils/json-schema-to-input-schema';
|
||||
import { slackAddReactionInputSchema } from './schemas/slack-add-reaction-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { SLACK_DELETE_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { slackDeleteMessageHandler } from 'src/logic-functions/handlers/slack-delete-message-handler';
|
||||
import { jsonSchemaToInputSchema } from 'src/logic-functions/utils/json-schema-to-input-schema';
|
||||
import { slackDeleteMessageInputSchema } from './schemas/slack-delete-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
import { SLACK_LIST_CHANNELS_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { slackListChannelsHandler } from 'src/logic-functions/handlers/slack-list-channels-handler';
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { SLACK_LIST_CHANNELS_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { slackListChannelsHandler } from 'src/logic-functions/handlers/slack-list-channels-handler';
|
||||
import { jsonSchemaToInputSchema } from 'src/logic-functions/utils/json-schema-to-input-schema';
|
||||
import { slackListChannelsInputSchema } from './schemas/slack-list-channels-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { SLACK_POST_EPHEMERAL_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { slackPostEphemeralMessageHandler } from 'src/logic-functions/handlers/slack-post-ephemeral-message-handler';
|
||||
import { jsonSchemaToInputSchema } from 'src/logic-functions/utils/json-schema-to-input-schema';
|
||||
import { slackPostEphemeralMessageInputSchema } from './schemas/slack-post-ephemeral-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/define';
|
||||
import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
|
||||
import { SLACK_POST_MESSAGE_ROUTE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { slackPostMessageHandler } from 'src/logic-functions/handlers/slack-post-message-handler';
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { SLACK_POST_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { slackPostMessageHandler } from 'src/logic-functions/handlers/slack-post-message-handler';
|
||||
import { jsonSchemaToInputSchema } from 'src/logic-functions/utils/json-schema-to-input-schema';
|
||||
import { slackPostMessageInputSchema } from './schemas/slack-post-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { SLACK_UPDATE_MESSAGE_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { slackUpdateMessageHandler } from 'src/logic-functions/handlers/slack-update-message-handler';
|
||||
import { jsonSchemaToInputSchema } from 'src/logic-functions/utils/json-schema-to-input-schema';
|
||||
import { slackUpdateMessageInputSchema } from './schemas/slack-update-message-input.schema';
|
||||
|
||||
export default defineLogicFunction({
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getSlackChatMessageBodyFields } from 'src/logic-functions/utils/get-slack-chat-message-body-fields';
|
||||
|
||||
describe('getSlackChatMessageBodyFields', () => {
|
||||
it('should send markdown_text when the format is markdown', () => {
|
||||
expect(getSlackChatMessageBodyFields('hello', 'markdown')).toEqual({
|
||||
markdown_text: 'hello',
|
||||
});
|
||||
});
|
||||
|
||||
it('should send plain text with mrkdwn disabled when the format is plain', () => {
|
||||
expect(getSlackChatMessageBodyFields('hello', 'plain')).toEqual({
|
||||
text: 'hello',
|
||||
mrkdwn: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fall back to a plain text body when no format is provided', () => {
|
||||
expect(getSlackChatMessageBodyFields('hello', undefined)).toEqual({
|
||||
text: 'hello',
|
||||
});
|
||||
});
|
||||
});
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getSlackConnection } from 'src/logic-functions/utils/get-slack-connection';
|
||||
|
||||
const { listConnectionsMock } = vi.hoisted(() => ({
|
||||
listConnectionsMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('twenty-sdk/logic-function', () => ({
|
||||
listConnections: listConnectionsMock,
|
||||
}));
|
||||
|
||||
describe('getSlackConnection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should query connections for the slack provider', async () => {
|
||||
listConnectionsMock.mockResolvedValue([
|
||||
{ visibility: 'workspace', accessToken: 'workspace-token' },
|
||||
]);
|
||||
|
||||
await getSlackConnection();
|
||||
|
||||
expect(listConnectionsMock).toHaveBeenCalledWith({ providerName: 'slack' });
|
||||
});
|
||||
|
||||
it('should prefer a workspace-visibility connection over a personal one', async () => {
|
||||
listConnectionsMock.mockResolvedValue([
|
||||
{ visibility: 'personal', accessToken: 'personal-token' },
|
||||
{ visibility: 'workspace', accessToken: 'workspace-token' },
|
||||
]);
|
||||
|
||||
const result = await getSlackConnection();
|
||||
|
||||
expect(result).toEqual({ success: true, accessToken: 'workspace-token' });
|
||||
});
|
||||
|
||||
it('should fall back to the first connection when none are workspace-visible', async () => {
|
||||
listConnectionsMock.mockResolvedValue([
|
||||
{ visibility: 'personal', accessToken: 'first-token' },
|
||||
{ visibility: 'personal', accessToken: 'second-token' },
|
||||
]);
|
||||
|
||||
const result = await getSlackConnection();
|
||||
|
||||
expect(result).toEqual({ success: true, accessToken: 'first-token' });
|
||||
});
|
||||
|
||||
it('should fail when there is no Slack connection', async () => {
|
||||
listConnectionsMock.mockResolvedValue([]);
|
||||
|
||||
const result = await getSlackConnection();
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail gracefully when listing connections throws', async () => {
|
||||
listConnectionsMock.mockRejectedValue(new Error('network down'));
|
||||
|
||||
const result = await getSlackConnection();
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'network down' });
|
||||
});
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { slackToolFailure } from 'src/logic-functions/utils/slack-tool-failure';
|
||||
|
||||
describe('slackToolFailure', () => {
|
||||
it('should surface the underlying Error message', () => {
|
||||
const result = slackToolFailure(
|
||||
'Failed to post Slack message',
|
||||
new Error('channel_not_found'),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
message: 'Failed to post Slack message',
|
||||
error: 'channel_not_found',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use a generic error when the thrown value is not an Error', () => {
|
||||
const result = slackToolFailure('Failed to post Slack message', 'boom');
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
message: 'Failed to post Slack message',
|
||||
error: 'Slack request failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
type InputSchemaProperty = {
|
||||
type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'unknown';
|
||||
enum?: string[];
|
||||
items?: InputSchemaProperty;
|
||||
properties?: Record<string, InputSchemaProperty>;
|
||||
multiline?: boolean;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const convertProperty = (jsonSchema: InputJsonSchema): InputSchemaProperty => {
|
||||
const property: InputSchemaProperty = { type: 'unknown' };
|
||||
|
||||
switch (jsonSchema.type) {
|
||||
case 'string':
|
||||
property.type = 'string';
|
||||
break;
|
||||
case 'number':
|
||||
case 'integer':
|
||||
property.type = 'number';
|
||||
break;
|
||||
case 'boolean':
|
||||
property.type = 'boolean';
|
||||
break;
|
||||
case 'array':
|
||||
property.type = 'array';
|
||||
if (jsonSchema.items) {
|
||||
property.items = convertProperty(jsonSchema.items);
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
property.type = 'object';
|
||||
if (jsonSchema.properties) {
|
||||
property.properties = Object.fromEntries(
|
||||
Object.entries(jsonSchema.properties).map(([key, value]) => [
|
||||
key,
|
||||
convertProperty(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
property.type = 'unknown';
|
||||
}
|
||||
|
||||
if (Array.isArray(jsonSchema.enum)) {
|
||||
property.enum = jsonSchema.enum.filter(
|
||||
(value): value is string => typeof value === 'string',
|
||||
);
|
||||
}
|
||||
|
||||
if (jsonSchema.multiline === true) {
|
||||
property.multiline = true;
|
||||
}
|
||||
|
||||
if (typeof jsonSchema.label === 'string' && jsonSchema.label.length > 0) {
|
||||
property.label = jsonSchema.label;
|
||||
}
|
||||
|
||||
return property;
|
||||
};
|
||||
|
||||
export const jsonSchemaToInputSchema = (
|
||||
jsonSchema: InputJsonSchema,
|
||||
): InputSchemaProperty[] => [convertProperty(jsonSchema)];
|
||||
@@ -17,5 +17,5 @@ export default defineRole({
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlags: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
});
|
||||
|
||||
@@ -27,5 +27,16 @@
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.integration-test.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["vitest/globals", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,7 +1,31 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020';
|
||||
const TWENTY_API_KEY =
|
||||
process.env.TWENTY_API_KEY ??
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc';
|
||||
|
||||
// Make env vars available to globalSetup (test.env only applies to workers)
|
||||
process.env.TWENTY_API_URL = TWENTY_API_URL;
|
||||
process.env.TWENTY_API_KEY = TWENTY_API_KEY;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
fileParallelism: false,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
globalSetup: ['src/__tests__/global-setup.ts'],
|
||||
env: {
|
||||
TWENTY_API_URL,
|
||||
TWENTY_API_KEY,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths({
|
||||
projects: ['tsconfig.spec.json'],
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
@@ -834,6 +834,87 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260616.1":
|
||||
version: 7.0.0-dev.20260616.1
|
||||
resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260616.1"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260616.1":
|
||||
version: 7.0.0-dev.20260616.1
|
||||
resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260616.1"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260616.1":
|
||||
version: 7.0.0-dev.20260616.1
|
||||
resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260616.1"
|
||||
conditions: os=linux & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260616.1":
|
||||
version: 7.0.0-dev.20260616.1
|
||||
resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260616.1"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260616.1":
|
||||
version: 7.0.0-dev.20260616.1
|
||||
resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260616.1"
|
||||
conditions: os=linux & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260616.1":
|
||||
version: 7.0.0-dev.20260616.1
|
||||
resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260616.1"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260616.1":
|
||||
version: 7.0.0-dev.20260616.1
|
||||
resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260616.1"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript/native-preview@npm:^7.0.0-dev.20260116.1":
|
||||
version: 7.0.0-dev.20260616.1
|
||||
resolution: "@typescript/native-preview@npm:7.0.0-dev.20260616.1"
|
||||
dependencies:
|
||||
"@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260616.1"
|
||||
"@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260616.1"
|
||||
"@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260616.1"
|
||||
"@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260616.1"
|
||||
"@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260616.1"
|
||||
"@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260616.1"
|
||||
"@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260616.1"
|
||||
dependenciesMeta:
|
||||
"@typescript/native-preview-darwin-arm64":
|
||||
optional: true
|
||||
"@typescript/native-preview-darwin-x64":
|
||||
optional: true
|
||||
"@typescript/native-preview-linux-arm":
|
||||
optional: true
|
||||
"@typescript/native-preview-linux-arm64":
|
||||
optional: true
|
||||
"@typescript/native-preview-linux-x64":
|
||||
optional: true
|
||||
"@typescript/native-preview-win32-arm64":
|
||||
optional: true
|
||||
"@typescript/native-preview-win32-x64":
|
||||
optional: true
|
||||
bin:
|
||||
tsgo: bin/tsgo.js
|
||||
checksum: 10c0/e858f6a5bc41e615d3a87381ac33c938cbaa3553c47f4f1c3e06682fd96303f624ecd457542b7980a7eb29f73b54da19e8a52650246cc98a145c4a46aa743b40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/expect@npm:4.1.8":
|
||||
version: 4.1.8
|
||||
resolution: "@vitest/expect@npm:4.1.8"
|
||||
@@ -1142,7 +1223,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:4":
|
||||
"debug@npm:4, debug@npm:^4.1.1":
|
||||
version: 4.4.3
|
||||
resolution: "debug@npm:4.4.3"
|
||||
dependencies:
|
||||
@@ -1525,6 +1606,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"globrex@npm:^0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "globrex@npm:0.1.2"
|
||||
checksum: 10c0/a54c029520cf58bda1d8884f72bd49b4cd74e977883268d931fd83bcbd1a9eb96d57c7dbd4ad80148fb9247467ebfb9b215630b2ed7563b2a8de02e1ff7f89d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gopd@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "gopd@npm:1.2.0"
|
||||
@@ -2530,6 +2618,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsconfck@npm:^3.0.3":
|
||||
version: 3.1.6
|
||||
resolution: "tsconfck@npm:3.1.6"
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
bin:
|
||||
tsconfck: bin/tsconfck.js
|
||||
checksum: 10c0/269c3c513540be44844117bb9b9258fe6f8aeab026d32aeebf458d5299125f330711429dbb556dbf125a0bc25f4a81e6c24ac96de2740badd295c3fb400f66c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tslib@npm:^1.9.3":
|
||||
version: 1.14.1
|
||||
resolution: "tslib@npm:1.14.1"
|
||||
@@ -2544,22 +2646,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-client-sdk@npm:2.13.0":
|
||||
version: 2.13.0
|
||||
resolution: "twenty-client-sdk@npm:2.13.0"
|
||||
"twenty-client-sdk@npm:2.14.0, twenty-client-sdk@npm:^2.14.0":
|
||||
version: 2.14.0
|
||||
resolution: "twenty-client-sdk@npm:2.14.0"
|
||||
dependencies:
|
||||
"@genql/runtime": "npm:^2.10.0"
|
||||
esbuild: "npm:^0.28.1"
|
||||
graphql: "npm:^16.8.1"
|
||||
lodash: "npm:^4.17.21"
|
||||
prettier: "npm:^3.8.3"
|
||||
checksum: 10c0/740464acec94c1d4cc5fa50ed6b190a7f1d1681963f61437a6c704835c05ffe0f5a13e254e57601a99b07bbe0f68483dee19211731853aafbcc15ec79f8039ce
|
||||
checksum: 10c0/eb222a726e21fc98f3a624f9acac7d47f0c769b989911889036665dac7c7c88698ec99beb844bf51a42e1696ea0a067bae8cf47fd751965af52a3720835242f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"twenty-sdk@npm:2.13.0":
|
||||
version: 2.13.0
|
||||
resolution: "twenty-sdk@npm:2.13.0"
|
||||
"twenty-sdk@npm:^2.14.0":
|
||||
version: 2.14.0
|
||||
resolution: "twenty-sdk@npm:2.14.0"
|
||||
dependencies:
|
||||
"@sniptt/guards": "npm:^0.2.0"
|
||||
axios: "npm:^1.16.0"
|
||||
@@ -2576,12 +2678,12 @@ __metadata:
|
||||
react: "npm:^19.2.0"
|
||||
react-dom: "npm:^19.2.0"
|
||||
tinyglobby: "npm:^0.2.15"
|
||||
twenty-client-sdk: "npm:2.13.0"
|
||||
twenty-client-sdk: "npm:2.14.0"
|
||||
typescript: "npm:^5.9.3"
|
||||
uuid: "npm:^13.0.2"
|
||||
bin:
|
||||
twenty: dist/cli.cjs
|
||||
checksum: 10c0/51c350abe5d344fcad3c961a77632fd3cb2d91e357d0562b3eb02f05f66dbc41221c5463d0f91c022316a16af03a05ad43f038d9534c0ae31c060008d0e48672
|
||||
checksum: 10c0/c4211772f8dd2ba81074a74be5f616c4c264d6bf8a174c5724be6d1c9a8cb3ca68a2674653bb58c059dae4bf1b0f43f91991e73d6a01395807206f0ac44afb08
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2592,11 +2694,14 @@ __metadata:
|
||||
"@slack/web-api": "npm:^7.8.0"
|
||||
"@types/node": "npm:^24.7.2"
|
||||
"@types/react": "npm:^18.2.0"
|
||||
"@typescript/native-preview": "npm:^7.0.0-dev.20260116.1"
|
||||
oxlint: "npm:^0.16.0"
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
twenty-sdk: "npm:2.13.0"
|
||||
twenty-client-sdk: "npm:^2.14.0"
|
||||
twenty-sdk: "npm:^2.14.0"
|
||||
typescript: "npm:^5.9.3"
|
||||
vite-tsconfig-paths: "npm:^4.2.1"
|
||||
vitest: "npm:^4.0.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -2674,6 +2779,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-tsconfig-paths@npm:^4.2.1":
|
||||
version: 4.3.2
|
||||
resolution: "vite-tsconfig-paths@npm:4.3.2"
|
||||
dependencies:
|
||||
debug: "npm:^4.1.1"
|
||||
globrex: "npm:^0.1.2"
|
||||
tsconfck: "npm:^3.0.3"
|
||||
peerDependencies:
|
||||
vite: "*"
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
checksum: 10c0/f390ac1d1c3992fc5ac50f9274c1090f8b55ab34a89ea88893db9a6924a3b26c9f64bc1163615150ad100749db73b6b2cf1d57f6cd60df6e762ceb5b8ad30024
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version: 8.0.16
|
||||
resolution: "vite@npm:8.0.16"
|
||||
|
||||
@@ -349,6 +349,32 @@ export default defineLogicFunction({
|
||||
});
|
||||
```
|
||||
|
||||
To declare your parameters **once** and serve both surfaces, define a single JSON Schema (`InputJsonSchema`) and convert it for the workflow action with `jsonSchemaToInputSchema` from `twenty-sdk/logic-function`. `toolTriggerSettings.inputSchema` takes the JSON Schema directly, while `workflowActionTriggerSettings.inputSchema` expects Twenty's `InputSchema`:
|
||||
|
||||
```ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { jsonSchemaToInputSchema, type InputJsonSchema } from 'twenty-sdk/logic-function';
|
||||
|
||||
const inputSchema: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: { type: 'string', label: 'Company name' },
|
||||
domain: { type: 'string', label: 'Domain' },
|
||||
},
|
||||
required: ['companyName'],
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolTriggerSettings: { inputSchema },
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Enrich Company',
|
||||
icon: 'IconBuilding',
|
||||
inputSchema: jsonSchemaToInputSchema(inputSchema),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
**Write a good `description`.** AI agents rely on the function's `description` field to decide when to use the tool. Be specific about what the tool does and when it should be called.
|
||||
</Note>
|
||||
@@ -356,6 +382,20 @@ export default defineLogicFunction({
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Note>
|
||||
**Runtime helpers.** `twenty-sdk/utils` re-exports small runtime helpers so handlers never import from `twenty-shared` directly. For example, `isDefined(value)` returns `false` for both `null` and `undefined` — use it to safely narrow optional handler inputs, which can arrive as `null` at runtime even when typed `T | undefined`:
|
||||
|
||||
```ts
|
||||
import { isDefined } from 'twenty-sdk/utils';
|
||||
|
||||
const handler = async (params: { parentMessageId?: string }) => {
|
||||
if (isDefined(params.parentMessageId)) {
|
||||
// params.parentMessageId is narrowed to string here
|
||||
}
|
||||
};
|
||||
```
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
**Install hooks** — pre-install and post-install handlers — share this runtime but are declared with their own define functions and don't take trigger settings. See [Install Hooks](/developers/extend/apps/config/install-hooks) for `definePreInstallLogicFunction` and `definePostInstallLogicFunction`.
|
||||
</Note>
|
||||
|
||||
@@ -36,7 +36,10 @@ export type {
|
||||
|
||||
export type { RoutePayload } from '@/sdk/define/logic-functions/triggers/route-payload-type';
|
||||
|
||||
export type { InputJsonSchema } from 'twenty-shared/logic-function';
|
||||
export {
|
||||
jsonSchemaToInputSchema,
|
||||
type InputJsonSchema,
|
||||
} from 'twenty-shared/logic-function';
|
||||
|
||||
export { getConnection } from '@/sdk/logic-function/connections/get-connection';
|
||||
export { listConnections } from '@/sdk/logic-function/connections/list-connections';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { getPublicAssetUrl } from '@/sdk/utils/get-public-asset-url';
|
||||
export { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
Reference in New Issue
Block a user