CVE-2026-46624
Description
Twenty is an open source CRM. From 1.7.7 through 1.16.7, a critical Remote Code Execution (RCE) vulnerability exists in Twenty CRM via a chained SQL Injection and PostgreSQL COPY TO PROGRAM attack. If Postgres user is a super user then any authenticated user can execute arbitrary OS commands on the database server by injecting SQL through the unsanitized timeZone parameter in the REST API groupBy endpoint. The timeZone field within the group_by query parameter is directly interpolated into a raw SQL expression using JavaScript template literals without any parameterization, validation, or escaping. This affects engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util.ts.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Twenty CRM versions 1.7.7 to 1.16.7 allow authenticated RCE via SQL injection in the timeZone parameter, chained with PostgreSQL COPY TO PROGRAM.
Vulnerability
A critical SQL injection vulnerability exists in Twenty CRM versions 1.7.7 through 1.16.7. The timeZone field in the group_by query parameter of the REST API groupBy endpoint is directly interpolated into raw SQL expressions using JavaScript template literals without sanitization or parameterization. This occurs in engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util.ts, allowing an attacker to break out of the string context and execute arbitrary SQL including stacked queries [1].
Exploitation
An authenticated user can exploit the unsanitized timeZone parameter to inject malicious SQL. By crafting a payload that escapes the single-quote string and appending stacked queries, the attacker can leverage PostgreSQL's COPY ... TO PROGRAM functionality if the database user is a superuser. The attack chain involves creating a large object containing a shell script via lo_from_bytea(), exporting it to the filesystem via lo_export(), and executing it via a stacked COPY (SELECT 1) TO PROGRAM query [1].
Impact
Successful exploitation grants the attacker arbitrary OS command execution on the PostgreSQL database server. In default Docker deployments, PostgreSQL runs as a superuser, enabling full control over the server and potentially compromising the entire Twenty CRM instance [1].
Mitigation
No fix version has been disclosed in the available references. Administrators should restrict PostgreSQL superuser privileges and consider input sanitization as a temporary workaround. Upgrading to a patched version once available is recommended [1].
AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
17d68868df537feat: fix junction toggle persistence and add type-safe documentation paths (#17421)
22 files changed · +736 −50
.github/workflows/docs-i18n-pull.yaml+4 −1 modified@@ -107,10 +107,13 @@ jobs: - name: Regenerate docs.json run: yarn docs:generate + - name: Regenerate documentation paths constants + run: yarn docs:generate-paths + - name: Commit artifacts to pull request branch if: github.event_name == 'pull_request' run: | - git add packages/twenty-docs/docs.json packages/twenty-docs/navigation/navigation.template.json + git add packages/twenty-docs/docs.json packages/twenty-docs/navigation/navigation.template.json packages/twenty-shared/src/constants/DocumentationPaths.ts if git diff --staged --quiet --exit-code; then echo "No navigation/doc changes to commit." exit 0
package.json+1 −0 modified@@ -210,6 +210,7 @@ "scripts": { "docs:generate": "tsx packages/twenty-docs/scripts/generate-docs-json.ts", "docs:generate-navigation-template": "tsx packages/twenty-docs/scripts/generate-navigation-template.ts", + "docs:generate-paths": "tsx packages/twenty-docs/scripts/generate-documentation-paths.ts", "start": "npx concurrently --kill-others 'npx nx run-many -t start -p twenty-server twenty-front' 'npx wait-on tcp:3000 && npx nx run twenty-server:worker'" }, "workspaces": {
packages/twenty-docs/docs.json+14 −0 modified@@ -80,6 +80,7 @@ "user-guide/data-model/how-tos/create-custom-objects", "user-guide/data-model/how-tos/create-custom-fields", "user-guide/data-model/how-tos/create-relation-fields", + "user-guide/data-model/how-tos/create-many-to-many-relations", "user-guide/data-model/how-tos/customize-your-data-model", "user-guide/data-model/how-tos/data-model-faq" ] @@ -524,6 +525,7 @@ "l/fr/user-guide/data-model/how-tos/create-custom-objects", "l/fr/user-guide/data-model/how-tos/create-custom-fields", "l/fr/user-guide/data-model/how-tos/create-relation-fields", + "l/fr/user-guide/data-model/how-tos/create-many-to-many-relations", "l/fr/user-guide/data-model/how-tos/customize-your-data-model", "l/fr/user-guide/data-model/how-tos/data-model-faq" ] @@ -968,6 +970,7 @@ "l/ar/user-guide/data-model/how-tos/create-custom-objects", "l/ar/user-guide/data-model/how-tos/create-custom-fields", "l/ar/user-guide/data-model/how-tos/create-relation-fields", + "l/ar/user-guide/data-model/how-tos/create-many-to-many-relations", "l/ar/user-guide/data-model/how-tos/customize-your-data-model", "l/ar/user-guide/data-model/how-tos/data-model-faq" ] @@ -1412,6 +1415,7 @@ "l/cs/user-guide/data-model/how-tos/create-custom-objects", "l/cs/user-guide/data-model/how-tos/create-custom-fields", "l/cs/user-guide/data-model/how-tos/create-relation-fields", + "l/cs/user-guide/data-model/how-tos/create-many-to-many-relations", "l/cs/user-guide/data-model/how-tos/customize-your-data-model", "l/cs/user-guide/data-model/how-tos/data-model-faq" ] @@ -1856,6 +1860,7 @@ "l/de/user-guide/data-model/how-tos/create-custom-objects", "l/de/user-guide/data-model/how-tos/create-custom-fields", "l/de/user-guide/data-model/how-tos/create-relation-fields", + "l/de/user-guide/data-model/how-tos/create-many-to-many-relations", "l/de/user-guide/data-model/how-tos/customize-your-data-model", "l/de/user-guide/data-model/how-tos/data-model-faq" ] @@ -2300,6 +2305,7 @@ "l/es/user-guide/data-model/how-tos/create-custom-objects", "l/es/user-guide/data-model/how-tos/create-custom-fields", "l/es/user-guide/data-model/how-tos/create-relation-fields", + "l/es/user-guide/data-model/how-tos/create-many-to-many-relations", "l/es/user-guide/data-model/how-tos/customize-your-data-model", "l/es/user-guide/data-model/how-tos/data-model-faq" ] @@ -2744,6 +2750,7 @@ "l/it/user-guide/data-model/how-tos/create-custom-objects", "l/it/user-guide/data-model/how-tos/create-custom-fields", "l/it/user-guide/data-model/how-tos/create-relation-fields", + "l/it/user-guide/data-model/how-tos/create-many-to-many-relations", "l/it/user-guide/data-model/how-tos/customize-your-data-model", "l/it/user-guide/data-model/how-tos/data-model-faq" ] @@ -3188,6 +3195,7 @@ "l/ja/user-guide/data-model/how-tos/create-custom-objects", "l/ja/user-guide/data-model/how-tos/create-custom-fields", "l/ja/user-guide/data-model/how-tos/create-relation-fields", + "l/ja/user-guide/data-model/how-tos/create-many-to-many-relations", "l/ja/user-guide/data-model/how-tos/customize-your-data-model", "l/ja/user-guide/data-model/how-tos/data-model-faq" ] @@ -3632,6 +3640,7 @@ "l/ko/user-guide/data-model/how-tos/create-custom-objects", "l/ko/user-guide/data-model/how-tos/create-custom-fields", "l/ko/user-guide/data-model/how-tos/create-relation-fields", + "l/ko/user-guide/data-model/how-tos/create-many-to-many-relations", "l/ko/user-guide/data-model/how-tos/customize-your-data-model", "l/ko/user-guide/data-model/how-tos/data-model-faq" ] @@ -4076,6 +4085,7 @@ "l/pt/user-guide/data-model/how-tos/create-custom-objects", "l/pt/user-guide/data-model/how-tos/create-custom-fields", "l/pt/user-guide/data-model/how-tos/create-relation-fields", + "l/pt/user-guide/data-model/how-tos/create-many-to-many-relations", "l/pt/user-guide/data-model/how-tos/customize-your-data-model", "l/pt/user-guide/data-model/how-tos/data-model-faq" ] @@ -4520,6 +4530,7 @@ "l/ro/user-guide/data-model/how-tos/create-custom-objects", "l/ro/user-guide/data-model/how-tos/create-custom-fields", "l/ro/user-guide/data-model/how-tos/create-relation-fields", + "l/ro/user-guide/data-model/how-tos/create-many-to-many-relations", "l/ro/user-guide/data-model/how-tos/customize-your-data-model", "l/ro/user-guide/data-model/how-tos/data-model-faq" ] @@ -4964,6 +4975,7 @@ "l/ru/user-guide/data-model/how-tos/create-custom-objects", "l/ru/user-guide/data-model/how-tos/create-custom-fields", "l/ru/user-guide/data-model/how-tos/create-relation-fields", + "l/ru/user-guide/data-model/how-tos/create-many-to-many-relations", "l/ru/user-guide/data-model/how-tos/customize-your-data-model", "l/ru/user-guide/data-model/how-tos/data-model-faq" ] @@ -5408,6 +5420,7 @@ "l/tr/user-guide/data-model/how-tos/create-custom-objects", "l/tr/user-guide/data-model/how-tos/create-custom-fields", "l/tr/user-guide/data-model/how-tos/create-relation-fields", + "l/tr/user-guide/data-model/how-tos/create-many-to-many-relations", "l/tr/user-guide/data-model/how-tos/customize-your-data-model", "l/tr/user-guide/data-model/how-tos/data-model-faq" ] @@ -5852,6 +5865,7 @@ "l/zh/user-guide/data-model/how-tos/create-custom-objects", "l/zh/user-guide/data-model/how-tos/create-custom-fields", "l/zh/user-guide/data-model/how-tos/create-relation-fields", + "l/zh/user-guide/data-model/how-tos/create-many-to-many-relations", "l/zh/user-guide/data-model/how-tos/customize-your-data-model", "l/zh/user-guide/data-model/how-tos/data-model-faq" ]
packages/twenty-docs/navigation/base-structure.json+1 −0 modified@@ -52,6 +52,7 @@ "user-guide/data-model/how-tos/create-custom-objects", "user-guide/data-model/how-tos/create-custom-fields", "user-guide/data-model/how-tos/create-relation-fields", + "user-guide/data-model/how-tos/create-many-to-many-relations", "user-guide/data-model/how-tos/customize-your-data-model", "user-guide/data-model/how-tos/data-model-faq" ]
packages/twenty-docs/navigation/supported-languages.ts+7 −21 modified@@ -1,21 +1,7 @@ -export const DEFAULT_LANGUAGE = 'en' as const; - -export const SUPPORTED_LANGUAGES = [ - DEFAULT_LANGUAGE, - 'fr', - 'ar', - 'cs', - 'de', - 'es', - 'it', - 'ja', - 'ko', - 'pt', - 'ro', - 'ru', - 'tr', - 'zh', -] as const; - -export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]; - +// Re-export from twenty-shared (source of truth) +// Using relative path because these scripts run via tsx from workspace root +export { DOCUMENTATION_DEFAULT_LANGUAGE as DEFAULT_LANGUAGE } from '../../twenty-shared/src/constants/DocumentationDefaultLanguage'; +export { + DOCUMENTATION_SUPPORTED_LANGUAGES as SUPPORTED_LANGUAGES, + type DocumentationSupportedLanguage as SupportedLanguage, +} from '../../twenty-shared/src/constants/DocumentationSupportedLanguages';
packages/twenty-docs/package.json+3 −0 modified@@ -13,6 +13,9 @@ "dependencies": { "mintlify": "latest" }, + "devDependencies": { + "twenty-shared": "workspace:*" + }, "engines": { "node": "^24.5.0", "npm": "please-use-yarn",
packages/twenty-docs/scripts/generate-documentation-paths.ts+101 −0 added@@ -0,0 +1,101 @@ +import fs from 'fs'; +import path from 'path'; + +type BasePage = string | BaseGroup; + +type BaseGroup = { + key: string; + label: string; + icon?: string; + pages: BasePage[]; +}; + +type BaseTab = { + key: string; + label: string; + groups: BaseGroup[]; +}; + +type BaseStructure = { + tabs: BaseTab[]; +}; + +const baseStructurePath = path.resolve( + __dirname, + '../navigation/base-structure.json', +); + +const outputDir = path.resolve(__dirname, '../../twenty-shared/src/constants'); + +const baseStructure: BaseStructure = JSON.parse( + fs.readFileSync(baseStructurePath, 'utf8'), +); + +const extractPaths = (pages: BasePage[]): string[] => { + const paths: string[] = []; + + for (const page of pages) { + if (typeof page === 'string') { + paths.push(page); + } else { + paths.push(...extractPaths(page.pages)); + } + } + + return paths; +}; + +const pathToConstantName = (docPath: string): string => { + return docPath.replace(/^\//, '').replace(/[/-]/g, '_').toUpperCase(); +}; + +const allPaths: string[] = []; + +for (const tab of baseStructure.tabs) { + for (const group of tab.groups) { + allPaths.push(...extractPaths(group.pages)); + } +} + +const sortedPaths = [...allPaths].sort(); + +const AUTO_GENERATED_HEADER = `/* + * _____ _ + *|_ _|_ _____ _ __ | |_ _ _ + * | | \\ \\ /\\ / / _ \\ '_ \\| __| | | | Auto-generated file + * | | \\ V V / __/ | | | |_| |_| | DO NOT EDIT - changes will be overwritten + * |_| \\_/\\_/ \\___|_| |_|\\__|\\__, | Generated by: yarn docs:generate-paths + * |___/ + * + * This file is generated from packages/twenty-docs/navigation/base-structure.json + * To add new documentation pages, add them to base-structure.json and run: + * yarn docs:generate-paths + */ + +`; + +// Generate DocumentationPaths.ts +const pathEntries = sortedPaths + .map((docPath) => { + const constName = pathToConstantName(docPath); + const value = `'/${docPath}'`; + // Check if line would be too long (80 char limit) + if (` ${constName}: ${value},`.length > 80) { + return ` ${constName}:\n ${value},`; + } + return ` ${constName}: ${value},`; + }) + .join('\n'); + +const pathsContent = `${AUTO_GENERATED_HEADER}export const DOCUMENTATION_PATHS = { +${pathEntries} +} as const; + +export type DocumentationPath = + (typeof DOCUMENTATION_PATHS)[keyof typeof DOCUMENTATION_PATHS]; +`; + +fs.writeFileSync(path.join(outputDir, 'DocumentationPaths.ts'), pathsContent); + +console.log(`Generated documentation constants to ${outputDir}`); +console.log(`Total paths: ${sortedPaths.length}`);
packages/twenty-docs/user-guide/data-model/capabilities/relation-fields.mdx+7 −3 modified@@ -37,12 +37,16 @@ Many records in Object A can be linked to many records in Object B. **Example:** Many People can be linked to many Projects, and vice versa. -<Warning> -**Many-to-Many is not yet supported.** +Many-to-many relations use a **junction object** pattern: an intermediate object that connects both sides. With the junction relation feature, Twenty displays the final linked records directly, hiding the intermediate object from the UI. + +<img src="/images/user-guide/fields/junction-relation-diagram.png" style={{width:'100%'}}/> -This relation type is planned for H1 2026. As a workaround, create an intermediate "junction" object (e.g., "Project Assignments") that has Many-to-One relations to both objects. +<Warning> +**Lab Feature**: Junction relations must be enabled at **Settings → Updates → Lab** before use. </Warning> +See [How to Create Many-to-Many Relations](/user-guide/data-model/how-tos/create-many-to-many-relations) for a complete step-by-step guide. + ## Creating a Relation Field 1. Go to **Settings → Data Model**
packages/twenty-docs/user-guide/data-model/how-tos/create-many-to-many-relations.mdx+175 −0 added@@ -0,0 +1,175 @@ +--- +title: Create Many-to-Many Relations +description: Connect records where many items on both sides can be linked together using junction objects. +--- + +Many-to-many relations let you connect multiple records on both sides. For example: many People can work on many Projects, and each Project can have many People. + +<Warning> +**Lab Feature**: Junction relations are currently in the Lab. Enable them at **Settings → Updates → Lab** before following this guide. +</Warning> + +<Note> +This feature also requires **Advanced mode** to be enabled (toggle at the bottom right of Settings). +</Note> + +## When to Use Many-to-Many + +Use many-to-many when both sides of a relationship can have multiple connections: + +| Relationship | Example | +|-------------|---------| +| People ↔ Projects | A person works on multiple projects; a project has multiple team members | +| Companies ↔ Tags | A company can have multiple tags; a tag can apply to multiple companies | +| Products ↔ Orders | A product can be in multiple orders; an order contains multiple products | + +## How It Works + +Twenty uses a **junction object** pattern for many-to-many relations. A junction object sits between two objects and holds the connections: + +``` +People ←→ Project Assignments ←→ Projects +``` + +The **Project Assignments** object (junction) has: +- A relation to People (many-to-one) +- A relation to Projects (many-to-one) + +When you enable the junction relation toggle, Twenty displays linked records directly instead of showing the intermediate junction records. + +## Prerequisites + +1. **Enable Junction Relations in Lab**: Go to **Settings → Updates → Lab** and enable **Junction Relations** +2. **Enable Advanced mode**: Toggle on **Advanced mode** at the bottom right of the Settings sidebar +3. Plan your data model: + - Which two objects are you connecting? + - What should the junction object be called? + +## Step 1: Create the Junction Object + +First, create the intermediate object that will hold the connections. + +1. Go to **Settings → Data Model** +2. Click **+ New object** +3. Name it descriptively (e.g., "Project Assignment", "Team Member", "Product Order") +4. Click **Save** + +<Tip> +**Naming convention**: Use a name that describes the relationship, like "Project Assignment" or "Team Membership". This makes the data model easier to understand. +</Tip> + +## Step 2: Create Relations from the Junction Object + +Add relation fields from the junction object to both objects you want to connect. + +### First Relation (Junction → Object A) + +1. Select your junction object in **Settings → Data Model** +2. Click **+ Add Field** +3. Choose **Relation** as the field type +4. Select the first object (e.g., "People") +5. Set the relation type to **Many-to-One** (many assignments can link to one person) +6. Name the fields: + - Field on junction: e.g., "Person" + - Field on People: e.g., "Project Assignments" +7. Click **Save** + +### Second Relation (Junction → Object B) + +1. Still on the junction object, click **+ Add Field** +2. Choose **Relation** as the field type +3. Select the second object (e.g., "Projects") +4. Set the relation type to **Many-to-One** +5. Name the fields: + - Field on junction: e.g., "Project" + - Field on Projects: e.g., "Team Members" +6. Click **Save** + +## Step 3: Configure the Junction Relation Display + +Now configure the source objects to display linked records directly, skipping the intermediate junction object. + +1. Go to **Settings → Data Model** +2. Select the first object (e.g., "People") +3. Find the relation field pointing to the junction object (e.g., "Project Assignments") +4. Click to edit the field +5. Enable **"This is a relation to a Junction Object"** +6. Select the **Target relation** (e.g., "Project" — the field on the junction that points to the other side) +7. Click **Save** + + +{/* TODO: Add image +<img src="/images/user-guide/fields/junction-relation-toggle.png" style={{width:'100%'}}/> +*/} + +Repeat for the other object: +1. Select "Projects" in Data Model +2. Edit the "Team Members" relation field +3. Enable the junction toggle +4. Select "Person" as the target relation +5. Save + +## Result + +After configuration: + +- On a **Person** record, the "Project Assignments" field displays **Projects** directly (not assignment records) +- On a **Project** record, the "Team Members" field displays **People** directly + +The junction object still exists and stores the connections, but the UI presents a cleaner many-to-many view. + +## Example: People ↔ Projects + +Here's a complete walkthrough: + +### Create the Junction Object +- Name: **Project Assignment** +- Description: "Links people to projects they work on" + +### Add Relations +1. **Project Assignment → People** + - Type: Many-to-One + - Field on Assignment: "Person" + - Field on People: "Project Assignments" + +2. **Project Assignment → Projects** + - Type: Many-to-One + - Field on Assignment: "Project" + - Field on Projects: "Team Members" + +### Configure Junction Display +1. On **People** object: + - Edit "Project Assignments" field + - Enable junction toggle + - Target: "Project" + +2. On **Projects** object: + - Edit "Team Members" field + - Enable junction toggle + - Target: "Person" + +### Use It +- Open a Person record → See their Projects directly +- Open a Project record → See team members directly +- Create new connections from either side + +## Adding Extra Data to Connections + +Since the junction object is a real object, you can add custom fields to store information about the relationship: + +- **Role**: "Developer", "Designer", "Manager" +- **Start Date**: When they joined the project +- **Hours Allocated**: Weekly hours on this project + +To access this data, navigate to the junction object directly or query it via the API. + +## Limitations + +- **CSV Import/Export**: Importing many-to-many relations directly is not supported. Import records to the junction object instead. +- **Filters**: Filtering by many-to-many relations may have limited options. + +## Related + +- [Relation Fields](/user-guide/data-model/capabilities/relation-fields) — relation types explained +- [Create Custom Objects](/user-guide/data-model/how-tos/create-custom-objects) — how to create objects +- [Create Relation Fields](/user-guide/data-model/how-tos/create-relation-fields) — basic relation setup
packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentBase.tsx+6 −0 modified@@ -32,4 +32,10 @@ export const StyledSettingsOptionCardTitle = styled.div` export const StyledSettingsOptionCardDescription = styled.div` color: ${({ theme }) => theme.font.color.tertiary}; font-size: ${({ theme }) => theme.font.size.sm}; + + a { + position: relative; + z-index: 1; + pointer-events: auto; + } `;
packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentToggle.tsx+1 −1 modified@@ -41,7 +41,7 @@ const StyledSettingsOptionCardToggleCover = styled.span` type SettingsOptionCardContentToggleProps = { Icon?: IconComponent; title: React.ReactNode; - description?: string; + description?: React.ReactNode; divider?: boolean; disabled?: boolean; advancedMode?: boolean;
packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationForm.tsx+6 −0 modified@@ -55,6 +55,12 @@ export const settingsDataModelFieldMorphRelationFormSchema = z.object({ ), targetFieldLabel: z.string().min(1), iconOnDestination: z.string().min(1), + settings: z + .object({ + junctionTargetFieldId: z.string().optional(), + }) + .catchall(z.unknown()) + .optional(), }); export type SettingsDataModelFieldMorphRelationFormValues = z.infer<
packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationJunctionForm.tsx+54 −15 modified@@ -1,16 +1,19 @@ +import { Trans, useLingui } from '@lingui/react/macro'; import { useFormContext } from 'react-hook-form'; +import { useRecoilValue } from 'recoil'; +import { DOCUMENTATION_PATHS } from 'twenty-shared/constants'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { IconLink } from 'twenty-ui/display'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect'; import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; +import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl'; import { Select } from '@/ui/input/components/Select'; import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; -import { useLingui } from '@lingui/react/macro'; -import { useRecoilValue } from 'recoil'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { IconLink } from 'twenty-ui/display'; import { RelationType } from '~/generated-metadata/graphql'; import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; @@ -26,6 +29,12 @@ export const SettingsDataModelFieldRelationJunctionForm = ({ useFormContext<SettingsDataModelFieldEditFormValues>(); const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + + const documentationUrl = getDocumentationUrl({ + locale: currentWorkspaceMember?.locale, + path: DOCUMENTATION_PATHS.USER_GUIDE_DATA_MODEL_HOW_TOS_CREATE_MANY_TO_MANY_RELATIONS, + }); const { objectMetadataItem: sourceObjectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); @@ -34,7 +43,8 @@ export const SettingsDataModelFieldRelationJunctionForm = ({ const relationType = watch('relationType') ?? RelationType.ONE_TO_MANY; const targetObjectIds = watch('morphRelationObjectMetadataIds') ?? []; - const junctionTargetFieldId = watch('settings.junctionTargetFieldId'); + const currentSettings = watch('settings'); + const junctionTargetFieldId = currentSettings?.junctionTargetFieldId; // Only applies to ONE_TO_MANY with single target if ( @@ -112,31 +122,60 @@ export const SettingsDataModelFieldRelationJunctionForm = ({ const handleJunctionToggle = (checked: boolean) => { if (checked && junctionFieldOptions.length > 0) { setValue( - 'settings.junctionTargetFieldId', - junctionFieldOptions[0].value, + 'settings', + { + ...currentSettings, + junctionTargetFieldId: junctionFieldOptions[0].value, + }, { shouldDirty: true, }, ); } else { - setValue('settings.junctionTargetFieldId', undefined, { - shouldDirty: true, - }); + setValue( + 'settings', + { + ...currentSettings, + junctionTargetFieldId: undefined, + }, + { + shouldDirty: true, + }, + ); } }; const handleSelectionChange = (selectedValue: string) => { - setValue('settings.junctionTargetFieldId', selectedValue, { - shouldDirty: true, - }); + setValue( + 'settings', + { + ...currentSettings, + junctionTargetFieldId: selectedValue, + }, + { + shouldDirty: true, + }, + ); }; return ( <> <SettingsOptionCardContentToggle Icon={IconLink} title={t`This is a relation to a Junction Object`} - description={t`Will show linked records directly instead of intermediate junction record`} + description={ + <Trans> + Build many-to-many relations.{' '} + <a + href={documentationUrl} + target="_blank" + rel="noopener noreferrer" + style={{ textDecoration: 'underline', color: 'inherit' }} + > + Learn more + </a> + </Trans> + } checked={isJunctionConfigEnabled} onChange={handleJunctionToggle} divider={isJunctionConfigEnabled}
packages/twenty-front/src/modules/support/utils/getDocumentationUrl.ts+19 −8 modified@@ -1,15 +1,17 @@ -const DOCUMENTATION_BASE_URL = 'https://docs.twenty.com'; -const DOCUMENTATION_PATH = '/user-guide/introduction'; - -// Locales that have documentation translations available -const SUPPORTED_DOC_LOCALES = ['fr', 'pt', 'de', 'es', 'it', 'ja', 'ko', 'zh']; +import { + DOCUMENTATION_BASE_URL, + DOCUMENTATION_DEFAULT_LANGUAGE, + DOCUMENTATION_DEFAULT_PATH, + DOCUMENTATION_SUPPORTED_LANGUAGES, + type DocumentationPath, +} from 'twenty-shared/constants'; export const getDocumentationUrl = ({ locale, - path = DOCUMENTATION_PATH, + path = DOCUMENTATION_DEFAULT_PATH, }: { locale?: string | null; - path?: string; + path?: DocumentationPath | string; }): string => { if (!locale) { return `${DOCUMENTATION_BASE_URL}${path}`; @@ -18,7 +20,16 @@ export const getDocumentationUrl = ({ // Extract language code from locale (e.g., 'fr' from 'fr-FR') const langCode = locale.split('-')[0].toLowerCase(); - if (SUPPORTED_DOC_LOCALES.includes(langCode)) { + // English content is served at root path (no /l/en/ prefix) + if (langCode === DOCUMENTATION_DEFAULT_LANGUAGE) { + return `${DOCUMENTATION_BASE_URL}${path}`; + } + + const isSupported = DOCUMENTATION_SUPPORTED_LANGUAGES.includes( + langCode as (typeof DOCUMENTATION_SUPPORTED_LANGUAGES)[number], + ); + + if (isSupported) { return `${DOCUMENTATION_BASE_URL}/l/${langCode}${path}`; }
packages/twenty-shared/src/constants/DocumentationBaseUrl.ts+1 −0 added@@ -0,0 +1 @@ +export const DOCUMENTATION_BASE_URL = 'https://docs.twenty.com';
packages/twenty-shared/src/constants/DocumentationDefaultLanguage.ts+1 −0 added@@ -0,0 +1 @@ +export const DOCUMENTATION_DEFAULT_LANGUAGE = 'en' as const;
packages/twenty-shared/src/constants/DocumentationDefaultPath.ts+4 −0 added@@ -0,0 +1,4 @@ +import { DOCUMENTATION_PATHS } from './DocumentationPaths'; + +export const DOCUMENTATION_DEFAULT_PATH = + DOCUMENTATION_PATHS.USER_GUIDE_INTRODUCTION;
packages/twenty-shared/src/constants/DocumentationPaths.ts+298 −0 added@@ -0,0 +1,298 @@ +/* + * _____ _ + *|_ _|_ _____ _ __ | |_ _ _ + * | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file + * | | \ V V / __/ | | | |_| |_| | DO NOT EDIT - changes will be overwritten + * |_| \_/\_/ \___|_| |_|\__|\__, | Generated by: yarn docs:generate-paths + * |___/ + * + * This file is generated from packages/twenty-docs/navigation/base-structure.json + * To add new documentation pages, add them to base-structure.json and run: + * yarn docs:generate-paths + */ + +export const DOCUMENTATION_PATHS = { + DEVELOPERS_CONTRIBUTE_CAPABILITIES_BACKEND_DEVELOPMENT_BEST_PRACTICES_SERVER: + '/developers/contribute/capabilities/backend-development/best-practices-server', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_BACKEND_DEVELOPMENT_CUSTOM_OBJECTS: + '/developers/contribute/capabilities/backend-development/custom-objects', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_BACKEND_DEVELOPMENT_FEATURE_FLAGS: + '/developers/contribute/capabilities/backend-development/feature-flags', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_BACKEND_DEVELOPMENT_FOLDER_ARCHITECTURE_SERVER: + '/developers/contribute/capabilities/backend-development/folder-architecture-server', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_BACKEND_DEVELOPMENT_QUEUE: + '/developers/contribute/capabilities/backend-development/queue', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_BACKEND_DEVELOPMENT_SERVER_COMMANDS: + '/developers/contribute/capabilities/backend-development/server-commands', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_BACKEND_DEVELOPMENT_ZAPIER: + '/developers/contribute/capabilities/backend-development/zapier', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_BUG_AND_REQUESTS: + '/developers/contribute/capabilities/bug-and-requests', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_FRONTEND_DEVELOPMENT_BEST_PRACTICES_FRONT: + '/developers/contribute/capabilities/frontend-development/best-practices-front', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_FRONTEND_DEVELOPMENT_FOLDER_ARCHITECTURE_FRONT: + '/developers/contribute/capabilities/frontend-development/folder-architecture-front', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_FRONTEND_DEVELOPMENT_FRONTEND_COMMANDS: + '/developers/contribute/capabilities/frontend-development/frontend-commands', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_FRONTEND_DEVELOPMENT_HOTKEYS: + '/developers/contribute/capabilities/frontend-development/hotkeys', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_FRONTEND_DEVELOPMENT_STORYBOOK: + '/developers/contribute/capabilities/frontend-development/storybook', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_FRONTEND_DEVELOPMENT_STYLE_GUIDE: + '/developers/contribute/capabilities/frontend-development/style-guide', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_FRONTEND_DEVELOPMENT_WORK_WITH_FIGMA: + '/developers/contribute/capabilities/frontend-development/work-with-figma', + DEVELOPERS_CONTRIBUTE_CAPABILITIES_LOCAL_SETUP: + '/developers/contribute/capabilities/local-setup', + DEVELOPERS_CONTRIBUTE_CONTRIBUTE: '/developers/contribute/contribute', + DEVELOPERS_EXTEND_CAPABILITIES_APIS: '/developers/extend/capabilities/apis', + DEVELOPERS_EXTEND_CAPABILITIES_APPS: '/developers/extend/capabilities/apps', + DEVELOPERS_EXTEND_CAPABILITIES_WEBHOOKS: + '/developers/extend/capabilities/webhooks', + DEVELOPERS_EXTEND_EXTEND: '/developers/extend/extend', + DEVELOPERS_INTRODUCTION: '/developers/introduction', + DEVELOPERS_SELF_HOST_CAPABILITIES_CLOUD_PROVIDERS: + '/developers/self-host/capabilities/cloud-providers', + DEVELOPERS_SELF_HOST_CAPABILITIES_DOCKER_COMPOSE: + '/developers/self-host/capabilities/docker-compose', + DEVELOPERS_SELF_HOST_CAPABILITIES_SETUP: + '/developers/self-host/capabilities/setup', + DEVELOPERS_SELF_HOST_CAPABILITIES_TROUBLESHOOTING: + '/developers/self-host/capabilities/troubleshooting', + DEVELOPERS_SELF_HOST_CAPABILITIES_UPGRADE_GUIDE: + '/developers/self-host/capabilities/upgrade-guide', + DEVELOPERS_SELF_HOST_SELF_HOST: '/developers/self-host/self-host', + TWENTY_UI_DISPLAY_APP_TOOLTIP: '/twenty-ui/display/app-tooltip', + TWENTY_UI_DISPLAY_CHECKMARK: '/twenty-ui/display/checkmark', + TWENTY_UI_DISPLAY_CHIP: '/twenty-ui/display/chip', + TWENTY_UI_DISPLAY_ICONS: '/twenty-ui/display/icons', + TWENTY_UI_DISPLAY_SOON_PILL: '/twenty-ui/display/soon-pill', + TWENTY_UI_DISPLAY_TAG: '/twenty-ui/display/tag', + TWENTY_UI_INPUT_BLOCK_EDITOR: '/twenty-ui/input/block-editor', + TWENTY_UI_INPUT_BUTTONS: '/twenty-ui/input/buttons', + TWENTY_UI_INPUT_CHECKBOX: '/twenty-ui/input/checkbox', + TWENTY_UI_INPUT_COLOR_SCHEME: '/twenty-ui/input/color-scheme', + TWENTY_UI_INPUT_ICON_PICKER: '/twenty-ui/input/icon-picker', + TWENTY_UI_INPUT_IMAGE_INPUT: '/twenty-ui/input/image-input', + TWENTY_UI_INPUT_RADIO: '/twenty-ui/input/radio', + TWENTY_UI_INPUT_SELECT: '/twenty-ui/input/select', + TWENTY_UI_INPUT_TEXT: '/twenty-ui/input/text', + TWENTY_UI_INPUT_TOGGLE: '/twenty-ui/input/toggle', + TWENTY_UI_INTRODUCTION: '/twenty-ui/introduction', + TWENTY_UI_NAVIGATION: '/twenty-ui/navigation', + TWENTY_UI_NAVIGATION_BREADCRUMB: '/twenty-ui/navigation/breadcrumb', + TWENTY_UI_NAVIGATION_LINKS: '/twenty-ui/navigation/links', + TWENTY_UI_NAVIGATION_MENU_ITEM: '/twenty-ui/navigation/menu-item', + TWENTY_UI_NAVIGATION_NAVIGATION_BAR: '/twenty-ui/navigation/navigation-bar', + TWENTY_UI_NAVIGATION_STEP_BAR: '/twenty-ui/navigation/step-bar', + TWENTY_UI_PROGRESS_BAR: '/twenty-ui/progress-bar', + USER_GUIDE_AI_CAPABILITIES_AI_AGENTS: '/user-guide/ai/capabilities/ai-agents', + USER_GUIDE_AI_CAPABILITIES_AI_CHATBOT: + '/user-guide/ai/capabilities/ai-chatbot', + USER_GUIDE_AI_CAPABILITIES_PERMISSIONS_ACCESS_CONTROL: + '/user-guide/ai/capabilities/permissions-access-control', + USER_GUIDE_AI_HOW_TOS_AI_FAQ: '/user-guide/ai/how-tos/ai-faq', + USER_GUIDE_AI_OVERVIEW: '/user-guide/ai/overview', + USER_GUIDE_BILLING_CAPABILITIES_PRICING_PLANS: + '/user-guide/billing/capabilities/pricing-plans', + USER_GUIDE_BILLING_CAPABILITIES_WORKFLOW_CREDITS: + '/user-guide/billing/capabilities/workflow-credits', + USER_GUIDE_BILLING_HOW_TOS_BILLING_FAQ: + '/user-guide/billing/how-tos/billing-faq', + USER_GUIDE_BILLING_OVERVIEW: '/user-guide/billing/overview', + USER_GUIDE_CALENDAR_EMAILS_CAPABILITIES_CALENDAR: + '/user-guide/calendar-emails/capabilities/calendar', + USER_GUIDE_CALENDAR_EMAILS_CAPABILITIES_MAILBOX: + '/user-guide/calendar-emails/capabilities/mailbox', + USER_GUIDE_CALENDAR_EMAILS_HOW_TOS_CAN_I_BOOK_MEETINGS_FROM_TWENTY: + '/user-guide/calendar-emails/how-tos/can-i-book-meetings-from-twenty', + USER_GUIDE_CALENDAR_EMAILS_HOW_TOS_CAN_I_SEND_EMAILS_FROM_TWENTY: + '/user-guide/calendar-emails/how-tos/can-i-send-emails-from-twenty', + USER_GUIDE_CALENDAR_EMAILS_HOW_TOS_CAN_I_TRACK_EMAIL_ACTIVITY_ON_ALL_OBJECTS: + '/user-guide/calendar-emails/how-tos/can-i-track-email-activity-on-all-objects', + USER_GUIDE_CALENDAR_EMAILS_HOW_TOS_CONNECT_SEVERAL_MAILBOXES_PER_USER: + '/user-guide/calendar-emails/how-tos/connect-several-mailboxes-per-user', + USER_GUIDE_CALENDAR_EMAILS_HOW_TOS_I_DONT_SEE_EMAILS_ON_RECORDS: + '/user-guide/calendar-emails/how-tos/i-dont-see-emails-on-records', + USER_GUIDE_CALENDAR_EMAILS_HOW_TOS_LIMIT_EMAILS_IMPORTED: + '/user-guide/calendar-emails/how-tos/limit-emails-imported', + USER_GUIDE_CALENDAR_EMAILS_OVERVIEW: '/user-guide/calendar-emails/overview', + USER_GUIDE_DASHBOARDS_CAPABILITIES_CHART_SETTINGS: + '/user-guide/dashboards/capabilities/chart-settings', + USER_GUIDE_DASHBOARDS_CAPABILITIES_DASHBOARDS: + '/user-guide/dashboards/capabilities/dashboards', + USER_GUIDE_DASHBOARDS_CAPABILITIES_WIDGETS: + '/user-guide/dashboards/capabilities/widgets', + USER_GUIDE_DASHBOARDS_HOW_TOS_DASHBOARDS_FAQ: + '/user-guide/dashboards/how-tos/dashboards-faq', + USER_GUIDE_DASHBOARDS_HOW_TOS_WIDGET_FAQ: + '/user-guide/dashboards/how-tos/widget-faq', + USER_GUIDE_DASHBOARDS_OVERVIEW: '/user-guide/dashboards/overview', + USER_GUIDE_DATA_MIGRATION_CAPABILITIES_ERROR_HANDLING: + '/user-guide/data-migration/capabilities/error-handling', + USER_GUIDE_DATA_MIGRATION_CAPABILITIES_FIELD_MAPPING: + '/user-guide/data-migration/capabilities/field-mapping', + USER_GUIDE_DATA_MIGRATION_CAPABILITIES_FILE_FORMATS: + '/user-guide/data-migration/capabilities/file-formats', + USER_GUIDE_DATA_MIGRATION_CAPABILITIES_IMPORT_RELATIONS: + '/user-guide/data-migration/capabilities/import-relations', + USER_GUIDE_DATA_MIGRATION_CAPABILITIES_UNIQUENESS_CONSTRAINTS: + '/user-guide/data-migration/capabilities/uniqueness-constraints', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_EXPORT_YOUR_DATA: + '/user-guide/data-migration/how-tos/export-your-data', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_FIX_IMPORT_ERRORS: + '/user-guide/data-migration/how-tos/fix-import-errors', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_IMPORT_COMPANIES_VIA_CSV: + '/user-guide/data-migration/how-tos/import-companies-via-csv', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_IMPORT_CONTACTS_VIA_CSV: + '/user-guide/data-migration/how-tos/import-contacts-via-csv', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_IMPORT_DATA_VIA_API: + '/user-guide/data-migration/how-tos/import-data-via-api', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_IMPORT_RELATIONS_BETWEEN_OBJECTS_VIA_CSV: + '/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_MIGRATING_FROM_OTHER_CRMS: + '/user-guide/data-migration/how-tos/migrating-from-other-crms', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_MIGRATING_FROM_SELF_HOSTED_TO_CLOUD: + '/user-guide/data-migration/how-tos/migrating-from-self-hosted-to-cloud', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_PREPARE_YOUR_CSV_FILES: + '/user-guide/data-migration/how-tos/prepare-your-csv-files', + USER_GUIDE_DATA_MIGRATION_HOW_TOS_UPDATE_EXISTING_RECORDS_VIA_IMPORT: + '/user-guide/data-migration/how-tos/update-existing-records-via-import', + USER_GUIDE_DATA_MIGRATION_OVERVIEW: '/user-guide/data-migration/overview', + USER_GUIDE_DATA_MODEL_CAPABILITIES_FIELDS: + '/user-guide/data-model/capabilities/fields', + USER_GUIDE_DATA_MODEL_CAPABILITIES_OBJECTS: + '/user-guide/data-model/capabilities/objects', + USER_GUIDE_DATA_MODEL_CAPABILITIES_RELATION_FIELDS: + '/user-guide/data-model/capabilities/relation-fields', + USER_GUIDE_DATA_MODEL_HOW_TOS_CREATE_CUSTOM_FIELDS: + '/user-guide/data-model/how-tos/create-custom-fields', + USER_GUIDE_DATA_MODEL_HOW_TOS_CREATE_CUSTOM_OBJECTS: + '/user-guide/data-model/how-tos/create-custom-objects', + USER_GUIDE_DATA_MODEL_HOW_TOS_CREATE_MANY_TO_MANY_RELATIONS: + '/user-guide/data-model/how-tos/create-many-to-many-relations', + USER_GUIDE_DATA_MODEL_HOW_TOS_CREATE_RELATION_FIELDS: + '/user-guide/data-model/how-tos/create-relation-fields', + USER_GUIDE_DATA_MODEL_HOW_TOS_CUSTOMIZE_YOUR_DATA_MODEL: + '/user-guide/data-model/how-tos/customize-your-data-model', + USER_GUIDE_DATA_MODEL_HOW_TOS_DATA_MODEL_FAQ: + '/user-guide/data-model/how-tos/data-model-faq', + USER_GUIDE_DATA_MODEL_OVERVIEW: '/user-guide/data-model/overview', + USER_GUIDE_GETTING_STARTED_CAPABILITIES_GLOSSARY: + '/user-guide/getting-started/capabilities/glossary', + USER_GUIDE_GETTING_STARTED_CAPABILITIES_IMPLEMENTATION_SERVICES: + '/user-guide/getting-started/capabilities/implementation-services', + USER_GUIDE_GETTING_STARTED_CAPABILITIES_WHAT_IS_TWENTY: + '/user-guide/getting-started/capabilities/what-is-twenty', + USER_GUIDE_GETTING_STARTED_HOW_TOS_CONFIGURE_YOUR_WORKSPACE: + '/user-guide/getting-started/how-tos/configure-your-workspace', + USER_GUIDE_GETTING_STARTED_HOW_TOS_CREATE_WORKSPACE: + '/user-guide/getting-started/how-tos/create-workspace', + USER_GUIDE_GETTING_STARTED_HOW_TOS_NAVIGATE_AROUND_TWENTY: + '/user-guide/getting-started/how-tos/navigate-around-twenty', + USER_GUIDE_INTRODUCTION: '/user-guide/introduction', + USER_GUIDE_PERMISSIONS_ACCESS_CAPABILITIES_PERMISSIONS: + '/user-guide/permissions-access/capabilities/permissions', + USER_GUIDE_PERMISSIONS_ACCESS_CAPABILITIES_SSO_CONFIGURATION: + '/user-guide/permissions-access/capabilities/sso-configuration', + USER_GUIDE_PERMISSIONS_ACCESS_HOW_TOS_PERMISSIONS_FAQ: + '/user-guide/permissions-access/how-tos/permissions-faq', + USER_GUIDE_PERMISSIONS_ACCESS_OVERVIEW: + '/user-guide/permissions-access/overview', + USER_GUIDE_SETTINGS_CAPABILITIES_DOMAINS_SETTINGS: + '/user-guide/settings/capabilities/domains-settings', + USER_GUIDE_SETTINGS_CAPABILITIES_EXPERIENCE_SETTINGS: + '/user-guide/settings/capabilities/experience-settings', + USER_GUIDE_SETTINGS_CAPABILITIES_MEMBER_MANAGEMENT: + '/user-guide/settings/capabilities/member-management', + USER_GUIDE_SETTINGS_CAPABILITIES_PROFILE_SETTINGS: + '/user-guide/settings/capabilities/profile-settings', + USER_GUIDE_SETTINGS_CAPABILITIES_UPDATES_SETTINGS: + '/user-guide/settings/capabilities/updates-settings', + USER_GUIDE_SETTINGS_CAPABILITIES_WORKSPACE_SETTINGS: + '/user-guide/settings/capabilities/workspace-settings', + USER_GUIDE_SETTINGS_HOW_TOS_SETTINGS_FAQ: + '/user-guide/settings/how-tos/settings-faq', + USER_GUIDE_SETTINGS_OVERVIEW: '/user-guide/settings/overview', + USER_GUIDE_VIEWS_PIPELINES_CAPABILITIES_CALENDAR_VIEW: + '/user-guide/views-pipelines/capabilities/calendar-view', + USER_GUIDE_VIEWS_PIPELINES_CAPABILITIES_FIELDS_AND_COLUMNS: + '/user-guide/views-pipelines/capabilities/fields-and-columns', + USER_GUIDE_VIEWS_PIPELINES_CAPABILITIES_FILTERS_AND_SORTING: + '/user-guide/views-pipelines/capabilities/filters-and-sorting', + USER_GUIDE_VIEWS_PIPELINES_CAPABILITIES_KANBAN_VIEWS: + '/user-guide/views-pipelines/capabilities/kanban-views', + USER_GUIDE_VIEWS_PIPELINES_CAPABILITIES_TABLE_VIEWS: + '/user-guide/views-pipelines/capabilities/table-views', + USER_GUIDE_VIEWS_PIPELINES_CAPABILITIES_VIEW_SETTINGS: + '/user-guide/views-pipelines/capabilities/view-settings', + USER_GUIDE_VIEWS_PIPELINES_HOW_TOS_CREATE_A_CALENDAR_VIEW_FOR_TASKS_DUE: + '/user-guide/views-pipelines/how-tos/create-a-calendar-view-for-tasks-due', + USER_GUIDE_VIEWS_PIPELINES_HOW_TOS_CREATE_A_KANBAN_VIEW_FOR_PROJECTS: + '/user-guide/views-pipelines/how-tos/create-a-kanban-view-for-projects', + USER_GUIDE_VIEWS_PIPELINES_HOW_TOS_CREATE_A_TABLE_VIEW_WITH_GROUPING: + '/user-guide/views-pipelines/how-tos/create-a-table-view-with-grouping', + USER_GUIDE_VIEWS_PIPELINES_HOW_TOS_RESTRICT_ACCESS_TO_YOUR_VIEW: + '/user-guide/views-pipelines/how-tos/restrict-access-to-your-view', + USER_GUIDE_VIEWS_PIPELINES_HOW_TOS_SET_UP_A_SALES_PIPELINE: + '/user-guide/views-pipelines/how-tos/set-up-a-sales-pipeline', + USER_GUIDE_VIEWS_PIPELINES_HOW_TOS_SHOW_EXPECTED_AMOUNT_IN_PIPELINE: + '/user-guide/views-pipelines/how-tos/show-expected-amount-in-pipeline', + USER_GUIDE_VIEWS_PIPELINES_HOW_TOS_TRACK_TIME_IN_STAGE: + '/user-guide/views-pipelines/how-tos/track-time-in-stage', + USER_GUIDE_VIEWS_PIPELINES_OVERVIEW: '/user-guide/views-pipelines/overview', + USER_GUIDE_WORKFLOWS_CAPABILITIES_SEND_EMAILS_FROM_WORKFLOWS: + '/user-guide/workflows/capabilities/send-emails-from-workflows', + USER_GUIDE_WORKFLOWS_CAPABILITIES_USE_BRANCHES_IN_WORKFLOWS: + '/user-guide/workflows/capabilities/use-branches-in-workflows', + USER_GUIDE_WORKFLOWS_CAPABILITIES_USE_ITERATOR: + '/user-guide/workflows/capabilities/use-iterator', + USER_GUIDE_WORKFLOWS_CAPABILITIES_WORKFLOW_ACTIONS: + '/user-guide/workflows/capabilities/workflow-actions', + USER_GUIDE_WORKFLOWS_CAPABILITIES_WORKFLOW_BRANCHES: + '/user-guide/workflows/capabilities/workflow-branches', + USER_GUIDE_WORKFLOWS_CAPABILITIES_WORKFLOW_CREDITS: + '/user-guide/workflows/capabilities/workflow-credits', + USER_GUIDE_WORKFLOWS_CAPABILITIES_WORKFLOW_RUNS: + '/user-guide/workflows/capabilities/workflow-runs', + USER_GUIDE_WORKFLOWS_CAPABILITIES_WORKFLOW_TRIGGERS: + '/user-guide/workflows/capabilities/workflow-triggers', + USER_GUIDE_WORKFLOWS_CAPABILITIES_WORKFLOW_VERSIONS: + '/user-guide/workflows/capabilities/workflow-versions', + USER_GUIDE_WORKFLOWS_HOW_TOS_ADVANCED_CONFIGURATIONS_HANDLE_ARRAYS_IN_CODE_ACTIONS: + '/user-guide/workflows/how-tos/advanced-configurations/handle-arrays-in-code-actions', + USER_GUIDE_WORKFLOWS_HOW_TOS_CONNECT_TO_OTHER_TOOLS_BRING_PRODUCT_DATA_IN_TWENTY: + '/user-guide/workflows/how-tos/connect-to-other-tools/bring-product-data-in-twenty', + USER_GUIDE_WORKFLOWS_HOW_TOS_CONNECT_TO_OTHER_TOOLS_BRING_TYPEFORM_SUBMISSIONS_IN_TWENTY: + '/user-guide/workflows/how-tos/connect-to-other-tools/bring-typeform-submissions-in-twenty', + USER_GUIDE_WORKFLOWS_HOW_TOS_CONNECT_TO_OTHER_TOOLS_GENERATE_PDF_FROM_TWENTY: + '/user-guide/workflows/how-tos/connect-to-other-tools/generate-pdf-from-twenty', + USER_GUIDE_WORKFLOWS_HOW_TOS_CONNECT_TO_OTHER_TOOLS_GENERATE_QUOTE_OR_INVOICE_FROM_TWENTY: + '/user-guide/workflows/how-tos/connect-to-other-tools/generate-quote-or-invoice-from-twenty', + USER_GUIDE_WORKFLOWS_HOW_TOS_CONNECT_TO_OTHER_TOOLS_SET_UP_A_WEBHOOK_TRIGGER: + '/user-guide/workflows/how-tos/connect-to-other-tools/set-up-a-webhook-trigger', + USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_CLOSED_WON_AUTOMATIONS: + '/user-guide/workflows/how-tos/crm-automations/closed-won-automations', + USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_DETECT_STALE_OPPORTUNITIES: + '/user-guide/workflows/how-tos/crm-automations/detect-stale-opportunities', + USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_DISPLAY_NUMBER_OF_EMAILS_RECEIVED: + '/user-guide/workflows/how-tos/crm-automations/display-number-of-emails-received', + USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_DISPLAY_RELATED_RECORD_DATA: + '/user-guide/workflows/how-tos/crm-automations/display-related-record-data', + USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_FORMULA_FIELDS: + '/user-guide/workflows/how-tos/crm-automations/formula-fields', + USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_NOTIFY_TEAMMATES_OF_NOTE_TO_REVIEW: + '/user-guide/workflows/how-tos/crm-automations/notify-teammates-of-note-to-review', + USER_GUIDE_WORKFLOWS_HOW_TOS_CRM_AUTOMATIONS_SEND_EMAIL_ALERTS_WITH_TASKS_DUE: + '/user-guide/workflows/how-tos/crm-automations/send-email-alerts-with-tasks-due', + USER_GUIDE_WORKFLOWS_HOW_TOS_NEED_MORE_HELP_PROFESSIONAL_SERVICES: + '/user-guide/workflows/how-tos/need-more-help/professional-services', + USER_GUIDE_WORKFLOWS_HOW_TOS_NEED_MORE_HELP_WORKFLOW_TROUBLESHOOTING: + '/user-guide/workflows/how-tos/need-more-help/workflow-troubleshooting', + USER_GUIDE_WORKFLOWS_HOW_TOS_NEED_MORE_HELP_WORKFLOWS_FAQ: + '/user-guide/workflows/how-tos/need-more-help/workflows-faq', + USER_GUIDE_WORKFLOWS_OVERVIEW: '/user-guide/workflows/overview', +} as const; + +export type DocumentationPath = + (typeof DOCUMENTATION_PATHS)[keyof typeof DOCUMENTATION_PATHS];
packages/twenty-shared/src/constants/DocumentationSupportedLanguages.ts+21 −0 added@@ -0,0 +1,21 @@ +import { DOCUMENTATION_DEFAULT_LANGUAGE } from './DocumentationDefaultLanguage'; + +export const DOCUMENTATION_SUPPORTED_LANGUAGES = [ + DOCUMENTATION_DEFAULT_LANGUAGE, + 'fr', + 'ar', + 'cs', + 'de', + 'es', + 'it', + 'ja', + 'ko', + 'pt', + 'ro', + 'ru', + 'tr', + 'zh', +] as const; + +export type DocumentationSupportedLanguage = + (typeof DOCUMENTATION_SUPPORTED_LANGUAGES)[number];
packages/twenty-shared/src/constants/index.ts+7 −0 modified@@ -14,6 +14,13 @@ export { CURRENCY_CODE_LABELS } from './CurrencyCodeLabels'; export { DATE_TYPE_FORMAT } from './DateTypeFormat'; export { DEFAULT_NUMBER_OF_GROUPS_LIMIT } from './DefaultNumberOfGroupsLimit'; export { DEFAULT_RELATIVE_DATE_FILTER_VALUE } from './DefaultRelativeDateFilterValue'; +export { DOCUMENTATION_BASE_URL } from './DocumentationBaseUrl'; +export { DOCUMENTATION_DEFAULT_LANGUAGE } from './DocumentationDefaultLanguage'; +export { DOCUMENTATION_DEFAULT_PATH } from './DocumentationDefaultPath'; +export type { DocumentationPath } from './DocumentationPaths'; +export { DOCUMENTATION_PATHS } from './DocumentationPaths'; +export type { DocumentationSupportedLanguage } from './DocumentationSupportedLanguages'; +export { DOCUMENTATION_SUPPORTED_LANGUAGES } from './DocumentationSupportedLanguages'; export { FIELD_FOR_TOTAL_COUNT_AGGREGATE_OPERATION } from './FieldForTotalCountAggregateOperation'; export { MAX_OPTIONS_TO_DISPLAY } from './FieldMetadataMaxOptionsToDisplay'; export { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from './FieldRestrictedAdditionalPermissionsRequired';
packages/twenty-shared/src/utils/validation/isValidLocale.ts+4 −1 modified@@ -1,4 +1,7 @@ -import { APP_LOCALES, type AppLocale } from '@/translations/constants/AppLocales'; +import { + APP_LOCALES, + type AppLocale, +} from '@/translations/constants/AppLocales'; export const isValidLocale = (value: string | null): value is AppLocale => value !== null && value in APP_LOCALES;
yarn.lock+1 −0 modified@@ -57297,6 +57297,7 @@ __metadata: resolution: "twenty-docs@workspace:packages/twenty-docs" dependencies: mintlify: "npm:latest" + twenty-shared: "workspace:*" languageName: unknown linkType: soft
Vulnerability mechanics
Root cause
"Missing input validation and sanitization on the timeZone parameter allows SQL injection via direct template-literal interpolation into raw SQL expressions."
Attack vector
An authenticated attacker sends a crafted REST API request to the groupBy endpoint with a malicious timeZone value in the group_by query parameter. The timeZone field is directly interpolated into a SQL expression without parameterization, validation, or escaping [ref_id=1]. The attacker can break out of the SQL string context and inject stacked queries. If the PostgreSQL user is a superuser (the default in Twenty CRM's Docker deployment), the attacker can chain the SQL injection with PostgreSQL's COPY TO PROGRAM feature to execute arbitrary OS commands on the database server [ref_id=1]. No administrator role is required; any authenticated user can exploit this vulnerability [ref_id=1].
Affected code
The vulnerable file is `packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util.ts` [ref_id=1]. The timeZone field from the group_by query parameter is directly interpolated into raw SQL expressions using JavaScript template literals without any parameterization, validation, or escaping [ref_id=1].
What the fix does
The provided patch [patch_id=2567065] does not address the SQL injection vulnerability. Its commit message focuses on fixing junction relation toggle persistence, adding type-safe documentation path constants, and creating many-to-many relations documentation. The advisory [ref_id=1] states that the vulnerable code resides in `packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util.ts`, where the timeZone parameter is interpolated directly into SQL. No fix for this specific vulnerability is present in the supplied patch. The advisory does not specify whether a separate fix has been published.
Preconditions
- authAttacker must be an authenticated user of the Twenty CRM instance
- configPostgreSQL user must be a superuser (default in Twenty CRM's Docker deployment)
- configTarget must be running Twenty CRM version 1.7.7 through 1.16.7
- networkAttacker must be able to reach the REST API groupBy endpoint over the network
Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.