Files
twenty/packages/twenty-shared/package.json
T
Etienne c6309fd92b feat(workflow): auto-layout steps on AI workflow creation via shared tidy-up (#21756)
## Context

The workflow builder has a "Tidy up" action that auto-positions steps
using a
Dagre layout. However, this lived entirely in the frontend and depended
on node
dimensions measured by React Flow after rendering in the browser.

As a result, workflows (and steps) created through AI Chat / MCP tools
were never
laid out: `create_complete_workflow` accepted optional `stepPositions`
that the
LLM had to invent, and `create_workflow_version_step` stored an optional
position
verbatim. In practice this produced overlapping / poorly positioned
steps.

## What this does

Extracts the tidy-up layout into a pure, frontend-free util in
`twenty-shared` and
reuses it from both the frontend tidy-up and the server, so
AI/MCP-created
workflows are auto-laid out at creation time.

### twenty-shared
- New `computeWorkflowLayout({ nodes, edges, options? })` — a pure Dagre
layout over
a minimal `{ id, width, height }` / `{ source, target }` graph,
returning
top-left-anchored positions (matching React Flow). Ignores edges
pointing to
  unknown nodes.
- New constants: `WORKFLOW_LAYOUT_DEFAULT_OPTIONS`
(ranksep/nodesep/rankdir) and
`WORKFLOW_DIAGRAM_DEFAULT_NODE_DIMENSIONS` (estimated node size for
server-side
  layout, where measured sizes are unavailable).
- Added `@dagrejs/dagre` dependency.

### twenty-front
- `getOrganizedDiagram` now delegates to `computeWorkflowLayout`,
passing real
  measured node sizes. No behavior change for users.

### twenty-server
- New `WorkflowVersionWorkspaceService.autoLayoutWorkflowVersion(...)`
builds the
graph topology via the existing `buildWorkflowGraph` (covers if-else
branches and
iterator loops), feeds estimated node sizes into
`computeWorkflowLayout`, and
  persists through the existing `updateWorkflowVersionPositions`.
- `create_complete_workflow`: removed `stepPositions` from the tool
schema; the
  server always auto-lays out after creation/edges.
- `create_workflow_version_step`: re-tidies the whole version after each
added step
(wired at the tool level so the builder UI is unaffected) and dropped
the now
  redundant `position` field.

## Notes
- Server-side layout uses estimated node sizes, so it is "good enough";
opening the
workflow and running the existing FE tidy-up refines it with real
measured sizes.
- Auto-layout is wired in the MCP tools, not in the shared creation
service, so
  manual step creation in the builder UI is unchanged.

## Test plan
- [x] `twenty-shared` unit tests for `computeWorkflowLayout` (linear
chain, if-else
  spread, dangling-edge safety)
- [x] `twenty-shared` builds; `twenty-server` and `twenty-front`
typecheck
- [x] Lint/format clean on changed files
- [ ] Create a workflow via AI Chat / MCP and confirm steps are laid out
without
  overlap
- [x] Add a step via MCP and confirm the version is re-tidied
- [ ] Frontend "Tidy up" still behaves as before

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21756?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. -->
2026-06-18 14:10:04 +00:00

204 lines
5.0 KiB
JSON

{
"name": "twenty-shared",
"private": true,
"sideEffects": false,
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"license": "AGPL-3.0",
"scripts": {
"build": "npx vite build"
},
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": "^4.0.2"
},
"devDependencies": {
"@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.24.6",
"@lingui/core": "^5.9.5",
"@prettier/sync": "^0.5.2",
"@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39",
"@types/babel__preset-env": "^7",
"@types/handlebars": "^4.1.0",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.escaperegexp": "^4.1.9",
"@types/qs": "6.9.16",
"@types/uuid": "^9.0.2",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"babel-plugin-module-resolver": "^5.0.2",
"glob": "^11.1.0",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"prettier": "^3.1.1",
"slash": "^5.1.0",
"tsc-alias": "^1.8.16",
"tsx": "^4.19.3",
"typescript": "^5.9.3",
"vite": "^8.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^4.2.1"
},
"dependencies": {
"@dagrejs/dagre": "^1.1.8",
"@sniptt/guards": "^0.2.0",
"ai": "6.0.97",
"class-validator": "^0.14.0",
"expr-eval-fork": "3.0.3",
"handlebars": "^4.7.9",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.escaperegexp": "^4.1.2",
"microdiff": "^1.3.2",
"qs": "^6.15.2",
"react-router-dom": "^6.4.4",
"temporal-polyfill": "^0.3.0",
"transliteration": "^2.3.5",
"uuid": "^11.1.1",
"zod": "^4.1.11"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./ai": {
"types": "./dist/ai/index.d.ts",
"import": "./dist/ai.mjs",
"require": "./dist/ai.cjs"
},
"./application": {
"types": "./dist/application/index.d.ts",
"import": "./dist/application.mjs",
"require": "./dist/application.cjs"
},
"./constants": {
"types": "./dist/constants/index.d.ts",
"import": "./dist/constants.mjs",
"require": "./dist/constants.cjs"
},
"./database-events": {
"types": "./dist/database-events/index.d.ts",
"import": "./dist/database-events.mjs",
"require": "./dist/database-events.cjs"
},
"./i18n": {
"types": "./dist/i18n/index.d.ts",
"import": "./dist/i18n.mjs",
"require": "./dist/i18n.cjs"
},
"./logic-function": {
"types": "./dist/logic-function/index.d.ts",
"import": "./dist/logic-function.mjs",
"require": "./dist/logic-function.cjs"
},
"./metadata": {
"types": "./dist/metadata/index.d.ts",
"import": "./dist/metadata.mjs",
"require": "./dist/metadata.cjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"import": "./dist/testing.mjs",
"require": "./dist/testing.cjs"
},
"./translations": {
"types": "./dist/translations/index.d.ts",
"import": "./dist/translations.mjs",
"require": "./dist/translations.cjs"
},
"./types": {
"types": "./dist/types/index.d.ts",
"import": "./dist/types.mjs",
"require": "./dist/types.cjs"
},
"./utils": {
"types": "./dist/utils/index.d.ts",
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
},
"./vite": {
"types": "./dist/vite/index.d.ts",
"import": "./dist/vite.mjs",
"require": "./dist/vite.cjs"
},
"./workflow": {
"types": "./dist/workflow/index.d.ts",
"import": "./dist/workflow.mjs",
"require": "./dist/workflow.cjs"
},
"./workspace": {
"types": "./dist/workspace/index.d.ts",
"import": "./dist/workspace.mjs",
"require": "./dist/workspace.cjs"
}
},
"files": [
"dist",
"ai",
"application",
"constants",
"database-events",
"i18n",
"logic-function",
"metadata",
"testing",
"translations",
"types",
"utils",
"vite",
"workflow",
"workspace"
],
"typesVersions": {
"*": {
"ai": [
"dist/ai/index.d.ts"
],
"application": [
"dist/application/index.d.ts"
],
"constants": [
"dist/constants/index.d.ts"
],
"database-events": [
"dist/database-events/index.d.ts"
],
"i18n": [
"dist/i18n/index.d.ts"
],
"logic-function": [
"dist/logic-function/index.d.ts"
],
"metadata": [
"dist/metadata/index.d.ts"
],
"testing": [
"dist/testing/index.d.ts"
],
"translations": [
"dist/translations/index.d.ts"
],
"types": [
"dist/types/index.d.ts"
],
"utils": [
"dist/utils/index.d.ts"
],
"vite": [
"dist/vite/index.d.ts"
],
"workflow": [
"dist/workflow/index.d.ts"
],
"workspace": [
"dist/workspace/index.d.ts"
]
}
}
}