Compare commits

...
Author SHA1 Message Date
c2b058a6a7 fix: use workspace-generated id for core dual-write in message folder save (#19038)
Co-authored-by: Charles Bochet <[email protected]>
2026-03-27 13:53:30 +01:00
Abdullah.andGitHub 22c9693ce5 First PR to bring in the new twenty website. (#19035)
This PR contains Menu, Hero, TrustedBy, Problem, ThreeCards and Footer
sections of the new website.

Most components in there match the Figma designs, except for two things.
- Zoom levels on 3D illustrations from Endless Tools.
- Menu needs to have the same color as Hero - it's not happening at the
moment since Menu is in the layout, not nested inside pages or Hero.

Images are placeholders (same as Figma).
2026-03-27 13:48:03 +01:00
50ea560e57 Seed company workflow for email upserts (#18909)
@thomtrp

---------

Co-authored-by: Charles Bochet <[email protected]>
2026-03-27 13:47:07 +01:00
EtienneandGitHub 56056b885d refacto - remove hello-pangea/dnd from navigation-menu-item module (#19033)
Removed all @hello-pangea/dnd imports from the navigation-menu-item/
module by replacing the type bridge layer with a native
NavigationMenuItemDropResult type. The dnd-kit events were already
powering all DnD — hello-pangea was only used as a type contract and one
dead <Droppable> component.
3 files deleted (dead <Droppable> wrapper, DROP_RESULT_OPTIONS shim,
toDropResult bridge), 4 files edited (handler signatures simplified,
bridge removed from orchestrator, utility type narrowed), 1 type
created. Zero behavioral changes, typecheck and tests pass.
2026-03-27 11:23:25 +00:00
Paul RastoinandGitHub db9da3194f Refactor client auto heal (#19032) 2026-03-27 09:23:26 +00:00
BugIsGodandGitHub 68f5e70ade fix: sign file URLs in timeline and file loss on click outside (#19001)
Fixes: #18943
## Problem

  Two bugs related to the Files field:

1. **File loss on click outside**: When `MultiItemFieldInput` was not in
edit mode (input hidden), clicking outside would still call
`validateInputAndComputeUpdatedItems()` with an empty `inputValue` and
`itemToEditIndex = 0`, causing the first file to be silently deleted.
<img width="624" height="332" alt="image"
src="https://github.com/user-attachments/assets/657aff2c-e497-4685-b8b7-fa22aba772f8"
/>


2. **Unsigned file URLs in timeline activity**: FileId, FileName stored
in `timelineActivity.properties.diff` (before/after values) were not
being signed (timeActivity.properties is stored as json in database),
making them inaccessible from the
frontend.

<img width="1092" height="103" alt="image"
src="https://github.com/user-attachments/assets/23d83ea3-4eb9-41ef-a99c-c4515d1cfb35"
/>


 ## Reproduction


https://github.com/user-attachments/assets/e75b842b-5cbb-46e8-a923-ac9df62deb98

## Changes
- `MultiItemFieldInput.tsx`: Wrap the validate + onChange logic in `if
(isInputDisplayed)` so it only runs when the input is actually open.
- `timeline-activity-query-result-getter.handler.ts`: New handler that
iterates over `properties.diff` fields and signs any file arrays found
in `before`/`after` values (call same method:
`fileUrlService.signFileByIdUrl` as table field view
`FilesFieldQueryResultGetterHandler`.
- `common-result-getters.service.ts`: Register the new handler for
`timelineActivity`.


## After 


https://github.com/user-attachments/assets/368c7be1-3101-43a2-ac93-fe1b0d8a7a37
2026-03-27 08:28:30 +00:00
Félix MalfaitandGitHub 08077476f3 fix: remove remaining direct cookie writes that make tokenPair a session cookie on renewal (#19031)
## Summary

- Removes two remaining direct `cookieStorage.setItem('tokenPair', ...)`
calls that were overwriting the Jotai-managed cookie (180-day expiry)
with a session cookie (no expiry) during **token renewal**
- Followup to #18795 which fixed the same issue in `handleSetAuthTokens`
but missed the renewal code paths

## Root cause

Two token renewal paths still had direct cookie writes without
`expires`:

1. **`apollo.factory.ts`** — `attemptTokenRenewal()` fires on every
`UNAUTHENTICATED` GraphQL error after a successful token refresh
2. **`useAgentChat.ts`** — `retryFetchWithRenewedToken()` fires on 401
from the AI chat endpoint

Both called `cookieStorage.setItem('tokenPair', JSON.stringify(tokens))`
without an `expires` attribute, creating a session cookie that overwrote
the Jotai-managed one. This is why the bug was **intermittent after
#18795**: it only appeared after a token renewal, not on fresh login.

The `onTokenPairChange` / `setTokenPair` calls already write through
Jotai's `atomWithStorage` → `createJotaiCookieStorage`, which always
sets `expires: 180 days`.

## Test plan

- Log in to the app
- Wait for a token renewal to occur (or force one by letting the access
token expire)
- Inspect the `tokenPair` cookie in DevTools → Application → Cookies
- Verify the cookie retains an expiration date ~180 days from now (not
"Session")
- Close and reopen the browser — confirm you remain logged in


Made with [Cursor](https://cursor.com)
2026-03-27 08:21:26 +00:00
6f0ac88e20 fix: batch viewGroup mutations sequentially to prevent race conditions (#19027)
## Bug Description

When reordering stages in the Kanban board, the frontend fires all
viewGroup update mutations concurrently via Promise.all, causing race
conditions in the workspace migration runner's cache invalidation,
database contention, and a thundering herd effect that stalls the
server.

## Changes

Changed `usePerformViewGroupAPIPersist` to execute viewGroup update
mutations sequentially instead of concurrently. The `Promise.all`
pattern fired all N mutations simultaneously, each triggering a full
workspace migration runner pipeline (transaction + cache invalidation).
The sequential `for...of` loop ensures each mutation completes
(including its cache invalidation) before the next begins, eliminating
the race condition.

## Related Issue

Fixes #18865

## Testing

This fix addresses the root cause identified in the Sonarly analysis on
the issue. The concurrent mutation pattern was causing:
- PostgreSQL row-level lock contention on viewGroup rows
- Cache thundering herd from repeated invalidation/recomputation cycles
- Server stalls requiring container restarts

The sequential approach ensures proper ordering and prevents these race
conditions.

---------

Co-authored-by: Rayan <[email protected]>
Co-authored-by: Charles Bochet <[email protected]>
2026-03-27 08:13:51 +00:00
Charles BochetandGitHub 17424320e3 fix: gate command-menu-items query behind feature flag (#19029)
## Summary
- Gate the `FindManyCommandMenuItems` GraphQL query behind the
`IS_COMMAND_MENU_ITEM_ENABLED` feature flag on the frontend, preventing
an uncaught error when the flag is not enabled for a workspace
- Remove `IS_CONNECTED_ACCOUNT_MIGRATED` from the default feature flags
list
2026-03-27 08:11:26 +01:00
6360fb3bce chore: sync AI model catalog from models.dev (#19028)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <[email protected]>
2026-03-27 07:22:18 +01:00
Baptiste DevessierandGitHub da1b1f1cbc Combine and clean upgrade commands for record page layouts (#19004)
- Remove the label identifier field for all standard objects as it's a
first-class citizen that is displayed specifically in the app; it
doesn't make a lot of sense to display it in the Fields widgets
- Disable logic that made the label identifier required and in first
position
- Add all fields for all standard objects in record page layout view
fields
- Do not include position and ts vector fields in custom objects

> [!IMPORTANT]
> The command will create Field widgets for all relations. It is
consistent to the way the frontend dynamically generates them as of
today. We will have to decide which relations we pin as individual Field
widgets before the release. (This will likely land in this command or in
another one.)
2026-03-26 17:50:29 +01:00
nitinandGitHub 31718d163c [Dashboards] fix rich text widget AGAIN (#19017) 2026-03-26 17:50:10 +01:00
Abdul RahmanandGitHub f47608de07 Clear navbar edit selection when closing the side panel (#18940)
In navbar edit mode, selecting an item for edit stored
selectedNavigationMenuItemIdInEditModeState (and related
pending-insertion state). Closing the side panel did not reset those
atoms, so the nav item stayed visually selected. Reset both when the
panel closes so the highlight matches the closed panel; reopening in
edit mode then starts from the generic “new item” entry unless the user
picks an item again.
2026-03-26 17:49:50 +01:00
Thomas des FrancsandGitHub 695518a15e Fix not shared chip height + hard coded border radius (#19020)
## Summary
- align the forbidden field "Not shared" chip with small chip dimensions
in the object table
- use the shared small border radius token instead of a hardcoded value
- add `overflow: hidden` and `user-select: none` to match chip behavior
more closely

## Testing
- Not run (not requested)
2026-03-26 17:48:20 +01:00
231 changed files with 11517 additions and 6322 deletions
+1
View File
@@ -203,6 +203,7 @@
"packages/twenty-utils",
"packages/twenty-zapier",
"packages/twenty-website",
"packages/twenty-website-new",
"packages/twenty-docs",
"packages/twenty-e2e-testing",
"packages/twenty-shared",
@@ -2273,6 +2273,11 @@ type FieldConnection {
edges: [FieldEdge!]!
}
type LogicFunctionLogs {
"""Execution Logs"""
logs: String!
}
type DeleteTwoFactorAuthenticationMethod {
"""Boolean that confirms query was dispatched"""
success: Boolean!
@@ -2593,11 +2598,6 @@ type UsageAnalytics {
userDailyUsage: UsageUserDaily
}
type LogicFunctionLogs {
"""Execution Logs"""
logs: String!
}
type FrontComponent {
id: UUID!
name: String!
@@ -3501,6 +3501,7 @@ type Mutation {
createViewGroup(input: CreateViewGroupInput!): ViewGroup!
createManyViewGroups(inputs: [CreateViewGroupInput!]!): [ViewGroup!]!
updateViewGroup(input: UpdateViewGroupInput!): ViewGroup!
updateManyViewGroups(inputs: [UpdateViewGroupInput!]!): [ViewGroup!]!
deleteViewGroup(input: DeleteViewGroupInput!): ViewGroup!
destroyViewGroup(input: DestroyViewGroupInput!): ViewGroup!
updateMessageFolder(input: UpdateMessageFolderInput!): MessageFolder!
@@ -1937,6 +1937,12 @@ export interface FieldConnection {
__typename: 'FieldConnection'
}
export interface LogicFunctionLogs {
/** Execution Logs */
logs: Scalars['String']
__typename: 'LogicFunctionLogs'
}
export interface DeleteTwoFactorAuthenticationMethod {
/** Boolean that confirms query was dispatched */
success: Scalars['Boolean']
@@ -2298,12 +2304,6 @@ export interface UsageAnalytics {
__typename: 'UsageAnalytics'
}
export interface LogicFunctionLogs {
/** Execution Logs */
logs: Scalars['String']
__typename: 'LogicFunctionLogs'
}
export interface FrontComponent {
id: Scalars['UUID']
name: Scalars['String']
@@ -2954,6 +2954,7 @@ export interface Mutation {
createViewGroup: ViewGroup
createManyViewGroups: ViewGroup[]
updateViewGroup: ViewGroup
updateManyViewGroups: ViewGroup[]
deleteViewGroup: ViewGroup
destroyViewGroup: ViewGroup
updateMessageFolder: MessageFolder
@@ -5096,6 +5097,13 @@ export interface FieldConnectionGenqlSelection{
__scalar?: boolean | number
}
export interface LogicFunctionLogsGenqlSelection{
/** Execution Logs */
logs?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface DeleteTwoFactorAuthenticationMethodGenqlSelection{
/** Boolean that confirms query was dispatched */
success?: boolean | number
@@ -5501,13 +5509,6 @@ export interface UsageAnalyticsGenqlSelection{
__scalar?: boolean | number
}
export interface LogicFunctionLogsGenqlSelection{
/** Execution Logs */
logs?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface FrontComponentGenqlSelection{
id?: boolean | number
name?: boolean | number
@@ -6207,6 +6208,7 @@ export interface MutationGenqlSelection{
createViewGroup?: (ViewGroupGenqlSelection & { __args: {input: CreateViewGroupInput} })
createManyViewGroups?: (ViewGroupGenqlSelection & { __args: {inputs: CreateViewGroupInput[]} })
updateViewGroup?: (ViewGroupGenqlSelection & { __args: {input: UpdateViewGroupInput} })
updateManyViewGroups?: (ViewGroupGenqlSelection & { __args: {inputs: UpdateViewGroupInput[]} })
deleteViewGroup?: (ViewGroupGenqlSelection & { __args: {input: DeleteViewGroupInput} })
destroyViewGroup?: (ViewGroupGenqlSelection & { __args: {input: DestroyViewGroupInput} })
updateMessageFolder?: (MessageFolderGenqlSelection & { __args: {input: UpdateMessageFolderInput} })
@@ -8047,6 +8049,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const LogicFunctionLogs_possibleTypes: string[] = ['LogicFunctionLogs']
export const isLogicFunctionLogs = (obj?: { __typename?: any } | null): obj is LogicFunctionLogs => {
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunctionLogs"')
return LogicFunctionLogs_possibleTypes.includes(obj.__typename)
}
const DeleteTwoFactorAuthenticationMethod_possibleTypes: string[] = ['DeleteTwoFactorAuthenticationMethod']
export const isDeleteTwoFactorAuthenticationMethod = (obj?: { __typename?: any } | null): obj is DeleteTwoFactorAuthenticationMethod => {
if (!obj?.__typename) throw new Error('__typename is missing in "isDeleteTwoFactorAuthenticationMethod"')
@@ -8431,14 +8441,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const LogicFunctionLogs_possibleTypes: string[] = ['LogicFunctionLogs']
export const isLogicFunctionLogs = (obj?: { __typename?: any } | null): obj is LogicFunctionLogs => {
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunctionLogs"')
return LogicFunctionLogs_possibleTypes.includes(obj.__typename)
}
const FrontComponent_possibleTypes: string[] = ['FrontComponent']
export const isFrontComponent = (obj?: { __typename?: any } | null): obj is FrontComponent => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFrontComponent"')
@@ -63,8 +63,8 @@ export default {
210,
227,
244,
282,
283,
284,
299,
300,
325,
@@ -4458,6 +4458,14 @@ export default {
1
]
},
"LogicFunctionLogs": {
"logs": [
1
],
"__typename": [
1
]
},
"DeleteTwoFactorAuthenticationMethod": {
"success": [
6
@@ -4503,10 +4511,10 @@ export default {
},
"AuthTokenPair": {
"accessOrWorkspaceAgnosticToken": [
250
251
],
"refreshToken": [
250
251
],
"__typename": [
1
@@ -4514,7 +4522,7 @@ export default {
},
"AvailableWorkspacesAndAccessTokens": {
"tokens": [
251
252
],
"availableWorkspaces": [
224
@@ -4566,10 +4574,10 @@ export default {
},
"SignUp": {
"loginToken": [
250
251
],
"workspace": [
256
257
],
"__typename": [
1
@@ -4577,7 +4585,7 @@ export default {
},
"TransientToken": {
"transientToken": [
250
251
],
"__typename": [
1
@@ -4599,7 +4607,7 @@ export default {
},
"VerifyEmailAndGetLoginToken": {
"loginToken": [
250
251
],
"workspaceUrls": [
157
@@ -4618,7 +4626,7 @@ export default {
},
"AuthTokens": {
"tokens": [
251
252
],
"__typename": [
1
@@ -4626,7 +4634,7 @@ export default {
},
"LoginToken": {
"loginToken": [
250
251
],
"__typename": [
1
@@ -4656,10 +4664,10 @@ export default {
},
"Impersonate": {
"loginToken": [
250
251
],
"workspace": [
256
257
],
"__typename": [
1
@@ -4689,10 +4697,10 @@ export default {
},
"ApplicationTokenPair": {
"applicationAccessToken": [
250
251
],
"applicationRefreshToken": [
250
251
],
"__typename": [
1
@@ -4764,7 +4772,7 @@ export default {
1
],
"fields": [
271
272
],
"__typename": [
1
@@ -4861,10 +4869,10 @@ export default {
6
],
"objectPermissions": [
275
276
],
"fieldPermissions": [
276
277
],
"permissionFlags": [
1
@@ -4914,19 +4922,19 @@ export default {
1
],
"objects": [
272
],
"fields": [
271
],
"logicFunctions": [
273
],
"frontComponents": [
"fields": [
272
],
"logicFunctions": [
274
],
"frontComponents": [
275
],
"defaultRole": [
277
278
],
"sourcePackage": [
1
@@ -4986,13 +4994,13 @@ export default {
1
],
"driver": [
282
],
"status": [
283
],
"status": [
284
],
"verificationRecords": [
280
281
],
"verifiedAt": [
4
@@ -5039,7 +5047,7 @@ export default {
1
],
"location": [
285
286
],
"__typename": [
1
@@ -5067,13 +5075,13 @@ export default {
},
"ImapSmtpCaldavConnectionParameters": {
"IMAP": [
287
288
],
"SMTP": [
287
288
],
"CALDAV": [
287
288
],
"__typename": [
1
@@ -5093,7 +5101,7 @@ export default {
3
],
"connectionParameters": [
288
289
],
"__typename": [
1
@@ -5157,7 +5165,7 @@ export default {
1
],
"dailyUsage": [
293
294
],
"__typename": [
1
@@ -5165,13 +5173,13 @@ export default {
},
"UsageAnalytics": {
"usageByUser": [
292
293
],
"usageByOperationType": [
292
293
],
"timeSeries": [
293
294
],
"periodStart": [
4
@@ -5180,15 +5188,7 @@ export default {
4
],
"userDailyUsage": [
294
],
"__typename": [
1
]
},
"LogicFunctionLogs": {
"logs": [
1
295
],
"__typename": [
1
@@ -5235,7 +5235,7 @@ export default {
6
],
"applicationTokenPair": [
269
270
],
"__typename": [
1
@@ -6656,7 +6656,7 @@ export default {
}
],
"checkUserExists": [
264,
265,
{
"email": [
1,
@@ -6668,7 +6668,7 @@ export default {
}
],
"checkWorkspaceInviteHashIsValid": [
265,
266,
{
"inviteHash": [
1,
@@ -6686,7 +6686,7 @@ export default {
}
],
"validatePasswordResetToken": [
259,
260,
{
"passwordResetToken": [
1,
@@ -6769,7 +6769,7 @@ export default {
220
],
"getConnectedImapSmtpCaldavAccount": [
289,
290,
{
"id": [
3,
@@ -6778,7 +6778,7 @@ export default {
}
],
"getAutoCompleteAddress": [
284,
285,
{
"address": [
1,
@@ -6797,7 +6797,7 @@ export default {
}
],
"getAddressDetails": [
286,
287,
{
"placeId": [
1,
@@ -6889,19 +6889,19 @@ export default {
}
],
"getPostgresCredentials": [
291
292
],
"findManyPublicDomains": [
279
280
],
"getEmailingDomains": [
281
282
],
"findManyMarketplaceApps": [
278
279
],
"findOneMarketplaceApp": [
278,
279,
{
"universalIdentifier": [
1,
@@ -6924,7 +6924,7 @@ export default {
}
],
"getUsageAnalytics": [
295,
296,
{
"input": [
365
@@ -8130,6 +8130,15 @@ export default {
]
}
],
"updateManyViewGroups": [
56,
{
"inputs": [
450,
"[UpdateViewGroupInput!]!"
]
}
],
"deleteViewGroup": [
56,
{
@@ -8300,7 +8309,7 @@ export default {
}
],
"getAuthorizationUrlForSSO": [
254,
255,
{
"input": [
466,
@@ -8309,7 +8318,7 @@ export default {
}
],
"getLoginTokenFromCredentials": [
263,
264,
{
"email": [
1,
@@ -8335,7 +8344,7 @@ export default {
}
],
"signIn": [
252,
253,
{
"email": [
1,
@@ -8357,7 +8366,7 @@ export default {
}
],
"verifyEmailAndGetLoginToken": [
260,
261,
{
"emailVerificationToken": [
1,
@@ -8377,7 +8386,7 @@ export default {
}
],
"verifyEmailAndGetWorkspaceAgnosticToken": [
252,
253,
{
"emailVerificationToken": [
1,
@@ -8393,7 +8402,7 @@ export default {
}
],
"getAuthTokensFromOTP": [
262,
263,
{
"otp": [
1,
@@ -8413,7 +8422,7 @@ export default {
}
],
"signUp": [
252,
253,
{
"email": [
1,
@@ -8435,7 +8444,7 @@ export default {
}
],
"signUpInWorkspace": [
257,
258,
{
"email": [
1,
@@ -8466,13 +8475,13 @@ export default {
}
],
"signUpInNewWorkspace": [
257
],
"generateTransientToken": [
258
],
"generateTransientToken": [
259
],
"getAuthTokensFromLoginToken": [
262,
263,
{
"loginToken": [
1,
@@ -8485,7 +8494,7 @@ export default {
}
],
"authorizeApp": [
249,
250,
{
"clientId": [
1,
@@ -8507,7 +8516,7 @@ export default {
}
],
"renewToken": [
262,
263,
{
"appToken": [
1,
@@ -8516,7 +8525,7 @@ export default {
}
],
"generateApiKeyToken": [
261,
262,
{
"apiKeyId": [
3,
@@ -8529,7 +8538,7 @@ export default {
}
],
"emailPasswordResetLink": [
253,
254,
{
"email": [
1,
@@ -8541,7 +8550,7 @@ export default {
}
],
"updatePasswordViaResetToken": [
255,
256,
{
"passwordResetToken": [
1,
@@ -8642,7 +8651,7 @@ export default {
}
],
"initiateOTPProvisioning": [
247,
248,
{
"loginToken": [
1,
@@ -8655,10 +8664,10 @@ export default {
}
],
"initiateOTPProvisioningForAuthenticatedUser": [
247
248
],
"deleteTwoFactorAuthenticationMethod": [
246,
247,
{
"twoFactorAuthenticationMethodId": [
3,
@@ -8667,7 +8676,7 @@ export default {
}
],
"verifyTwoFactorAuthenticationMethodForAuthenticatedUser": [
248,
249,
{
"otp": [
1,
@@ -8773,7 +8782,7 @@ export default {
}
],
"impersonate": [
266,
267,
{
"userId": [
3,
@@ -8795,7 +8804,7 @@ export default {
}
],
"saveImapSmtpCaldavAccount": [
290,
291,
{
"accountOwnerId": [
3,
@@ -8998,13 +9007,13 @@ export default {
}
],
"enablePostgresProxy": [
291
292
],
"disablePostgresProxy": [
291
292
],
"createPublicDomain": [
279,
280,
{
"domain": [
1,
@@ -9031,14 +9040,14 @@ export default {
}
],
"createEmailingDomain": [
281,
282,
{
"domain": [
1,
"String!"
],
"driver": [
282,
283,
"EmailingDomainDriver!"
]
}
@@ -9053,7 +9062,7 @@ export default {
}
],
"verifyEmailingDomain": [
281,
282,
{
"id": [
1,
@@ -9130,7 +9139,7 @@ export default {
}
],
"createDevelopmentApplication": [
267,
268,
{
"universalIdentifier": [
1,
@@ -9143,7 +9152,7 @@ export default {
}
],
"generateApplicationToken": [
269,
270,
{
"applicationId": [
3,
@@ -9152,7 +9161,7 @@ export default {
}
],
"syncApplication": [
268,
269,
{
"manifest": [
15,
@@ -9161,7 +9170,7 @@ export default {
}
],
"uploadApplicationFile": [
270,
271,
{
"file": [
372,
@@ -9195,7 +9204,7 @@ export default {
}
],
"renewApplicationToken": [
269,
270,
{
"applicationRefreshToken": [
1,
@@ -11455,7 +11464,7 @@ export default {
}
],
"logicFunctionLogs": [
296,
246,
{
"input": [
490,
File diff suppressed because one or more lines are too long
@@ -31,8 +31,6 @@ import { useCallback, useState } from 'react';
import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { isDefined } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { cookieStorage } from '~/utils/cookie-storage';
export const useAgentChat = (
uiMessages: ExtendedUIMessage[],
ensureThreadIdForSend: () => Promise<string | null>,
@@ -94,7 +92,6 @@ export const useAgentChat = (
return null;
}
cookieStorage.setItem('tokenPair', JSON.stringify(renewedTokens));
setTokenPair(renewedTokens);
const updatedHeaders = new Headers(init?.headers ?? {});
@@ -34,7 +34,6 @@ import {
import isEmpty from 'lodash.isempty';
import { getGenericOperationName, isDefined } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { cookieStorage } from '~/utils/cookie-storage';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
const logger = loggerLink(() => 'Twenty');
@@ -175,7 +174,6 @@ export class ApolloFactory implements ApolloManager {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
}
};
@@ -6,11 +6,13 @@ import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelate
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { transformPageLayout } from '@/page-layout/utils/transformPageLayout';
import { logicFunctionsState } from '@/settings/logic-functions/states/logicFunctionsState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useApolloClient } from '@apollo/client/react';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
FeatureFlagKey,
FindAllViewsDocument,
FindManyCommandMenuItemsDocument,
FindAllRecordPageLayoutsDocument,
@@ -55,6 +57,9 @@ export const useLoadStaleMetadataEntities = () => {
const client = useApolloClient();
const store = useStore();
const { replaceDraft, applyChanges } = useUpdateMetadataStoreDraft();
const isCommandMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
);
const loadStaleMetadataEntities = useCallback(
async (staleEntityKeys: MetadataEntityKey[]) => {
@@ -194,7 +199,10 @@ export const useLoadStaleMetadataEntities = () => {
);
}
if (staleEntityKeys.includes('commandMenuItems')) {
if (
staleEntityKeys.includes('commandMenuItems') &&
isCommandMenuItemEnabled
) {
fetchPromises.push(
client
.query({
@@ -214,7 +222,7 @@ export const useLoadStaleMetadataEntities = () => {
await Promise.all(fetchPromises);
applyChanges();
},
[client, store, replaceDraft, applyChanges],
[client, store, replaceDraft, applyChanges, isCommandMenuItemEnabled],
);
return { loadStaleMetadataEntities };
@@ -0,0 +1,8 @@
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
export type NavigationMenuItemDropResult = {
source: DropDestination;
destination: DropDestination | null;
draggableId: string;
insertBeforeItemId?: string | null;
};
@@ -1,14 +1,14 @@
import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig';
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
import type { DropResult } from '@hello-pangea/dnd';
export const getDropTargetIdFromDestination = ({
navigationMenuItemSection,
destination,
}: {
navigationMenuItemSection: NavigationMenuItemSection;
destination: DropResult['destination'];
destination: DropDestination | null;
}): string | null => {
if (
!destination ||
@@ -1,69 +0,0 @@
import { Droppable } from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection';
type NavigationMenuItemDroppableProps = {
droppableId: string;
children: React.ReactNode;
isDragIndicatorVisible?: boolean;
showDropLine?: boolean;
isWorkspaceSection?: boolean;
};
const StyledDroppableWrapper = styled.div`
position: relative;
transition: all 150ms ease-in-out;
width: 100%;
&[data-dragging-over='true'] {
background-color: ${themeCssVariables.background.transparent.blue};
}
&[data-dragging-over='true'][data-show-drop-line='true']::before {
background-color: ${themeCssVariables.color.blue};
border-radius: ${themeCssVariables.border.radius.sm}
${themeCssVariables.border.radius.sm} 0 0;
bottom: 0;
content: '';
height: 2px;
left: 0;
position: absolute;
width: 100%;
}
`;
export const NavigationMenuItemDroppable = ({
droppableId,
children,
isDragIndicatorVisible = true,
showDropLine = true,
isWorkspaceSection = false,
}: NavigationMenuItemDroppableProps) => {
const isDropDisabled = useIsDropDisabledForSection(isWorkspaceSection);
return (
<Droppable droppableId={droppableId} isDropDisabled={isDropDisabled}>
{(provided, snapshot) => (
<StyledDroppableWrapper
data-dragging-over={
snapshot.isDraggingOver && isDragIndicatorVisible
? 'true'
: undefined
}
data-show-drop-line={showDropLine ? 'true' : undefined}
>
<div
ref={provided.innerRef}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{children}
{provided.placeholder}
</div>
</StyledDroppableWrapper>
)}
</Droppable>
);
};
@@ -1,6 +0,0 @@
export const DROP_RESULT_OPTIONS = {
reason: 'DROP' as const,
combine: null,
mode: 'FLUID' as const,
type: 'DEFAULT' as const,
};
@@ -1,4 +1,3 @@
import type { DropResult, ResponderProvided } from '@hello-pangea/dnd';
import { t } from '@lingui/core/macro';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -28,6 +27,8 @@ import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useStore } from 'jotai';
import { NavigationMenuItemType } from 'twenty-shared/types';
import type { NavigationMenuItemDropResult } from '@/navigation-menu-item/common/types/navigationMenuItemDropResult';
export const useHandleAddToNavigationDrop = () => {
const store = useStore();
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
@@ -50,7 +51,7 @@ export const useHandleAddToNavigationDrop = () => {
);
const handleAddToNavigationDrop = useCallback(
(result: DropResult, _provided: ResponderProvided) => {
(result: NavigationMenuItemDropResult) => {
const { source, destination, draggableId } = result;
if (
source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID ||
@@ -1,8 +1,9 @@
import { type OnDragEndResponder } from '@hello-pangea/dnd';
import { useStore } from 'jotai';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared/utils';
import type { NavigationMenuItemDropResult } from '@/navigation-menu-item/common/types/navigationMenuItemDropResult';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { useUpdateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems';
import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig';
@@ -115,10 +116,8 @@ export const useHandleNavigationMenuItemDragAndDrop = (
});
};
const handleNavigationMenuItemDragAndDrop: OnDragEndResponder = async (
result: Parameters<OnDragEndResponder>[0] & {
insertBeforeItemId?: string | null;
},
const handleNavigationMenuItemDragAndDrop = async (
result: NavigationMenuItemDropResult,
) => {
const { destination, source, draggableId } = result;
@@ -1,6 +1,5 @@
import { type DragDropProvider } from '@dnd-kit/react';
import { isSortable } from '@dnd-kit/react/sortable';
import type { ResponderProvided } from '@hello-pangea/dnd';
import { useStore } from 'jotai';
import { type ComponentProps, useCallback, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -11,17 +10,16 @@ import { NavigationSections } from '@/navigation-menu-item/common/constants/Navi
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData';
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
import type { NavigationMenuItemDropResult } from '@/navigation-menu-item/common/types/navigationMenuItemDropResult';
import type { SortableTargetDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination';
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
import { extractFolderIdFromDroppableId } from '@/navigation-menu-item/common/utils/extractFolderIdFromDroppableId';
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
import { DROP_RESULT_OPTIONS } from '@/navigation-menu-item/display/dnd/constants/navigationMenuItemDndKitDropResultOptions';
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop';
import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop';
import { resolveDropTarget } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget';
import { toDropResult } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitToDropResult';
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
@@ -172,23 +170,13 @@ export const useNavigationMenuItemDndKit = (
destination: DropDestination,
insertBeforeItemId?: string | null,
) => {
const result = toDropResult(
id,
{
sourceDroppableId: source.droppableId,
sourceIndex: source.index,
},
const result: NavigationMenuItemDropResult = {
draggableId: id,
source,
destination,
);
const provided: ResponderProvided = { announce: () => {} };
handleNavigationMenuItemDragAndDrop(
{
...result,
...DROP_RESULT_OPTIONS,
...(insertBeforeItemId != null && { insertBeforeItemId }),
},
provided,
);
...(insertBeforeItemId != null && { insertBeforeItemId }),
};
handleNavigationMenuItemDragAndDrop(result);
},
[handleNavigationMenuItemDragAndDrop],
);
@@ -351,12 +339,18 @@ export const useNavigationMenuItemDndKit = (
destination = fallback;
}
const result = toDropResult(draggableId, data, destination);
const provided: ResponderProvided = { announce: () => {} };
const dropResult = { ...result, ...DROP_RESULT_OPTIONS };
const dropResult: NavigationMenuItemDropResult = {
draggableId,
source: {
droppableId: data?.sourceDroppableId ?? '',
index: data?.sourceIndex ?? 0,
},
destination,
...(insertBeforeItemId != null && { insertBeforeItemId }),
};
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
handleAddToNavigationDrop(dropResult, provided);
handleAddToNavigationDrop(dropResult);
return;
}
@@ -386,7 +380,7 @@ export const useNavigationMenuItemDndKit = (
return;
}
handleNavigationMenuItemDragAndDrop(dropResult, provided);
handleNavigationMenuItemDragAndDrop(dropResult);
};
const contextValues: NavigationMenuItemDndKitContextValues = {
@@ -1,20 +0,0 @@
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData';
export const toDropResult = (
draggableId: string,
data: DraggableData | undefined,
destination: DropDestination | null,
): {
source: DropDestination;
destination: DropDestination | null;
draggableId: string;
} => {
const sourceDroppableId = data?.sourceDroppableId ?? '';
const sourceIndex = data?.sourceIndex ?? 0;
return {
source: { droppableId: sourceDroppableId, index: sourceIndex },
destination,
draggableId,
};
};
@@ -1,102 +0,0 @@
import {
type DraggableProvided,
type DraggableRubric,
type DraggableStateSnapshot,
} from '@hello-pangea/dnd';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular';
import { getObjectNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemSecondaryLabel';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
type WorkspaceNavigationMenuItemFolderDragCloneProps = {
draggableProvided: DraggableProvided;
draggableSnapshot: DraggableStateSnapshot;
rubric: DraggableRubric;
navigationMenuItems: NavigationMenuItem[];
navigationMenuItemFolderContentLength: number;
selectedNavigationMenuItemIndex: number;
};
export const WorkspaceNavigationMenuItemFolderDragClone = ({
draggableProvided,
draggableSnapshot,
rubric,
navigationMenuItems,
navigationMenuItemFolderContentLength,
selectedNavigationMenuItemIndex,
}: WorkspaceNavigationMenuItemFolderDragCloneProps) => {
const { theme } = useContext(ThemeContext);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const navigationMenuItem = navigationMenuItems[rubric.source.index];
if (!isDefined(navigationMenuItem)) {
return null;
}
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
const view = isDefined(navigationMenuItem.viewId)
? views.find((view) => view.id === navigationMenuItem.viewId)
: undefined;
const isIndexView = view?.key === ViewKey.INDEX;
return (
<div
ref={draggableProvided.innerRef}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps}
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.dragHandleProps}
style={{
...draggableProvided.draggableProps.style,
background: draggableSnapshot.isDragging
? theme.background.transparent.light
: undefined,
}}
>
<NavigationDrawerSubItem
secondaryLabel={
isIndexView
? undefined
: getObjectNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular: objectNameSingular ?? '',
})
}
label={label}
Icon={() => (
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
)}
to={undefined}
active={false}
isDragging={true}
subItemState={getNavigationSubItemLeftAdornment({
index: rubric.source.index,
arrayLength: navigationMenuItemFolderContentLength,
selectedIndex: selectedNavigationMenuItemIndex,
})}
triggerEvent="CLICK"
/>
</div>
);
};
@@ -8,14 +8,17 @@ const StyledContainer = styled.div`
align-items: center;
background: ${themeCssVariables.background.transparent.light};
border-radius: 4px;
border-radius: ${themeCssVariables.border.radius.sm};
color: ${themeCssVariables.font.color.tertiary};
display: inline-flex;
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.regular};
gap: ${themeCssVariables.spacing[1]};
height: ${themeCssVariables.spacing[3]};
overflow: hidden;
padding: ${themeCssVariables.spacing[1]};
user-select: none;
`;
export const ForbiddenFieldDisplay = () => {
@@ -91,13 +91,20 @@ export const MultiItemFieldInput = <T,>({
) {
return;
}
const { isValid, updatedItems } = validateInputAndComputeUpdatedItems();
if (!isValid) {
if (isInputDisplayed) {
const { isValid, updatedItems } = validateInputAndComputeUpdatedItems();
if (!isValid) {
return;
}
onChange(updatedItems);
onClickOutside(updatedItems, event);
return;
}
onChange(updatedItems);
onClickOutside(items, event);
},
listenerId: instanceId,
@@ -76,6 +76,12 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
return;
}
if (widgetType === WidgetType.STANDALONE_RICH_TEXT) {
setPageLayoutEditingWidgetId(widgetId);
closeSidePanelMenu();
return;
}
setSidePanelPage(SidePanelPages.CommandMenuDisplay);
closeSidePanelMenu();
},
@@ -1,8 +1,20 @@
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
import { viewableRecordIdState } from '@/object-record/record-side-panel/states/viewableRecordIdState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
import { SIDE_PANEL_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/side-panel/constants/SidePanelContextChipGroupsDropdownId';
import { SIDE_PANEL_SELECTABLE_LIST_ID } from '@/side-panel/constants/SidePanelSelectableListId';
import { SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelPreviousComponentInstanceId';
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
import { SIDE_PANEL_SELECTABLE_LIST_ID } from '@/side-panel/constants/SidePanelSelectableListId';
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { sidePanelNavigationMorphItemsByPageState } from '@/side-panel/states/sidePanelNavigationMorphItemsByPageState';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
@@ -10,19 +22,10 @@ import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { sidePanelShowHiddenObjectsState } from '@/side-panel/states/sidePanelShowHiddenObjectsState';
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { viewableRecordIdState } from '@/object-record/record-side-panel/states/viewableRecordIdState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { emitSidePanelCloseEvent } from '@/ui/layout/side-panel/utils/emitSidePanelCloseEvent';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { emitSidePanelCloseEvent } from '@/ui/layout/side-panel/utils/emitSidePanelCloseEvent';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { WORKFLOW_LOGIC_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/workflow-actions/code-action/constants/WorkflowLogicFunctionTabListComponentId';
import { WorkflowLogicFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowLogicFunctionTabId';
@@ -41,6 +44,12 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
const { closeDropdown } = useCloseDropdown();
const resetNavigationMenuItemState = () => {
store.set(selectedNavigationMenuItemIdInEditModeState.atom, null);
store.set(pendingInsertionNavigationMenuItemState.atom, null);
store.set(addToNavPayloadRegistryState.atom, new Map());
};
const sidePanelCloseAnimationCompleteCleanup = useCallback(
(options?: { emitSidePanelCloseEvent?: boolean }) => {
closeDropdown(SIDE_PANEL_CONTEXT_CHIP_GROUPS_DROPDOWN_ID);
@@ -110,6 +119,7 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
store.set(sidePanelShowHiddenObjectsState.atom, false);
store.set(sidePanelNavigationMorphItemsByPageState.atom, new Map());
store.set(sidePanelNavigationStackState.atom, []);
resetNavigationMenuItemState();
resetSelectedItem();
store.set(hasUserSelectedSidePanelListItemState.atom, false);
@@ -136,7 +146,13 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
);
}
},
[closeDropdown, resetContextStoreStates, resetSelectedItem, store],
[
closeDropdown,
resetContextStoreStates,
resetNavigationMenuItemState,
resetSelectedItem,
store,
],
);
return {
@@ -1,5 +1,4 @@
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
@@ -29,7 +28,6 @@ export const useSidePanelMenu = () => {
const isSidePanelOpened = store.get(isSidePanelOpenedState.atom);
if (isSidePanelOpened) {
store.set(addToNavPayloadRegistryState.atom, new Map());
store.set(isSidePanelOpenedState.atom, false);
store.set(isSidePanelClosingState.atom, true);
closeAnyOpenDropdown();
@@ -0,0 +1,11 @@
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
import { gql } from '@apollo/client';
export const UPDATE_MANY_VIEW_GROUPS = gql`
${VIEW_GROUP_FRAGMENT}
mutation UpdateManyViewGroups($inputs: [UpdateViewGroupInput!]!) {
updateManyViewGroups(inputs: $inputs) {
...ViewGroupFragment
}
}
`;
@@ -8,43 +8,44 @@ import { t } from '@lingui/core/macro';
import { CrudOperationType } from 'twenty-shared/types';
import { useMutation } from '@apollo/client/react';
import {
type UpdateViewGroupMutationVariables,
UpdateViewGroupDocument,
type UpdateManyViewGroupsMutationVariables,
UpdateManyViewGroupsDocument,
} from '~/generated-metadata/graphql';
export const usePerformViewGroupAPIPersist = () => {
const [updateViewGroupMutation] = useMutation(UpdateViewGroupDocument);
const [updateManyViewGroupsMutation] = useMutation(
UpdateManyViewGroupsDocument,
);
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const performViewGroupAPIUpdate = useCallback(
async (
updateViewGroupInputs: UpdateViewGroupMutationVariables[],
updateViewGroupInputs: UpdateManyViewGroupsMutationVariables,
): Promise<
MetadataRequestResult<
Awaited<ReturnType<typeof updateViewGroupMutation>>[]
>
MetadataRequestResult<Awaited<
ReturnType<typeof updateManyViewGroupsMutation>
> | null>
> => {
if (updateViewGroupInputs.length === 0) {
if (
!Array.isArray(updateViewGroupInputs.inputs) ||
updateViewGroupInputs.inputs.length === 0
) {
return {
status: 'successful',
response: [],
response: null,
};
}
try {
const results = await Promise.all(
updateViewGroupInputs.map((variables) =>
updateViewGroupMutation({
variables,
}),
),
);
const result = await updateManyViewGroupsMutation({
variables: updateViewGroupInputs,
});
return {
status: 'successful',
response: results,
response: result,
};
} catch (error) {
if (CombinedGraphQLErrors.is(error)) {
@@ -62,7 +63,7 @@ export const usePerformViewGroupAPIPersist = () => {
};
}
},
[updateViewGroupMutation, handleMetadataError, enqueueErrorSnackBar],
[updateManyViewGroupsMutation, handleMetadataError, enqueueErrorSnackBar],
);
return {
@@ -67,9 +67,9 @@ export const useSaveCurrentViewGroups = () => {
return;
}
await performViewGroupAPIUpdate([
{
input: {
await performViewGroupAPIUpdate({
inputs: [
{
id: existingField.id,
update: {
isVisible: viewGroupToSave.isVisible,
@@ -77,8 +77,8 @@ export const useSaveCurrentViewGroups = () => {
fieldValue: viewGroupToSave.fieldValue,
},
},
},
]);
],
});
},
[
store,
@@ -109,7 +109,7 @@ export const useSaveCurrentViewGroups = () => {
const currentViewGroups = view.viewGroups;
const viewGroupsToUpdate = viewGroupsToSave
const viewGroupInputsToUpdate = viewGroupsToSave
.map((viewGroupToSave) => {
const existingField = currentViewGroups.find(
(currentViewGroup) =>
@@ -136,13 +136,11 @@ export const useSaveCurrentViewGroups = () => {
}
return {
input: {
id: existingField.id,
update: {
isVisible: viewGroupToSave.isVisible,
position: viewGroupToSave.position,
fieldValue: viewGroupToSave.fieldValue,
},
id: existingField.id,
update: {
isVisible: viewGroupToSave.isVisible,
position: viewGroupToSave.position,
fieldValue: viewGroupToSave.fieldValue,
},
};
})
@@ -152,7 +150,7 @@ export const useSaveCurrentViewGroups = () => {
throw new Error('mainGroupByFieldMetadataId is required');
}
await performViewGroupAPIUpdate(viewGroupsToUpdate);
await performViewGroupAPIUpdate({ inputs: viewGroupInputsToUpdate });
},
[
store,
@@ -1,431 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import {
CoreObjectNameSingular,
FieldMetadataType,
PageLayoutTabLayoutMode,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import {
GRID_POSITIONS,
VERTICAL_LIST_LAYOUT_POSITIONS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
const HOME_TAB_POSITION = 10;
const isActivityTargetField = (
fieldName: string,
objectNameSingular: string,
): boolean =>
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldName === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldName === 'taskTargets');
const isJunctionRelationField = (field: FlatFieldMetadata): boolean => {
const settings = field.settings as
| { junctionTargetFieldId?: string }
| undefined;
return (
isDefined(settings?.junctionTargetFieldId) &&
typeof settings?.junctionTargetFieldId === 'string' &&
settings.junctionTargetFieldId.length > 0
);
};
const isRelationTargetAvailable = (
targetObject: FlatObjectMetadata | undefined,
): boolean => {
if (!isDefined(targetObject)) {
return false;
}
if (targetObject.isRemote) {
return false;
}
if (
targetObject.isSystem &&
targetObject.nameSingular !== CoreObjectNameSingular.WorkspaceMember
) {
return false;
}
return true;
};
@Command({
name: 'upgrade:1-20:backfill-field-widgets',
description:
'Backfill FIELD widgets for relation fields in existing page layouts',
})
export class BackfillFieldWidgetsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of FIELD widgets for workspace ${workspaceId}`,
);
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
]);
// Build a set of fieldMetadataIds that already have a FIELD widget
const existingFieldWidgetFieldIds = new Set<string>();
for (const widget of Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)) {
if (
isDefined(widget) &&
widget.configuration?.configurationType ===
WidgetConfigurationType.FIELD &&
isDefined(widget.configuration.fieldMetadataId)
) {
existingFieldWidgetFieldIds.add(widget.configuration.fieldMetadataId);
}
}
// Build object ID → objectMetadata map
const objectById = new Map<string, FlatObjectMetadata>();
for (const obj of Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)) {
if (isDefined(obj)) {
objectById.set(obj.id, obj);
}
}
// Build object ID → RECORD_PAGE layout map
const recordPageLayoutByObjectId = new Map<
string,
{ id: string; universalIdentifier: string }
>();
for (const layout of Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
)) {
if (
isDefined(layout) &&
layout.type === PageLayoutType.RECORD_PAGE &&
isDefined(layout.objectMetadataId)
) {
recordPageLayoutByObjectId.set(layout.objectMetadataId, {
id: layout.id,
universalIdentifier: layout.universalIdentifier,
});
}
}
// Build pageLayoutId → home tab map
const homeTabByPageLayoutId = new Map<
string,
{
id: string;
universalIdentifier: string;
widgetCount: number;
}
>();
for (const tab of Object.values(
flatPageLayoutTabMaps.byUniversalIdentifier,
)) {
if (
isDefined(tab) &&
tab.position === HOME_TAB_POSITION &&
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
) {
homeTabByPageLayoutId.set(tab.pageLayoutId, {
id: tab.id,
universalIdentifier: tab.universalIdentifier,
widgetCount: tab.widgetIds?.length ?? 0,
});
}
}
// Group fields by objectMetadataId
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
// Map morphId → all field IDs sharing that morphId (for dedup)
const fieldIdsByMorphId = new Map<string, string[]>();
for (const field of Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(field) || !isDefined(field.objectMetadataId)) {
continue;
}
const list = fieldsByObjectId.get(field.objectMetadataId) ?? [];
list.push(field);
fieldsByObjectId.set(field.objectMetadataId, list);
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
const morphFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
morphFieldIds.push(field.id);
fieldIdsByMorphId.set(field.morphId, morphFieldIds);
}
}
const now = new Date().toISOString();
const standardWidgetsToCreate: FlatPageLayoutWidget[] = [];
const customWidgetsToCreate: FlatPageLayoutWidget[] = [];
const processedMorphIds = new Set<string>();
for (const object of objectById.values()) {
const pageLayout = recordPageLayoutByObjectId.get(object.id);
if (!isDefined(pageLayout)) {
continue;
}
const homeTab = homeTabByPageLayoutId.get(pageLayout.id);
if (!isDefined(homeTab)) {
continue;
}
const flatApplication: FlatApplication = object.isCustom
? workspaceCustomFlatApplication
: twentyStandardFlatApplication;
const targetList: FlatPageLayoutWidget[] = object.isCustom
? customWidgetsToCreate
: standardWidgetsToCreate;
const fields = fieldsByObjectId.get(object.id) ?? [];
let nextWidgetIndex = homeTab.widgetCount;
for (const field of fields) {
if (
field.type !== FieldMetadataType.RELATION &&
field.type !== FieldMetadataType.MORPH_RELATION
) {
continue;
}
if (isActivityTargetField(field.name, object.nameSingular)) {
continue;
}
if (isJunctionRelationField(field)) {
continue;
}
if (field.type === FieldMetadataType.RELATION) {
const targetObject = isDefined(field.relationTargetObjectMetadataId)
? objectById.get(field.relationTargetObjectMetadataId)
: undefined;
if (!isRelationTargetAvailable(targetObject)) {
continue;
}
}
// For morph relations, skip if any sibling field (same morphId)
// already has a widget or was already processed in this run
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
if (processedMorphIds.has(field.morphId)) {
continue;
}
const siblingFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
const alreadyHasWidget = siblingFieldIds.some((id) =>
existingFieldWidgetFieldIds.has(id),
);
if (alreadyHasWidget) {
continue;
}
processedMorphIds.add(field.morphId);
}
if (existingFieldWidgetFieldIds.has(field.id)) {
continue;
}
const widget: FlatPageLayoutWidget = {
id: v4(),
universalIdentifier: v4(),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
pageLayoutTabId: homeTab.id,
pageLayoutTabUniversalIdentifier: homeTab.universalIdentifier,
title: field.label,
type: WidgetType.FIELD,
gridPosition: { ...GRID_POSITIONS.FULL_WIDTH },
position: {
...VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
index: nextWidgetIndex,
},
configuration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.id,
fieldDisplayMode: FieldDisplayMode.CARD,
},
universalConfiguration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.universalIdentifier,
fieldDisplayMode: FieldDisplayMode.CARD,
},
objectMetadataId: object.id,
objectMetadataUniversalIdentifier: object.universalIdentifier,
createdAt: now,
updatedAt: now,
deletedAt: null,
conditionalDisplay: null,
overrides: null,
};
targetList.push(widget);
existingFieldWidgetFieldIds.add(field.id);
nextWidgetIndex++;
}
}
const totalWidgets =
standardWidgetsToCreate.length + customWidgetsToCreate.length;
if (totalWidgets === 0) {
this.logger.log(
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
);
return;
}
this.logger.log(
`Found ${totalWidgets} FIELD widget(s) to create for workspace ${workspaceId} (${standardWidgetsToCreate.length} standard, ${customWidgetsToCreate.length} custom)`,
);
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would create ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
);
return;
}
if (standardWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: standardWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create standard FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create standard FIELD widgets for workspace ${workspaceId}`,
);
}
}
if (customWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: customWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create custom FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create custom FIELD widgets for workspace ${workspaceId}`,
);
}
}
this.logger.log(
`Successfully created ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,868 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import {
CoreObjectNameSingular,
FeatureFlagKey,
FieldMetadataType,
PageLayoutTabLayoutMode,
ViewType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import {
GRID_POSITIONS,
VERTICAL_LIST_LAYOUT_POSITIONS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
const HOME_TAB_POSITION = 10;
const isActivityTargetField = (
fieldName: string,
objectNameSingular: string,
): boolean =>
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldName === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldName === 'taskTargets');
const isJunctionRelationField = (field: FlatFieldMetadata): boolean => {
const settings = field.settings as
| { junctionTargetFieldId?: string }
| undefined;
return (
isDefined(settings?.junctionTargetFieldId) &&
typeof settings?.junctionTargetFieldId === 'string' &&
settings.junctionTargetFieldId.length > 0
);
};
const isRelationTargetAvailable = (
targetObject: FlatObjectMetadata | undefined,
): boolean => {
if (!isDefined(targetObject)) {
return false;
}
if (targetObject.isRemote) {
return false;
}
if (
targetObject.isSystem &&
targetObject.nameSingular !== CoreObjectNameSingular.WorkspaceMember
) {
return false;
}
return true;
};
@Command({
name: 'upgrade:1-20:backfill-page-layouts-and-fields-widget-view-fields',
description:
'Backfill RECORD_PAGE page layouts, sync FIELDS_WIDGET view fields, create FIELD widgets, and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
})
export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of page layouts and field widgets for workspace ${workspaceId}`,
);
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
workspaceId,
);
if (isAlreadyEnabled) {
this.logger.log(
`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED already enabled for workspace ${workspaceId}, skipping`,
);
return;
}
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would create page layouts, sync view fields, create FIELD widgets and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`,
);
return;
}
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
await this.backfillStandardPageLayoutsAndSyncViews({
workspaceId,
twentyStandardFlatApplication,
});
await this.backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
});
await this.backfillFieldWidgets({
workspaceId,
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
});
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED],
workspaceId,
);
this.logger.log(
`Successfully backfilled page layouts and field widgets for workspace ${workspaceId}`,
);
}
private async backfillStandardPageLayoutsAndSyncViews({
workspaceId,
twentyStandardFlatApplication,
}: {
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
}): Promise<void> {
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
shouldIncludeRecordPageLayouts: true,
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const recordPageLayoutUniversalIdentifiers = new Set<string>();
const recordPageLayoutsFromStandard = Object.values(
standardAllFlatEntityMaps.flatPageLayoutMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((pageLayout) => {
if (pageLayout.type !== PageLayoutType.RECORD_PAGE) {
return false;
}
recordPageLayoutUniversalIdentifiers.add(
pageLayout.universalIdentifier,
);
return true;
});
const tabUniversalIdentifiers = new Set<string>();
const tabsFromStandard = Object.values(
standardAllFlatEntityMaps.flatPageLayoutTabMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((tab) => {
if (
!recordPageLayoutUniversalIdentifiers.has(
tab.pageLayoutUniversalIdentifier,
)
) {
return false;
}
tabUniversalIdentifiers.add(tab.universalIdentifier);
return true;
});
const widgetsFromStandard = Object.values(
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((widget) =>
tabUniversalIdentifiers.has(widget.pageLayoutTabUniversalIdentifier),
);
const {
flatPageLayoutMaps: existingFlatPageLayoutMaps,
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
'flatViewMaps',
'flatViewFieldMaps',
'flatViewFieldGroupMaps',
]);
// Filter out page layouts, tabs, and widgets that already exist in the workspace
const pageLayoutsToCreate = recordPageLayoutsFromStandard.filter(
(pageLayout) =>
!isDefined(
existingFlatPageLayoutMaps.byUniversalIdentifier[
pageLayout.universalIdentifier
],
),
);
const pageLayoutTabsToCreate = tabsFromStandard.filter(
(tab) =>
!isDefined(
existingFlatPageLayoutTabMaps.byUniversalIdentifier[
tab.universalIdentifier
],
),
);
const pageLayoutWidgetsToCreate = widgetsFromStandard.filter(
(widget) =>
!isDefined(
existingFlatPageLayoutWidgetMaps.byUniversalIdentifier[
widget.universalIdentifier
],
),
);
// Collect all standard FIELDS_WIDGET view universal identifiers
// This includes both new views to create AND existing views (created by 1-19)
const allStandardFieldsWidgetViewUniversalIds = new Set<string>();
const viewsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((view) => {
if (view.type !== ViewType.FIELDS_WIDGET) {
return false;
}
allStandardFieldsWidgetViewUniversalIds.add(view.universalIdentifier);
if (
isDefined(
existingFlatViewMaps.byUniversalIdentifier[
view.universalIdentifier
],
)
) {
return false;
}
return true;
});
// Delete all existing viewFields for standard FIELDS_WIDGET views and recreate from current standard.
// This ensures positions, visibility, and groups match the current standard definition.
const viewFieldsToDelete = Object.values(
existingFlatViewFieldMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((viewField) =>
allStandardFieldsWidgetViewUniversalIds.has(
viewField.viewUniversalIdentifier,
),
);
const viewFieldsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((viewField) =>
allStandardFieldsWidgetViewUniversalIds.has(
viewField.viewUniversalIdentifier,
),
);
// Same delete-and-recreate approach for viewFieldGroups
const viewFieldGroupsToDelete = Object.values(
existingFlatViewFieldGroupMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((viewFieldGroup) =>
allStandardFieldsWidgetViewUniversalIds.has(
viewFieldGroup.viewUniversalIdentifier,
),
);
const viewFieldGroupsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewFieldGroupMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((viewFieldGroup) =>
allStandardFieldsWidgetViewUniversalIds.has(
viewFieldGroup.viewUniversalIdentifier,
),
);
this.logger.log(
`Creating ${pageLayoutsToCreate.length} page layouts, ${viewsToCreate.length} views, deleting ${viewFieldsToDelete.length} and creating ${viewFieldsToCreate.length} view fields, deleting ${viewFieldGroupsToDelete.length} and creating ${viewFieldGroupsToCreate.length} view field groups for workspace ${workspaceId}`,
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: pageLayoutsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: pageLayoutTabsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: pageLayoutWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: viewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: viewFieldsToCreate,
flatEntityToDelete: viewFieldsToDelete,
flatEntityToUpdate: [],
},
viewFieldGroup: {
flatEntityToCreate: viewFieldGroupsToCreate,
flatEntityToDelete: viewFieldGroupsToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to create standard page layouts and sync view fields:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create standard page layouts and sync view fields for workspace ${workspaceId}`,
);
}
}
private async backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
}: {
workspaceId: string;
workspaceCustomFlatApplication: FlatApplication;
}): Promise<void> {
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatPageLayoutMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutMaps',
]);
const existingPageLayouts = Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
).filter(isDefined);
const objectIdsWithRecordPageLayout = new Set(
existingPageLayouts
.filter(
(layout: FlatPageLayout) =>
layout.type === PageLayoutType.RECORD_PAGE &&
isDefined(layout.objectMetadataId),
)
.map((layout: FlatPageLayout) => layout.objectMetadataId),
);
const customObjectsWithoutPageLayout = Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(objectMetadata) =>
objectMetadata.isCustom &&
!objectMetadata.isRemote &&
!objectIdsWithRecordPageLayout.has(objectMetadata.id),
);
if (customObjectsWithoutPageLayout.length === 0) {
this.logger.log(
`No custom objects without page layouts found for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`Creating page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
);
const allCustomPageLayoutsToCreate: FlatPageLayout[] = [];
const allCustomPageLayoutTabsToCreate: FlatPageLayoutTab[] = [];
const allCustomPageLayoutWidgetsToCreate: FlatPageLayoutWidget[] = [];
const allCustomViewsToCreate: (UniversalFlatView & { id: string })[] = [];
const allCustomViewFieldsToCreate: UniversalFlatViewField[] = [];
for (const customObject of customObjectsWithoutPageLayout) {
const flatRecordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
});
const objectFieldMetadatas = Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((field) => field.objectMetadataId === customObject.id);
const viewFields = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: objectFieldMetadatas,
viewUniversalIdentifier: flatRecordPageFieldsView.universalIdentifier,
flatApplication: workspaceCustomFlatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
excludeLabelIdentifier: true,
});
const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } =
computeFlatDefaultRecordPageLayoutToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
recordPageFieldsView: flatRecordPageFieldsView,
workspaceId,
});
allCustomPageLayoutsToCreate.push(...pageLayouts);
allCustomPageLayoutTabsToCreate.push(...pageLayoutTabs);
allCustomPageLayoutWidgetsToCreate.push(...pageLayoutWidgets);
allCustomViewsToCreate.push(flatRecordPageFieldsView);
allCustomViewFieldsToCreate.push(...viewFields);
}
const customValidateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: allCustomPageLayoutsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: allCustomPageLayoutTabsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: allCustomPageLayoutWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: allCustomViewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: allCustomViewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (customValidateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to create custom object page layouts:\n${JSON.stringify(customValidateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create custom object page layouts for workspace ${workspaceId}`,
);
}
this.logger.log(
`Successfully created page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
);
}
private async backfillFieldWidgets({
workspaceId,
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
}: {
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
workspaceCustomFlatApplication: FlatApplication;
}): Promise<void> {
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatPageLayoutMaps,
flatPageLayoutTabMaps,
flatPageLayoutWidgetMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
]);
// Build a set of fieldMetadataIds that already have a FIELD widget
const existingFieldWidgetFieldIds = new Set<string>();
for (const widget of Object.values(
flatPageLayoutWidgetMaps.byUniversalIdentifier,
)) {
if (
isDefined(widget) &&
widget.configuration?.configurationType ===
WidgetConfigurationType.FIELD &&
isDefined(widget.configuration.fieldMetadataId)
) {
existingFieldWidgetFieldIds.add(widget.configuration.fieldMetadataId);
}
}
// Build object ID → objectMetadata map
const objectById = new Map<string, FlatObjectMetadata>();
for (const obj of Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)) {
if (isDefined(obj)) {
objectById.set(obj.id, obj);
}
}
// Build object ID → RECORD_PAGE layout map
const recordPageLayoutByObjectId = new Map<
string,
{ id: string; universalIdentifier: string }
>();
for (const layout of Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
)) {
if (
isDefined(layout) &&
layout.type === PageLayoutType.RECORD_PAGE &&
isDefined(layout.objectMetadataId)
) {
recordPageLayoutByObjectId.set(layout.objectMetadataId, {
id: layout.id,
universalIdentifier: layout.universalIdentifier,
});
}
}
// Build pageLayoutId → home tab map
const homeTabByPageLayoutId = new Map<
string,
{
id: string;
universalIdentifier: string;
widgetCount: number;
}
>();
for (const tab of Object.values(
flatPageLayoutTabMaps.byUniversalIdentifier,
)) {
if (
isDefined(tab) &&
tab.position === HOME_TAB_POSITION &&
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
) {
homeTabByPageLayoutId.set(tab.pageLayoutId, {
id: tab.id,
universalIdentifier: tab.universalIdentifier,
widgetCount: tab.widgetIds?.length ?? 0,
});
}
}
// Group fields by objectMetadataId
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
// Map morphId → all field IDs sharing that morphId (for dedup)
const fieldIdsByMorphId = new Map<string, string[]>();
for (const field of Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)) {
if (!isDefined(field) || !isDefined(field.objectMetadataId)) {
continue;
}
const list = fieldsByObjectId.get(field.objectMetadataId) ?? [];
list.push(field);
fieldsByObjectId.set(field.objectMetadataId, list);
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
const morphFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
morphFieldIds.push(field.id);
fieldIdsByMorphId.set(field.morphId, morphFieldIds);
}
}
const now = new Date().toISOString();
const standardWidgetsToCreate: FlatPageLayoutWidget[] = [];
const customWidgetsToCreate: FlatPageLayoutWidget[] = [];
const processedMorphIds = new Set<string>();
for (const object of objectById.values()) {
const pageLayout = recordPageLayoutByObjectId.get(object.id);
if (!isDefined(pageLayout)) {
continue;
}
const homeTab = homeTabByPageLayoutId.get(pageLayout.id);
if (!isDefined(homeTab)) {
continue;
}
const flatApplication: FlatApplication = object.isCustom
? workspaceCustomFlatApplication
: twentyStandardFlatApplication;
const targetList: FlatPageLayoutWidget[] = object.isCustom
? customWidgetsToCreate
: standardWidgetsToCreate;
const fields = fieldsByObjectId.get(object.id) ?? [];
let nextWidgetIndex = homeTab.widgetCount;
for (const field of fields) {
if (
field.type !== FieldMetadataType.RELATION &&
field.type !== FieldMetadataType.MORPH_RELATION
) {
continue;
}
if (isActivityTargetField(field.name, object.nameSingular)) {
continue;
}
if (isJunctionRelationField(field)) {
continue;
}
if (field.type === FieldMetadataType.RELATION) {
const targetObject = isDefined(field.relationTargetObjectMetadataId)
? objectById.get(field.relationTargetObjectMetadataId)
: undefined;
if (!isRelationTargetAvailable(targetObject)) {
continue;
}
}
// For morph relations, skip if any sibling field (same morphId)
// already has a widget or was already processed in this run
if (
field.type === FieldMetadataType.MORPH_RELATION &&
isDefined(field.morphId)
) {
if (processedMorphIds.has(field.morphId)) {
continue;
}
const siblingFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
const alreadyHasWidget = siblingFieldIds.some((id) =>
existingFieldWidgetFieldIds.has(id),
);
if (alreadyHasWidget) {
continue;
}
processedMorphIds.add(field.morphId);
}
if (existingFieldWidgetFieldIds.has(field.id)) {
continue;
}
const widget: FlatPageLayoutWidget = {
id: v4(),
universalIdentifier: v4(),
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
workspaceId,
pageLayoutTabId: homeTab.id,
pageLayoutTabUniversalIdentifier: homeTab.universalIdentifier,
title: field.label,
type: WidgetType.FIELD,
gridPosition: { ...GRID_POSITIONS.FULL_WIDTH },
position: {
...VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
index: nextWidgetIndex,
},
configuration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.id,
fieldDisplayMode: FieldDisplayMode.CARD,
},
universalConfiguration: {
configurationType: WidgetConfigurationType.FIELD,
fieldMetadataId: field.universalIdentifier,
fieldDisplayMode: FieldDisplayMode.CARD,
},
objectMetadataId: object.id,
objectMetadataUniversalIdentifier: object.universalIdentifier,
createdAt: now,
updatedAt: now,
deletedAt: null,
conditionalDisplay: null,
overrides: null,
};
targetList.push(widget);
existingFieldWidgetFieldIds.add(field.id);
nextWidgetIndex++;
}
}
const totalWidgets =
standardWidgetsToCreate.length + customWidgetsToCreate.length;
if (totalWidgets === 0) {
this.logger.log(
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
);
return;
}
this.logger.log(
`Found ${totalWidgets} FIELD widget(s) to create for workspace ${workspaceId} (${standardWidgetsToCreate.length} standard, ${customWidgetsToCreate.length} custom)`,
);
if (standardWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: standardWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create standard FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create standard FIELD widgets for workspace ${workspaceId}`,
);
}
}
if (customWidgetsToCreate.length > 0) {
const result =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayoutWidget: {
flatEntityToCreate: customWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (result.status === 'fail') {
this.logger.error(
`Failed to create custom FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
);
throw new Error(
`Failed to create custom FIELD widgets for workspace ${workspaceId}`,
);
}
}
this.logger.log(
`Successfully created ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
);
}
}
@@ -1,485 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { FeatureFlagKey, ViewType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
@Command({
name: 'upgrade:1-20:backfill-page-layouts',
description:
'Backfill RECORD_PAGE page layouts for legacy workspaces and enable the IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED feature flag',
})
export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of page layouts for workspace ${workspaceId}`,
);
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
workspaceId,
);
if (isAlreadyEnabled) {
this.logger.log(
`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED already enabled for workspace ${workspaceId}, skipping`,
);
return;
}
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
await this.backfillStandardObjectPageLayouts({
workspaceId,
twentyStandardFlatApplication,
isDryRun,
});
await this.backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
isDryRun,
});
if (!isDryRun) {
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED],
workspaceId,
);
}
this.logger.log(
isDryRun
? `[DRY RUN] Would create page layouts and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`
: `Successfully created page layouts and enabled IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`,
);
}
private async backfillStandardObjectPageLayouts({
workspaceId,
twentyStandardFlatApplication,
isDryRun,
}: {
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
isDryRun: boolean;
}): Promise<void> {
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
shouldIncludeRecordPageLayouts: true,
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const {
flatPageLayoutMaps: existingFlatPageLayoutMaps,
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
flatViewMaps: existingFlatViewMaps,
flatViewFieldMaps: existingFlatViewFieldMaps,
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatPageLayoutMaps',
'flatPageLayoutTabMaps',
'flatPageLayoutWidgetMaps',
'flatViewMaps',
'flatViewFieldMaps',
'flatViewFieldGroupMaps',
]);
const recordPageLayoutUniversalIdentifiers = new Set<string>();
const pageLayoutsToCreate = Object.values(
standardAllFlatEntityMaps.flatPageLayoutMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((pageLayout) => {
if (pageLayout.type !== PageLayoutType.RECORD_PAGE) {
return false;
}
recordPageLayoutUniversalIdentifiers.add(
pageLayout.universalIdentifier,
);
if (
isDefined(
existingFlatPageLayoutMaps.byUniversalIdentifier[
pageLayout.universalIdentifier
],
)
) {
return false;
}
return true;
});
const tabUniversalIdentifiers = new Set<string>();
const pageLayoutTabsToCreate = Object.values(
standardAllFlatEntityMaps.flatPageLayoutTabMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((tab) => {
if (
!recordPageLayoutUniversalIdentifiers.has(
tab.pageLayoutUniversalIdentifier,
)
) {
return false;
}
tabUniversalIdentifiers.add(tab.universalIdentifier);
if (
isDefined(
existingFlatPageLayoutTabMaps.byUniversalIdentifier[
tab.universalIdentifier
],
)
) {
return false;
}
return true;
});
const pageLayoutWidgetsToCreate = Object.values(
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(widget) =>
tabUniversalIdentifiers.has(
widget.pageLayoutTabUniversalIdentifier,
) &&
!isDefined(
existingFlatPageLayoutWidgetMaps.byUniversalIdentifier[
widget.universalIdentifier
],
),
);
const viewUniversalIdentifiers = new Set<string>();
const viewsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((view) => {
if (view.type !== ViewType.FIELDS_WIDGET) {
return false;
}
if (
isDefined(
existingFlatViewMaps.byUniversalIdentifier[
view.universalIdentifier
],
)
) {
return false;
}
viewUniversalIdentifiers.add(view.universalIdentifier);
return true;
});
const viewFieldsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(viewField) =>
viewUniversalIdentifiers.has(viewField.viewUniversalIdentifier) &&
!isDefined(
existingFlatViewFieldMaps.byUniversalIdentifier[
viewField.universalIdentifier
],
),
);
const viewFieldGroupsToCreate = Object.values(
standardAllFlatEntityMaps.flatViewFieldGroupMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(viewFieldGroup) =>
viewUniversalIdentifiers.has(
viewFieldGroup.viewUniversalIdentifier,
) &&
!isDefined(
existingFlatViewFieldGroupMaps.byUniversalIdentifier[
viewFieldGroup.universalIdentifier
],
),
);
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Found standard entities to create for workspace ${workspaceId}: ` +
`${pageLayoutsToCreate.length} page layout(s), ` +
`${pageLayoutTabsToCreate.length} tab(s), ` +
`${pageLayoutWidgetsToCreate.length} widget(s), ` +
`${viewsToCreate.length} view(s), ` +
`${viewFieldsToCreate.length} view field(s), ` +
`${viewFieldGroupsToCreate.length} view field group(s)`,
);
if (isDryRun) {
return;
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: pageLayoutsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: pageLayoutTabsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: pageLayoutWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: viewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: viewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewFieldGroup: {
flatEntityToCreate: viewFieldGroupsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to create standard page layouts:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create standard page layouts for workspace ${workspaceId}`,
);
}
}
private async backfillCustomObjectPageLayouts({
workspaceId,
workspaceCustomFlatApplication,
isDryRun,
}: {
workspaceId: string;
workspaceCustomFlatApplication: FlatApplication;
isDryRun: boolean;
}): Promise<void> {
const {
flatObjectMetadataMaps,
flatFieldMetadataMaps,
flatPageLayoutMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatPageLayoutMaps',
]);
const existingPageLayouts = Object.values(
flatPageLayoutMaps.byUniversalIdentifier,
).filter(isDefined);
const objectIdsWithRecordPageLayout = new Set(
existingPageLayouts
.filter(
(layout: FlatPageLayout) =>
layout.type === PageLayoutType.RECORD_PAGE &&
isDefined(layout.objectMetadataId),
)
.map((layout: FlatPageLayout) => layout.objectMetadataId),
);
const customObjectsWithoutPageLayout = Object.values(
flatObjectMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter(
(objectMetadata) =>
objectMetadata.isCustom &&
!objectMetadata.isRemote &&
!objectIdsWithRecordPageLayout.has(objectMetadata.id),
);
if (customObjectsWithoutPageLayout.length === 0) {
this.logger.log(
`No custom objects without page layouts found for workspace ${workspaceId}`,
);
return;
}
const allCustomPageLayoutsToCreate: FlatPageLayout[] = [];
const allCustomPageLayoutTabsToCreate: FlatPageLayoutTab[] = [];
const allCustomPageLayoutWidgetsToCreate: FlatPageLayoutWidget[] = [];
const allCustomViewsToCreate: (UniversalFlatView & { id: string })[] = [];
const allCustomViewFieldsToCreate: UniversalFlatViewField[] = [];
for (const customObject of customObjectsWithoutPageLayout) {
const flatRecordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
});
const objectFieldMetadatas = Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
)
.filter(isDefined)
.filter((field) => field.objectMetadataId === customObject.id);
const viewFields = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: objectFieldMetadatas,
viewUniversalIdentifier: flatRecordPageFieldsView.universalIdentifier,
flatApplication: workspaceCustomFlatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
});
const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } =
computeFlatDefaultRecordPageLayoutToCreate({
objectMetadata: customObject,
flatApplication: workspaceCustomFlatApplication,
recordPageFieldsView: flatRecordPageFieldsView,
workspaceId,
});
allCustomPageLayoutsToCreate.push(...pageLayouts);
allCustomPageLayoutTabsToCreate.push(...pageLayoutTabs);
allCustomPageLayoutWidgetsToCreate.push(...pageLayoutWidgets);
allCustomViewsToCreate.push(flatRecordPageFieldsView);
allCustomViewFieldsToCreate.push(...viewFields);
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Found custom entities to create for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}: ` +
`${allCustomPageLayoutsToCreate.length} page layout(s), ` +
`${allCustomPageLayoutTabsToCreate.length} tab(s), ` +
`${allCustomPageLayoutWidgetsToCreate.length} widget(s), ` +
`${allCustomViewsToCreate.length} view(s), ` +
`${allCustomViewFieldsToCreate.length} view field(s)`,
);
if (isDryRun) {
return;
}
const customValidateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
pageLayout: {
flatEntityToCreate: allCustomPageLayoutsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutTab: {
flatEntityToCreate: allCustomPageLayoutTabsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
pageLayoutWidget: {
flatEntityToCreate: allCustomPageLayoutWidgetsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
view: {
flatEntityToCreate: allCustomViewsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
viewField: {
flatEntityToCreate: allCustomViewFieldsToCreate,
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (customValidateAndBuildResult.status === 'fail') {
this.logger.error(
`Failed to create custom object page layouts:\n${JSON.stringify(customValidateAndBuildResult, null, 2)}`,
);
throw new Error(
`Failed to create custom object page layouts for workspace ${workspaceId}`,
);
}
this.logger.log(
`Successfully created page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
);
}
}
@@ -1,67 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@Command({
name: 'upgrade:1-20:generate-application-sdk-clients',
description:
'Generate SDK client archives for all existing applications so drivers do not crash with ARCHIVE_NOT_FOUND',
})
export class GenerateApplicationSdkClientsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly sdkClientGenerationService: SdkClientGenerationService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const dryRun = options.dryRun ?? false;
const applications = await this.applicationRepository.find({
where: { workspaceId },
});
this.logger.log(
`Found ${applications.length} application(s) in workspace ${workspaceId}`,
);
for (const application of applications) {
if (dryRun) {
this.logger.log(
`[DRY RUN] Would generate SDK client for application ${application.universalIdentifier} (${application.id})`,
);
continue;
}
try {
await this.sdkClientGenerationService.generateSdkClientForApplication({
workspaceId,
applicationId: application.id,
applicationUniversalIdentifier: application.universalIdentifier,
});
} catch (error) {
this.logger.error(
`Failed to generate SDK client for application ${application.universalIdentifier} (${application.id}): ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
}
@@ -2,12 +2,10 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
import { BackfillFieldWidgetsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-field-widgets.command';
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
import { GenerateApplicationSdkClientsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-generate-application-sdk-clients.command';
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
@@ -18,10 +16,8 @@ import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-vers
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command';
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
@@ -41,7 +37,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
imports: [
TypeOrmModule.forFeature([
WorkspaceEntity,
ApplicationEntity,
ConnectedAccountEntity,
MessageChannelEntity,
CalendarChannelEntity,
@@ -58,7 +53,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
ApplicationRegistrationModule,
WorkspaceMigrationModule,
FeatureFlagModule,
SdkClientModule,
WorkflowCommonModule,
],
providers: [
@@ -67,12 +61,10 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
IdentifyObjectPermissionMetadataCommand,
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
BackfillCommandMenuItemsCommand,
BackfillFieldWidgetsCommand,
BackfillNavigationMenuItemTypeCommand,
BackfillPageLayoutsCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
BackfillSelectFieldOptionIdsCommand,
DeleteOrphanNavigationMenuItemsCommand,
GenerateApplicationSdkClientsCommand,
SeedCliApplicationRegistrationCommand,
MigrateRichTextToTextCommand,
MigrateMessagingInfrastructureToMetadataCommand,
@@ -85,12 +77,10 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
IdentifyObjectPermissionMetadataCommand,
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
BackfillCommandMenuItemsCommand,
BackfillFieldWidgetsCommand,
BackfillNavigationMenuItemTypeCommand,
BackfillPageLayoutsCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
BackfillSelectFieldOptionIdsCommand,
DeleteOrphanNavigationMenuItemsCommand,
GenerateApplicationSdkClientsCommand,
SeedCliApplicationRegistrationCommand,
MigrateRichTextToTextCommand,
MigrateMessagingInfrastructureToMetadataCommand,
@@ -34,11 +34,10 @@ import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgra
import { FixInvalidStandardUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command';
import { SeedServerIdCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-seed-server-id.command';
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
import { BackfillFieldWidgetsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-field-widgets.command';
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
@@ -46,7 +45,6 @@ import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrati
import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command';
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command';
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
import { GenerateApplicationSdkClientsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-generate-application-sdk-clients.command';
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -106,12 +104,10 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand,
protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand,
protected readonly deleteOrphanNavigationMenuItemsCommand: DeleteOrphanNavigationMenuItemsCommand,
protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand,
protected readonly generateApplicationSdkClientsCommand: GenerateApplicationSdkClientsCommand,
protected readonly backfillPageLayoutsAndFieldsWidgetViewFieldsCommand: BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
protected readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand,
protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand,
protected readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand,
protected readonly backfillFieldWidgetsCommand: BackfillFieldWidgetsCommand,
protected readonly backfillSelectFieldOptionIdsCommand: BackfillSelectFieldOptionIdsCommand,
protected readonly updateStandardIndexViewNamesCommand: UpdateStandardIndexViewNamesCommand,
protected readonly makeWorkflowSearchableCommand: MakeWorkflowSearchableCommand,
@@ -172,14 +168,12 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.migrateRichTextToTextCommand,
this.deleteOrphanNavigationMenuItemsCommand,
this.backfillCommandMenuItemsCommand,
this.backfillPageLayoutsCommand,
this.backfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
this.seedCliApplicationRegistrationCommand,
this.migrateMessagingInfrastructureToMetadataCommand,
this.backfillFieldWidgetsCommand,
this.backfillSelectFieldOptionIdsCommand,
this.updateStandardIndexViewNamesCommand,
this.makeWorkflowSearchableCommand,
this.generateApplicationSdkClientsCommand,
];
this.allCommands = {
@@ -721,6 +721,7 @@ export class LambdaDriver implements LogicFunctionDriver {
const sdkArchiveBuffer =
await this.sdkClientArchiveService.downloadArchiveBuffer({
workspaceId: flatApplication.workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier,
});
@@ -113,6 +113,7 @@ export class LocalDriver implements LogicFunctionDriver {
await this.sdkClientArchiveService.downloadAndExtractToPackage({
workspaceId: flatApplication.workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier,
targetPackagePath: sdkPackagePath,
});
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
import crypto from 'crypto';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';
import { type QueryRunner } from 'typeorm';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -40,6 +41,7 @@ type UpdateSourceFilesParams = Omit<
'builtHandlerPath'
> & {
sourceHandlerCode: string;
queryRunner?: QueryRunner;
};
type GetSourceCodeParams = Identifier & {
@@ -128,6 +130,7 @@ export class LogicFunctionResourceService {
workspaceId,
applicationUniversalIdentifier,
sourceHandlerCode,
queryRunner,
}: UpdateSourceFilesParams): Promise<void> {
await this.fileStorageService.writeFile({
workspaceId,
@@ -137,6 +140,20 @@ export class LogicFunctionResourceService {
sourceFile: sourceHandlerCode,
settings: { isTemporaryFile: false, toDelete: false },
mimeType: 'application/typescript',
queryRunner,
});
}
async deleteSourceFile({
sourceHandlerPath,
workspaceId,
applicationUniversalIdentifier,
}: GetSourceCodeParams): Promise<void> {
await this.fileStorageService.delete({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: sourceHandlerPath,
});
}
@@ -59,6 +59,7 @@ export class SdkClientController {
const fileBuffer =
await this.sdkClientArchiveService.getClientModuleFromArchive({
workspaceId: workspace.id,
applicationId,
applicationUniversalIdentifier: application.universalIdentifier,
moduleName,
});
@@ -1,11 +1,7 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { createWriteStream } from 'fs';
import * as fs from 'fs/promises';
import { join } from 'path';
import { type Readable } from 'stream';
import { pipeline } from 'stream/promises';
import { FileFolder } from 'twenty-shared/types';
import { Repository } from 'typeorm';
@@ -16,12 +12,12 @@ import {
FileStorageException,
FileStorageExceptionCode,
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
import { type SdkModuleName } from 'src/engine/core-modules/sdk-client/constants/allowed-sdk-modules';
import {
SdkClientException,
SdkClientExceptionCode,
} from 'src/engine/core-modules/sdk-client/exceptions/sdk-client.exception';
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@@ -29,75 +25,74 @@ const SDK_CLIENT_ARCHIVE_NAME = 'twenty-client-sdk.zip';
@Injectable()
export class SdkClientArchiveService {
private readonly logger = new Logger(SdkClientArchiveService.name);
constructor(
private readonly fileStorageService: FileStorageService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly sdkClientGenerationService: SdkClientGenerationService,
) {}
async downloadAndExtractToPackage({
workspaceId,
applicationId,
applicationUniversalIdentifier,
targetPackagePath,
}: {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
targetPackagePath: string;
}): Promise<void> {
const temporaryDirManager = new TemporaryDirManager();
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
workspaceId,
applicationId,
applicationUniversalIdentifier,
});
try {
const { sourceTemporaryDir } = await temporaryDirManager.init();
const archivePath = join(sourceTemporaryDir, SDK_CLIENT_ARCHIVE_NAME);
await fs.rm(targetPackagePath, { recursive: true, force: true });
await fs.mkdir(targetPackagePath, { recursive: true });
const archiveStream = await this.readArchiveStream({
workspaceId,
applicationUniversalIdentifier,
});
const { default: unzipper } = await import('unzipper');
const directory = await unzipper.Open.buffer(archiveBuffer);
await pipeline(archiveStream, createWriteStream(archivePath));
await fs.rm(targetPackagePath, { recursive: true, force: true });
await fs.mkdir(targetPackagePath, { recursive: true });
const { default: unzipper } = await import('unzipper');
const directory = await unzipper.Open.file(archivePath);
await directory.extract({ path: targetPackagePath });
} finally {
await temporaryDirManager.clean();
}
await directory.extract({ path: targetPackagePath });
}
async downloadArchiveBuffer({
workspaceId,
applicationId,
applicationUniversalIdentifier,
}: {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): Promise<Buffer> {
const archiveStream = await this.readArchiveStream({
return this.downloadArchiveBufferOrGenerate({
workspaceId,
applicationId,
applicationUniversalIdentifier,
});
return streamToBuffer(archiveStream);
}
async getClientModuleFromArchive({
workspaceId,
applicationId,
applicationUniversalIdentifier,
moduleName,
}: {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
moduleName: SdkModuleName;
}): Promise<Buffer> {
const filePath = `dist/${moduleName}.mjs`;
const archiveBuffer = await this.downloadArchiveBuffer({
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
workspaceId,
applicationId,
applicationUniversalIdentifier,
});
@@ -135,32 +130,41 @@ export class SdkClientArchiveService {
]);
}
private async readArchiveStream({
private async downloadArchiveBufferOrGenerate({
workspaceId,
applicationId,
applicationUniversalIdentifier,
}: {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): Promise<Readable> {
}): Promise<Buffer> {
try {
return await this.fileStorageService.readFile({
const stream = await this.fileStorageService.readFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.GeneratedSdkClient,
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
});
return await streamToBuffer(stream);
} catch (error) {
if (
error instanceof FileStorageException &&
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
!(error instanceof FileStorageException) ||
error.code !== FileStorageExceptionCode.FILE_NOT_FOUND
) {
throw new SdkClientException(
`SDK client archive "${SDK_CLIENT_ARCHIVE_NAME}" not found for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}".`,
SdkClientExceptionCode.ARCHIVE_NOT_FOUND,
);
throw error;
}
throw error;
}
this.logger.warn(
`SDK client archive missing for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}", generating on-the-fly`,
);
return this.sdkClientGenerationService.generateSdkClientForApplication({
workspaceId,
applicationId,
applicationUniversalIdentifier,
});
}
}
@@ -44,13 +44,13 @@ export class SdkClientGenerationService {
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): Promise<void> {
}): Promise<Buffer> {
const graphqlSchema = await this.workspaceSchemaFactory.createGraphQLSchema(
{ id: workspaceId } as WorkspaceEntity,
applicationId,
);
await this.generateAndStore({
const archiveBuffer = await this.generateAndStore({
workspaceId,
applicationId,
applicationUniversalIdentifier,
@@ -60,6 +60,8 @@ export class SdkClientGenerationService {
this.logger.log(
`Generated SDK client for application ${applicationUniversalIdentifier}`,
);
return archiveBuffer;
}
private async generateAndStore({
@@ -72,7 +74,7 @@ export class SdkClientGenerationService {
applicationId: string;
applicationUniversalIdentifier: string;
schema: string;
}): Promise<void> {
}): Promise<Buffer> {
const temporaryDirManager = new TemporaryDirManager();
try {
@@ -101,12 +103,14 @@ export class SdkClientGenerationService {
await createZipFile(tempPackageRoot, archivePath);
const archiveBuffer = await fs.readFile(archivePath);
await this.fileStorageService.writeFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.GeneratedSdkClient,
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
sourceFile: await fs.readFile(archivePath),
sourceFile: archiveBuffer,
mimeType: 'application/zip',
settings: { isTemporaryFile: false, toDelete: false },
});
@@ -119,6 +123,8 @@ export class SdkClientGenerationService {
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'flatApplicationMaps',
]);
return archiveBuffer;
} catch (error) {
throw new SdkClientException(
`Failed to generate SDK client for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}": ${error instanceof Error ? error.message : String(error)}`,
@@ -33,6 +33,7 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
describe('WorkspaceService', () => {
@@ -119,6 +120,7 @@ describe('WorkspaceService', () => {
PermissionsService,
FileCorePictureService,
AiModelRegistryService,
PrefillLogicFunctionService,
].map((service) => ({
provide: service,
useValue: {},
@@ -53,12 +53,14 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-dashboards';
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-opportunities';
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-dashboards.util';
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-opportunities.util';
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-people.util';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util';
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-migration/constant/default-feature-flags';
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
@@ -111,6 +113,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
private readonly permissionsService: PermissionsService,
private readonly dnsManagerService: DnsManagerService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly subdomainManagerService: SubdomainManagerService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@@ -681,6 +684,14 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
},
);
await this.prefillLogicFunctionService.ensureSeeded({
workspaceId,
definitions:
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
workspaceId,
),
});
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
@@ -694,6 +705,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
await prefillWorkflows(
queryRunner.manager,
workspaceId,
schemaName,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
@@ -720,6 +732,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
);
}
}
throw error;
} finally {
await queryRunner.release();
@@ -41,12 +41,14 @@ import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill.module';
@Module({
imports: [
TypeORMModule,
TypeOrmModule.forFeature([BillingSubscriptionEntity, WorkspaceEntity]),
MetricsModule,
StandardObjectsPrefillModule,
NestjsQueryGraphQLModule.forFeature({
imports: [
AuditModule,
@@ -79,6 +81,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ApplicationModule,
EnterpriseModule,
StandardObjectsPrefillModule,
],
services: [WorkspaceService],
resolvers: workspaceAutoResolverOpts,
File diff suppressed because it is too large Load Diff
@@ -118,11 +118,11 @@ export class MessageFolderDataAccessService {
): Promise<void> {
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
await workspaceRepository.save(data);
const savedData = await workspaceRepository.save(data);
if (await this.isMigrated(workspaceId)) {
try {
const coreData = await this.toCore(workspaceId, data);
const coreData = await this.toCore(workspaceId, savedData);
await this.coreRepository.save(
coreData as unknown as MessageFolderEntity,
@@ -455,6 +455,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
viewUniversalIdentifier:
flatRecordPageFieldsViewToCreate.universalIdentifier,
excludeLabelIdentifier: true,
});
flatDefaultRecordPageLayoutsToCreate =
@@ -0,0 +1,209 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
import { computeFlatViewFieldsToCreate } from '../compute-flat-view-fields-to-create.util';
const makeFieldMetadata = (
overrides: Partial<UniversalFlatFieldMetadata> & {
name: string;
type: FieldMetadataType;
},
): UniversalFlatFieldMetadata => {
const universalIdentifier = overrides.universalIdentifier ?? v4();
return {
universalIdentifier,
objectMetadataUniversalIdentifier: 'object-uid',
applicationUniversalIdentifier: 'app-uid',
name: overrides.name,
label: overrides.label ?? overrides.name,
type: overrides.type,
isCustom: overrides.isCustom ?? false,
isActive: true,
isSystem: false,
isUIReadOnly: false,
isNullable: true,
isUnique: false,
isLabelSyncedWithName: false,
defaultValue: null,
description: null,
icon: null,
options: null,
settings: null,
standardOverrides: null,
relationTargetObjectMetadataUniversalIdentifier: null,
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
deletedAt: null,
} as unknown as UniversalFlatFieldMetadata;
};
const flatApplication = {
universalIdentifier: 'app-uid',
id: 'app-id',
} as never;
const viewUniversalIdentifier = 'view-uid';
describe('computeFlatViewFieldsToCreate', () => {
it('should exclude TS_VECTOR fields', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'searchVector',
type: FieldMetadataType.TS_VECTOR,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should exclude POSITION fields', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'position',
type: FieldMetadataType.POSITION,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should exclude RELATION and MORPH_RELATION fields', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'company',
type: FieldMetadataType.RELATION,
}),
makeFieldMetadata({
name: 'target',
type: FieldMetadataType.MORPH_RELATION,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should exclude deletedAt field', () => {
const fields = [
makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
}),
makeFieldMetadata({
name: 'deletedAt',
type: FieldMetadataType.DATE_TIME,
}),
];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier: null,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
fields[0].universalIdentifier,
);
});
it('should place label identifier field first', () => {
const labelField = makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
});
const otherField = makeFieldMetadata({
name: 'createdAt',
type: FieldMetadataType.DATE_TIME,
});
const fields = [otherField, labelField];
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: fields,
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
labelField.universalIdentifier,
});
expect(result).toHaveLength(2);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
labelField.universalIdentifier,
);
expect(result[0].position).toBe(0);
expect(result[1].fieldMetadataUniversalIdentifier).toBe(
otherField.universalIdentifier,
);
expect(result[1].position).toBe(1);
});
it('should exclude label identifier when excludeLabelIdentifier is true', () => {
const labelField = makeFieldMetadata({
name: 'name',
type: FieldMetadataType.TEXT,
});
const otherField = makeFieldMetadata({
name: 'createdAt',
type: FieldMetadataType.DATE_TIME,
});
const result = computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas: [labelField, otherField],
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier:
labelField.universalIdentifier,
excludeLabelIdentifier: true,
});
expect(result).toHaveLength(1);
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
otherField.universalIdentifier,
);
});
});
@@ -11,22 +11,30 @@ export const computeFlatViewFieldsToCreate = ({
viewUniversalIdentifier,
flatApplication,
labelIdentifierFieldMetadataUniversalIdentifier,
excludeLabelIdentifier = false,
}: {
flatApplication: FlatApplication;
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
viewUniversalIdentifier: string;
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
excludeLabelIdentifier?: boolean;
}): UniversalFlatViewField[] => {
const createdAt = new Date().toISOString();
const defaultViewFields = objectFlatFieldMetadatas
.filter(
(field) =>
field.name !== 'deletedAt' &&
field.type !== FieldMetadataType.TS_VECTOR &&
field.type !== FieldMetadataType.POSITION &&
field.type !== FieldMetadataType.MORPH_RELATION &&
field.type !== FieldMetadataType.RELATION &&
// Include 'id' only if it's the label identifier (e.g., for junction tables)
(field.name !== 'id' ||
field.universalIdentifier ===
labelIdentifierFieldMetadataUniversalIdentifier) &&
// Exclude label identifier field when requested (e.g., for FIELDS_WIDGET views)
(!excludeLabelIdentifier ||
field.universalIdentifier !==
labelIdentifierFieldMetadataUniversalIdentifier),
)
.sort((a, b) => {
@@ -84,6 +84,19 @@ export class ViewGroupResolver {
});
}
@Mutation(() => [ViewGroupDTO])
@UseGuards(UpdateViewGroupPermissionGuard)
async updateManyViewGroups(
@Args('inputs', { type: () => [UpdateViewGroupInput] })
updateViewGroupInputs: UpdateViewGroupInput[],
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewGroupDTO[]> {
return await this.viewGroupService.updateMany({
updateViewGroupInputs,
workspaceId,
});
}
@Mutation(() => ViewGroupDTO)
@UseGuards(DeleteViewGroupPermissionGuard)
async deleteViewGroup(
@@ -7,6 +7,7 @@ import { IsNull, Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { fromCreateViewGroupInputToFlatViewGroupToCreate } from 'src/engine/metadata-modules/flat-view-group/utils/from-create-view-group-input-to-flat-view-group-to-create.util';
@@ -152,6 +153,32 @@ export class ViewGroupService {
workspaceId: string;
updateViewGroupInput: UpdateViewGroupInput;
}): Promise<ViewGroupDTO> {
const [updatedViewGroup] = await this.updateMany({
updateViewGroupInputs: [updateViewGroupInput],
workspaceId,
});
if (!isDefined(updatedViewGroup)) {
throw new ViewGroupException(
'Failed to update view group',
ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
);
}
return updatedViewGroup;
}
async updateMany({
updateViewGroupInputs,
workspaceId,
}: {
updateViewGroupInputs: UpdateViewGroupInput[];
workspaceId: string;
}): Promise<ViewGroupDTO[]> {
if (updateViewGroupInputs.length === 0) {
return [];
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
@@ -167,11 +194,13 @@ export class ViewGroupService {
},
);
const optimisticallyUpdatedFlatViewGroup =
fromUpdateViewGroupInputToFlatViewGroupToUpdateOrThrow({
flatViewGroupMaps: existingFlatViewGroupMaps,
updateViewGroupInput,
});
const flatViewGroupsToUpdate = updateViewGroupInputs.map(
(updateViewGroupInput) =>
fromUpdateViewGroupInputToFlatViewGroupToUpdateOrThrow({
flatViewGroupMaps: existingFlatViewGroupMaps,
updateViewGroupInput,
}),
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
@@ -180,7 +209,7 @@ export class ViewGroupService {
viewGroup: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [optimisticallyUpdatedFlatViewGroup],
flatEntityToUpdate: flatViewGroupsToUpdate,
},
},
workspaceId,
@@ -193,7 +222,7 @@ export class ViewGroupService {
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating view group',
'Multiple validation errors occurred while updating view groups',
);
}
@@ -205,12 +234,13 @@ export class ViewGroupService {
},
);
return fromFlatViewGroupToViewGroupDto(
findFlatEntityByUniversalIdentifierOrThrow({
universalIdentifier:
optimisticallyUpdatedFlatViewGroup.universalIdentifier,
flatEntityMaps: recomputedExistingFlatViewGroupMaps,
}),
return updateViewGroupInputs.map(({ id }) =>
fromFlatViewGroupToViewGroupDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedExistingFlatViewGroupMaps,
}),
),
);
}
@@ -121,8 +121,10 @@ import {
WORKSPACE_MEMBER_DATA_SEED_COLUMNS,
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util';
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
type RecordSeedConfig = {
@@ -304,6 +306,7 @@ export class DevSeederDataService {
private readonly timelineActivitySeederService: TimelineActivitySeederService,
private readonly fileStorageService: FileStorageService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
) {}
public async seed({
@@ -331,6 +334,14 @@ export class DevSeederDataService {
const { seeds: attachmentSeeds, fileSeedMetadata: attachmentFileMeta } =
generateAttachmentSeedsForWorkspace(workspaceId);
await this.prefillLogicFunctionService.ensureSeeded({
workspaceId,
definitions:
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
workspaceId,
),
});
await this.coreDataSource.transaction(
async (entityManager: WorkspaceEntityManager) => {
await this.seedRecordsInBatches({
@@ -359,6 +370,7 @@ export class DevSeederDataService {
await prefillWorkflows(
entityManager,
workspaceId,
schemaName,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
@@ -26,6 +26,7 @@ import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/da
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill.module';
import { TwentyStandardApplicationModule } from 'src/engine/workspace-manager/twenty-standard-application/twenty-standard-application.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@@ -48,6 +49,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
TypeOrmModule.forFeature([WorkspaceEntity, ObjectMetadataEntity]),
ObjectPermissionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
StandardObjectsPrefillModule,
WorkspaceCacheModule,
WorkspaceMigrationModule,
TwentyStandardApplicationModule,
@@ -1,302 +0,0 @@
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type EntityManager } from 'typeorm';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
export const QUICK_LEAD_WORKFLOW_VERSION_ID =
'ac67974f-c524-4288-9d88-af8515400b68';
export const prefillWorkflows = async (
entityManager: EntityManager,
schemaName: string,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) => {
const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(
flatObjectMetadataMaps,
);
const companyObjectMetadataId = objectIdByNameSingular['company'];
const personObjectMetadataId = objectIdByNameSingular['person'];
if (
!isDefined(companyObjectMetadataId) ||
!isDefined(personObjectMetadataId)
) {
throw new Error('Company or person object metadata not found');
}
const companyObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: companyObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const personObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: personObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) {
throw new Error('Company or person object metadata not found');
}
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflow`, [
'id',
'name',
'lastPublishedVersionId',
'statuses',
'position',
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'createdByContext',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
])
.orIgnore()
.values([
{
id: QUICK_LEAD_WORKFLOW_ID,
name: 'Quick Lead',
lastPublishedVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
statuses: ['ACTIVE'],
position: 1,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
createdByContext: {},
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
])
.returning('*')
.execute();
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflowVersion`, [
'id',
'name',
'trigger',
'steps',
'status',
'position',
'workflowId',
])
.orIgnore()
.values([
{
id: QUICK_LEAD_WORKFLOW_VERSION_ID,
name: 'v1',
trigger: JSON.stringify({
name: 'Launch manually',
type: 'MANUAL',
settings: {
outputSchema: {},
icon: 'IconUserPlus',
availability: { type: 'GLOBAL', locations: undefined },
},
nextStepIds: ['6e089bc9-aabd-435f-865f-f31c01c8f4a7'],
}),
steps: JSON.stringify([
{
id: '6e089bc9-aabd-435f-865f-f31c01c8f4a7',
name: 'Quick Lead Form',
type: 'FORM',
valid: false,
settings: {
input: [
{
id: '14d669f0-5249-4fa4-b0bb-f8bd408328d5',
name: 'firstName',
type: 'TEXT',
label: 'First name',
placeholder: 'Tim',
},
{
id: '4eb6ce85-d231-4aef-9837-744490c026d0',
name: 'lastName',
type: 'TEXT',
label: 'Last Name',
placeholder: 'Apple',
},
{
id: 'adbf0e9f-1427-49be-b4fb-092b34d97350',
name: 'email',
type: 'TEXT',
label: 'Email',
placeholder: '[email protected]',
},
{
id: '4ffc7992-9e65-4a4d-9baf-b52e62f2c273',
name: 'jobTitle',
type: 'TEXT',
label: 'Job title',
placeholder: 'CEO',
},
{
id: '42f11926-04ea-4924-94a4-2293cc748362',
name: 'companyName',
type: 'TEXT',
label: 'Company name',
placeholder: 'Apple',
},
{
id: 'd6ca80ee-26cd-466d-91bf-984d7205451c',
name: 'companyDomain',
type: 'TEXT',
label: 'Company domain',
placeholder: 'https://www.apple.com',
},
],
outputSchema: {
email: {
type: 'TEXT',
label: 'Email',
value: 'My text',
isLeaf: true,
},
jobTitle: {
type: 'TEXT',
label: 'Job title',
value: 'My text',
isLeaf: true,
},
lastName: {
type: 'TEXT',
label: 'Last Name',
value: 'My text',
isLeaf: true,
},
firstName: {
type: 'TEXT',
label: 'First name',
value: 'My text',
isLeaf: true,
},
companyName: {
type: 'TEXT',
label: 'Company name',
value: 'My text',
isLeaf: true,
},
companyDomain: {
type: 'TEXT',
label: 'Company domain',
value: 'My text',
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: ['0715b6cd-7cc1-4b98-971b-00f54dfe643b'],
},
{
id: '0715b6cd-7cc1-4b98-971b-00f54dfe643b',
name: 'Create Company',
type: 'CREATE_RECORD',
valid: false,
settings: {
input: {
objectName: 'company',
objectRecord: {
name: '{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyName}}',
domainName: {
primaryLinkUrl:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyDomain}}',
primaryLinkLabel: '',
},
},
},
outputSchema: {
object: {
icon: 'IconBuildingSkyscraper',
label: 'Company',
value: 'A company',
isLeaf: true,
fieldIdName: 'id',
nameSingular: 'company',
},
_outputSchemaType: 'RECORD',
fields: generateObjectRecordFields({
objectMetadataInfo: {
flatObjectMetadata: companyObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}),
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: ['6f553ea7-b00e-4371-9d88-d8298568a246'],
},
{
id: '6f553ea7-b00e-4371-9d88-d8298568a246',
name: 'Create Person',
type: 'CREATE_RECORD',
valid: false,
settings: {
input: {
objectName: 'person',
objectRecord: {
name: {
lastName:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.lastName}}',
firstName:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.firstName}}',
},
emails: {
primaryEmail:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.email}}',
additionalEmails: [],
},
companyId: '{{0715b6cd-7cc1-4b98-971b-00f54dfe643b.id}}',
},
},
outputSchema: {
fields: generateObjectRecordFields({
objectMetadataInfo: {
flatObjectMetadata: personObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}),
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: null,
},
]),
status: 'ACTIVE',
position: 1,
workflowId: QUICK_LEAD_WORKFLOW_ID,
},
])
.returning('*')
.execute();
};
@@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { type PrefilledWorkflowCodeStepLogicFunctionDefinition } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
@Injectable()
export class PrefillLogicFunctionService {
constructor(
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async ensureSeeded({
workspaceId,
definitions,
}: {
workspaceId: string;
definitions: PrefilledWorkflowCodeStepLogicFunctionDefinition[];
}) {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
for (const definition of definitions) {
const existingLogicFunction = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: definition.id,
flatEntityMaps: flatLogicFunctionMaps,
});
if (isDefined(existingLogicFunction)) {
continue;
}
await this.logicFunctionFromSourceService.createOneFromSource({
workspaceId,
input: {
id: definition.id,
name: definition.name,
description: definition.description,
toolInputSchema: definition.toolInputSchema,
source: {
sourceHandlerCode: definition.sourceHandlerCode,
toolInputSchema: definition.toolInputSchema,
handlerName: 'main',
},
},
});
}
}
}
@@ -1,32 +0,0 @@
import { type DataSource, type EntityManager } from 'typeorm';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
export const standardObjectsPrefillData = async (
dataSource: DataSource,
schemaName: string,
workspaceId: string,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) => {
dataSource.transaction(async (entityManager: EntityManager) => {
await prefillCompanies(entityManager, schemaName);
await prefillPeople(entityManager, schemaName);
await prefillWorkflows(
entityManager,
schemaName,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
await prefillWorkflowCommandMenuItems(entityManager, workspaceId);
});
};
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
@Module({
imports: [LogicFunctionModule, WorkspaceManyOrAllFlatEntityMapsCacheModule],
providers: [PrefillLogicFunctionService],
exports: [PrefillLogicFunctionService],
})
export class StandardObjectsPrefillModule {}
@@ -7,14 +7,14 @@ import {
FIGMA_ID,
NOTION_ID,
STRIPE_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
import {
BRIAN_CHESKY_ID,
DARIO_AMODEI_ID,
DYLAN_FIELD_ID,
IVAN_ZHAO_ID,
PATRICK_COLLISON_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-people.util';
export const OPPORTUNITY_STRIPE_PLATFORM_MIGRATION_ID =
'822639e5-9bf7-40f1-8882-a11140362339';
@@ -7,7 +7,7 @@ import {
FIGMA_ID,
NOTION_ID,
STRIPE_ID,
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
export const BRIAN_CHESKY_ID = 'a2e78a5e-338b-46df-8811-fa08c7d19d35'; // Airbnb
export const DARIO_AMODEI_ID = '93c72d2e-e65c-44c4-99ad-f87f50349dcf'; // Anthropic
@@ -0,0 +1,336 @@
import { v5 as uuidv5 } from 'uuid';
const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE =
'd41b0a2d-aa97-44dc-ad5d-89774164af27';
export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds = (
workspaceId: string,
) => ({
extractDomainLogicFunctionId: uuidv5(
`${workspaceId}:create-company-when-adding-new-person:extract-domain`,
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
),
findMatchingCompanyByDomainLogicFunctionId: uuidv5(
`${workspaceId}:create-company-when-adding-new-person:find-matching-company-by-domain`,
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
),
isPersonalEmailLogicFunctionId: uuidv5(
`${workspaceId}:create-company-when-adding-new-person:is-personal-email`,
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
),
});
const EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA = {
type: 'object',
properties: {
email: {
type: 'string',
},
},
required: ['email'],
};
const IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA = {
type: 'object',
properties: {
primaryEmail: {
type: 'string',
},
},
required: ['primaryEmail'],
};
const FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA = {
type: 'object',
properties: {
companies: {
type: 'array',
items: {
type: 'object',
},
},
domain: {
type: 'string',
},
},
required: ['companies', 'domain'],
};
const EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
'ac.uk',
'co.in',
'co.jp',
'co.kr',
'co.nz',
'co.uk',
'co.za',
'com.ar',
'com.au',
'com.br',
'com.cn',
'com.hk',
'com.mx',
'com.sg',
'com.tr',
'com.tw',
'com.ua',
'gov.uk',
'ne.jp',
'net.au',
'org.au',
'org.uk',
]);
const getRegistrableDomain = (host) => {
const normalizedHost = host.toLowerCase().replace(/^www\\./, '');
const parts = normalizedHost.split('.').filter(Boolean);
if (parts.length <= 2) {
return normalizedHost;
}
const lastTwo = parts.slice(-2).join('.');
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
return parts.slice(-3).join('.');
}
return lastTwo;
};
export const main = async (params) => {
const email =
typeof params?.email === 'string' ? params.email.trim().toLowerCase() : '';
const domainFromEmail = email.split('@')[1] ?? '';
const domain = domainFromEmail ? getRegistrableDomain(domainFromEmail) : '';
return {
url: domain ? \`https://\${domain}\` : '',
domain,
};
};`;
const IS_PERSONAL_EMAIL_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
'ac.uk',
'co.in',
'co.jp',
'co.kr',
'co.nz',
'co.uk',
'co.za',
'com.ar',
'com.au',
'com.br',
'com.cn',
'com.hk',
'com.mx',
'com.sg',
'com.tr',
'com.tw',
'com.ua',
'gov.uk',
'ne.jp',
'net.au',
'org.au',
'org.uk',
]);
const PERSONAL_EMAIL_DOMAINS = new Set([
'aol.com',
'att.net',
'btinternet.com',
'comcast.net',
'fastmail.com',
'free.fr',
'gmx.com',
'gmx.de',
'googlemail.com',
'hotmail.com',
'hotmail.fr',
'icloud.com',
'laposte.net',
'live.com',
'mac.com',
'mail.com',
'me.com',
'msn.com',
'orange.fr',
'outlook.com',
'outlook.fr',
'pm.me',
'proton.me',
'protonmail.com',
'sfr.fr',
'verizon.net',
'wanadoo.fr',
'yahoo.co.jp',
'yahoo.co.uk',
'yahoo.com',
'yandex.com',
'yandex.ru',
'zoho.com',
'gmail.com',
]);
const getRegistrableDomain = (host) => {
const normalizedHost = host.toLowerCase().replace(/^www\\./, '');
const parts = normalizedHost.split('.').filter(Boolean);
if (parts.length <= 2) {
return normalizedHost;
}
const lastTwo = parts.slice(-2).join('.');
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
return parts.slice(-3).join('.');
}
return lastTwo;
};
export const main = async (params) => {
const email =
typeof params?.primaryEmail === 'string'
? params.primaryEmail.trim().toLowerCase()
: '';
if (!email || !email.includes('@')) {
return { isPersonal: true };
}
const host = email.split('@')[1] ?? '';
const registrableDomain = host ? getRegistrableDomain(host) : '';
return {
isPersonal:
!registrableDomain ||
PERSONAL_EMAIL_DOMAINS.has(host) ||
PERSONAL_EMAIL_DOMAINS.has(registrableDomain),
};
};`;
const FIND_MATCHING_COMPANY_BY_DOMAIN_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
'ac.uk',
'co.in',
'co.jp',
'co.kr',
'co.nz',
'co.uk',
'co.za',
'com.ar',
'com.au',
'com.br',
'com.cn',
'com.hk',
'com.mx',
'com.sg',
'com.tr',
'com.tw',
'com.ua',
'gov.uk',
'ne.jp',
'net.au',
'org.au',
'org.uk',
]);
const normalizeHost = (value) => {
if (typeof value !== 'string') {
return '';
}
return value
.trim()
.toLowerCase()
.replace(/^https?:\\/\\//, '')
.replace(/^www\\./, '')
.split('/')[0]
.split('?')[0]
.split('#')[0]
.split(':')[0];
};
const getRegistrableDomain = (value) => {
const normalizedHost = normalizeHost(value);
const parts = normalizedHost.split('.').filter(Boolean);
if (parts.length <= 2) {
return normalizedHost;
}
const lastTwo = parts.slice(-2).join('.');
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
return parts.slice(-3).join('.');
}
return lastTwo;
};
export const main = async (params) => {
const domain = getRegistrableDomain(params?.domain);
const companies = Array.isArray(params?.companies) ? params.companies : [];
const matchingCompany = companies.find((company) => {
const companyDomain = getRegistrableDomain(
company?.domainName?.primaryLinkUrl,
);
return companyDomain !== '' && companyDomain === domain;
});
const companyId =
typeof matchingCompany?.id === 'string' ? matchingCompany.id : '';
return {
companyId,
hasMatch: companyId !== '',
};
};`;
export type PrefilledWorkflowCodeStepLogicFunctionDefinition = {
id: string;
name: string;
description: string;
sourceHandlerCode: string;
toolInputSchema: object;
};
export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions =
(workspaceId: string): PrefilledWorkflowCodeStepLogicFunctionDefinition[] => {
const {
extractDomainLogicFunctionId,
findMatchingCompanyByDomainLogicFunctionId,
isPersonalEmailLogicFunctionId,
} =
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds(workspaceId);
return [
{
id: extractDomainLogicFunctionId,
name: 'Extract domain from email',
description:
'Extracts a normalized company domain and URL from a person email address.',
sourceHandlerCode: EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE,
toolInputSchema: EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA,
},
{
id: findMatchingCompanyByDomainLogicFunctionId,
name: 'Find matching company by domain',
description:
'Finds an existing company whose website matches a normalized registrable domain.',
sourceHandlerCode:
FIND_MATCHING_COMPANY_BY_DOMAIN_LOGIC_FUNCTION_SOURCE,
toolInputSchema: FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA,
},
{
id: isPersonalEmailLogicFunctionId,
name: 'Is this a personal email?',
description:
'Detects whether an email address belongs to a common personal email provider.',
sourceHandlerCode: IS_PERSONAL_EMAIL_LOGIC_FUNCTION_SOURCE,
toolInputSchema: IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA,
},
];
};
@@ -2,7 +2,7 @@ import { type EntityManager } from 'typeorm';
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
const QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =
@@ -0,0 +1,762 @@
import { FieldActorSource } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type EntityManager } from 'typeorm';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event';
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
export const QUICK_LEAD_WORKFLOW_VERSION_ID =
'ac67974f-c524-4288-9d88-af8515400b68';
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID =
'887c6c06-fbc5-4b45-8d6b-f7b6b0f40b12';
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID =
'0f276d7e-a950-41ab-ad98-35e80753dc58';
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_AUTOMATED_TRIGGER_ID =
'c54f5990-13a3-4c3b-b75d-df09e7843036';
export const prefillWorkflows = async (
entityManager: EntityManager,
workspaceId: string,
schemaName: string,
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) => {
const {
extractDomainLogicFunctionId,
findMatchingCompanyByDomainLogicFunctionId,
isPersonalEmailLogicFunctionId,
} = getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds(workspaceId);
const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(
flatObjectMetadataMaps,
);
const companyObjectMetadataId = objectIdByNameSingular['company'];
const personObjectMetadataId = objectIdByNameSingular['person'];
if (
!isDefined(companyObjectMetadataId) ||
!isDefined(personObjectMetadataId)
) {
throw new Error('Company or person object metadata not found');
}
const companyObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: companyObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
const personObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: personObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) {
throw new Error('Company or person object metadata not found');
}
const companyDomainNameFieldMetadata = Object.values(
flatFieldMetadataMaps.byUniversalIdentifier,
).find(
(fieldMetadata) =>
fieldMetadata?.objectMetadataId === companyObjectMetadataId &&
fieldMetadata?.name === 'domainName',
);
if (!isDefined(companyDomainNameFieldMetadata)) {
throw new Error('Company domainName field metadata not found');
}
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflow`, [
'id',
'name',
'lastPublishedVersionId',
'statuses',
'position',
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
'createdByContext',
'updatedBySource',
'updatedByWorkspaceMemberId',
'updatedByName',
])
.orIgnore()
.values([
{
id: QUICK_LEAD_WORKFLOW_ID,
name: 'Quick Lead',
lastPublishedVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
statuses: ['ACTIVE'],
position: 1,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
createdByContext: {},
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
{
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
name: 'Create company when adding a new person',
lastPublishedVersionId:
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID,
statuses: ['ACTIVE'],
position: 2,
createdBySource: FieldActorSource.SYSTEM,
createdByWorkspaceMemberId: null,
createdByName: 'System',
createdByContext: {},
updatedBySource: FieldActorSource.SYSTEM,
updatedByWorkspaceMemberId: null,
updatedByName: 'System',
},
])
.returning('*')
.execute();
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflowVersion`, [
'id',
'name',
'trigger',
'steps',
'status',
'position',
'workflowId',
])
.orIgnore()
.values([
{
id: QUICK_LEAD_WORKFLOW_VERSION_ID,
name: 'v1',
trigger: JSON.stringify({
name: 'Launch manually',
type: 'MANUAL',
settings: {
outputSchema: {},
icon: 'IconUserPlus',
availability: { type: 'GLOBAL', locations: undefined },
},
nextStepIds: ['6e089bc9-aabd-435f-865f-f31c01c8f4a7'],
}),
steps: JSON.stringify([
{
id: '6e089bc9-aabd-435f-865f-f31c01c8f4a7',
name: 'Quick Lead Form',
type: 'FORM',
valid: false,
settings: {
input: [
{
id: '14d669f0-5249-4fa4-b0bb-f8bd408328d5',
name: 'firstName',
type: 'TEXT',
label: 'First name',
placeholder: 'Tim',
},
{
id: '4eb6ce85-d231-4aef-9837-744490c026d0',
name: 'lastName',
type: 'TEXT',
label: 'Last Name',
placeholder: 'Apple',
},
{
id: 'adbf0e9f-1427-49be-b4fb-092b34d97350',
name: 'email',
type: 'TEXT',
label: 'Email',
placeholder: '[email protected]',
},
{
id: '4ffc7992-9e65-4a4d-9baf-b52e62f2c273',
name: 'jobTitle',
type: 'TEXT',
label: 'Job title',
placeholder: 'CEO',
},
{
id: '42f11926-04ea-4924-94a4-2293cc748362',
name: 'companyName',
type: 'TEXT',
label: 'Company name',
placeholder: 'Apple',
},
{
id: 'd6ca80ee-26cd-466d-91bf-984d7205451c',
name: 'companyDomain',
type: 'TEXT',
label: 'Company domain',
placeholder: 'https://www.apple.com',
},
],
outputSchema: {
email: {
type: 'TEXT',
label: 'Email',
value: 'My text',
isLeaf: true,
},
jobTitle: {
type: 'TEXT',
label: 'Job title',
value: 'My text',
isLeaf: true,
},
lastName: {
type: 'TEXT',
label: 'Last Name',
value: 'My text',
isLeaf: true,
},
firstName: {
type: 'TEXT',
label: 'First name',
value: 'My text',
isLeaf: true,
},
companyName: {
type: 'TEXT',
label: 'Company name',
value: 'My text',
isLeaf: true,
},
companyDomain: {
type: 'TEXT',
label: 'Company domain',
value: 'My text',
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: ['0715b6cd-7cc1-4b98-971b-00f54dfe643b'],
},
{
id: '0715b6cd-7cc1-4b98-971b-00f54dfe643b',
name: 'Create Company',
type: 'CREATE_RECORD',
valid: false,
settings: {
input: {
objectName: 'company',
objectRecord: {
name: '{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyName}}',
domainName: {
primaryLinkUrl:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyDomain}}',
primaryLinkLabel: '',
},
},
},
outputSchema: {
object: {
icon: 'IconBuildingSkyscraper',
label: 'Company',
value: 'A company',
isLeaf: true,
fieldIdName: 'id',
nameSingular: 'company',
},
_outputSchemaType: 'RECORD',
fields: generateObjectRecordFields({
objectMetadataInfo: {
flatObjectMetadata: companyObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}),
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: ['6f553ea7-b00e-4371-9d88-d8298568a246'],
},
{
id: '6f553ea7-b00e-4371-9d88-d8298568a246',
name: 'Create Person',
type: 'CREATE_RECORD',
valid: false,
settings: {
input: {
objectName: 'person',
objectRecord: {
name: {
lastName:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.lastName}}',
firstName:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.firstName}}',
},
emails: {
primaryEmail:
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.email}}',
additionalEmails: [],
},
companyId: '{{0715b6cd-7cc1-4b98-971b-00f54dfe643b.id}}',
},
},
outputSchema: {
fields: generateObjectRecordFields({
objectMetadataInfo: {
flatObjectMetadata: personObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
}),
},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
__typename: 'WorkflowAction',
nextStepIds: null,
},
]),
status: 'ACTIVE',
position: 1,
workflowId: QUICK_LEAD_WORKFLOW_ID,
},
{
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID,
name: 'v1',
trigger: JSON.stringify({
name: 'Record is created or updated',
type: 'DATABASE_EVENT',
settings: {
eventName: 'person.upserted',
fields: ['emails'],
outputSchema: generateFakeObjectRecordEvent(
{
flatObjectMetadata: personObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
},
DatabaseEventAction.UPSERTED,
),
},
nextStepIds: ['c30d7cbe-00e0-4966-bc1a-99b0a11a2cca'],
}),
steps: JSON.stringify([
{
id: 'c30d7cbe-00e0-4966-bc1a-99b0a11a2cca',
name: 'Is this a personal email?',
type: 'CODE',
valid: false,
position: {
x: 227.25,
y: 130,
},
settings: {
input: {
logicFunctionId: isPersonalEmailLogicFunctionId,
logicFunctionInput: {
primaryEmail:
'{{trigger.properties.after.emails.primaryEmail}}',
},
},
outputSchema: {
isPersonal: {
type: 'boolean',
label: 'isPersonal',
value: true,
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['01f3db05-aae5-4e4b-b361-96684f09c704'],
},
{
id: '01f3db05-aae5-4e4b-b361-96684f09c704',
name: 'If business email',
type: 'FILTER',
valid: false,
position: {
x: 249.25,
y: 260,
},
settings: {
input: {
stepFilters: [
{
id: '0e595385-9e18-4869-abfd-cf72952b124c',
type: 'boolean',
value: 'false',
operand: 'IS',
isFullRecord: false,
stepOutputKey:
'{{c30d7cbe-00e0-4966-bc1a-99b0a11a2cca.isPersonal}}',
stepFilterGroupId: '5204d5f5-7b23-428c-9f84-c37971d497d3',
positionInStepFilterGroup: 0,
},
],
stepFilterGroups: [
{
id: '5204d5f5-7b23-428c-9f84-c37971d497d3',
logicalOperator: 'AND',
},
],
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['1b01193b-8300-4d79-940b-44464bf45505'],
},
{
id: '1b01193b-8300-4d79-940b-44464bf45505',
name: 'Extract domain from email',
type: 'CODE',
valid: false,
position: {
x: 219.75,
y: 390,
},
settings: {
input: {
logicFunctionId: extractDomainLogicFunctionId,
logicFunctionInput: {
email: '{{trigger.properties.after.emails.primaryEmail}}',
},
},
outputSchema: {
url: {
icon: 'IconVariable',
type: 'string',
label: 'url',
value: 'https://twenty.com',
isLeaf: true,
},
domain: {
icon: 'IconVariable',
type: 'string',
label: 'domain',
value: 'twenty.com',
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['becb3acf-79bb-4672-8a42-3696e94957b5'],
},
{
id: 'becb3acf-79bb-4672-8a42-3696e94957b5',
name: 'Search Company',
type: 'FIND_RECORDS',
valid: false,
position: {
x: 247.75,
y: 520,
},
settings: {
input: {
limit: 25,
filter: {
recordFilters: [
{
id: 'a9b917a0-5c4c-4e8f-bf91-160d0b888693',
type: 'LINKS',
label: 'Domain Name',
value: '{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
operand: 'CONTAINS',
displayValue:
'{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
subFieldName: 'primaryLinkUrl',
fieldMetadataId: companyDomainNameFieldMetadata.id,
recordFilterGroupId:
'194e151c-cf46-4e8f-a48b-649c36082dfa',
},
],
recordFilterGroups: [
{
id: '194e151c-cf46-4e8f-a48b-649c36082dfa',
logicalOperator: 'AND',
},
],
},
objectName: 'company',
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['9d0b6ef2-aad2-4853-92e1-95f2abf10d5b'],
},
{
id: '9d0b6ef2-aad2-4853-92e1-95f2abf10d5b',
name: 'Find exact company match',
type: 'CODE',
valid: false,
position: {
x: 247.75,
y: 650,
},
settings: {
input: {
logicFunctionId: findMatchingCompanyByDomainLogicFunctionId,
logicFunctionInput: {
companies: '{{becb3acf-79bb-4672-8a42-3696e94957b5.all}}',
domain: '{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
},
},
outputSchema: {
companyId: {
type: 'string',
label: 'companyId',
value: '00000000-0000-0000-0000-000000000000',
isLeaf: true,
},
hasMatch: {
type: 'boolean',
label: 'hasMatch',
value: true,
isLeaf: true,
},
},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['0c99a900-656a-40e8-977e-5a7357be33b9'],
},
{
id: '0c99a900-656a-40e8-977e-5a7357be33b9',
name: 'If a company already exists',
type: 'IF_ELSE',
valid: false,
position: {
x: 216.25,
y: 780,
},
settings: {
input: {
branches: [
{
id: '1344c151-15ff-40e2-a1a3-925fabaf5b1c',
nextStepIds: ['ffdd4271-75d4-4805-b1f8-2167a113c3b2'],
filterGroupId: 'f5c41047-2a6e-49fb-968a-fa7789a90ee5',
},
{
id: 'fe6dd152-1103-4324-af51-3ff994d1f8a7',
nextStepIds: ['ddafb9db-a94f-40b9-a5c9-becce857edf7'],
},
],
stepFilters: [
{
id: '290cc6a3-08fd-4be5-b42e-966d0bb90ff7',
type: 'boolean',
value: 'true',
operand: 'IS',
isFullRecord: false,
stepOutputKey:
'{{9d0b6ef2-aad2-4853-92e1-95f2abf10d5b.hasMatch}}',
stepFilterGroupId: 'f5c41047-2a6e-49fb-968a-fa7789a90ee5',
positionInStepFilterGroup: 0,
},
],
stepFilterGroups: [
{
id: 'f5c41047-2a6e-49fb-968a-fa7789a90ee5',
logicalOperator: 'AND',
},
],
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
},
{
id: 'ffdd4271-75d4-4805-b1f8-2167a113c3b2',
name: 'Attach person to existing company',
type: 'UPDATE_RECORD',
valid: false,
position: {
x: 0,
y: 910,
},
settings: {
input: {
objectName: 'person',
objectRecord: {
companyId:
'{{9d0b6ef2-aad2-4853-92e1-95f2abf10d5b.companyId}}',
},
fieldsToUpdate: ['companyId'],
objectRecordId: '{{trigger.properties.after.id}}',
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
},
{
id: 'ddafb9db-a94f-40b9-a5c9-becce857edf7',
name: 'Create a new company',
type: 'CREATE_RECORD',
valid: false,
position: {
x: 440,
y: 910,
},
settings: {
input: {
objectName: 'company',
objectRecord: {
name: '{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
domainName: {
primaryLinkUrl:
'{{1b01193b-8300-4d79-940b-44464bf45505.url}}',
primaryLinkLabel:
'{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
},
},
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
nextStepIds: ['d5d5d6e1-391f-4142-83c1-670f7087f079'],
},
{
id: 'd5d5d6e1-391f-4142-83c1-670f7087f079',
name: 'Attach person to this company',
type: 'UPDATE_RECORD',
valid: false,
position: {
x: 420.5,
y: 1040,
},
settings: {
input: {
objectName: 'person',
objectRecord: {
companyId: '{{ddafb9db-a94f-40b9-a5c9-becce857edf7.id}}',
},
fieldsToUpdate: ['companyId'],
objectRecordId: '{{trigger.properties.after.id}}',
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: {
value: false,
},
continueOnFailure: {
value: false,
},
},
},
__typename: 'WorkflowAction',
},
]),
status: 'ACTIVE',
position: 2,
workflowId: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
},
])
.returning('*')
.execute();
await entityManager
.createQueryBuilder()
.insert()
.into(`${schemaName}.workflowAutomatedTrigger`, [
'id',
'workflowId',
'type',
'settings',
])
.orIgnore()
.values([
{
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_AUTOMATED_TRIGGER_ID,
workflowId: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
type: 'DATABASE_EVENT',
settings: {
eventName: 'person.upserted',
fields: ['emails'],
},
},
])
.execute();
};
@@ -6,40 +6,37 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
"id": "00000000-0000-0000-0000-000000000011",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000027",
"id": "00000000-0000-0000-0000-000000000026",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000028",
"id": "00000000-0000-0000-0000-000000000027",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000025",
"id": "00000000-0000-0000-0000-000000000024",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000026",
"id": "00000000-0000-0000-0000-000000000025",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000023",
"id": "00000000-0000-0000-0000-000000000022",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000024",
"id": "00000000-0000-0000-0000-000000000023",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000012",
"widgets": {
"accountOwner": {
"id": "00000000-0000-0000-0000-000000000015",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000013",
},
"opportunities": {
"id": "00000000-0000-0000-0000-000000000016",
"id": "00000000-0000-0000-0000-000000000015",
},
"people": {
"id": "00000000-0000-0000-0000-000000000014",
@@ -47,26 +44,26 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000021",
"id": "00000000-0000-0000-0000-000000000020",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000022",
"id": "00000000-0000-0000-0000-000000000021",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000019",
"id": "00000000-0000-0000-0000-000000000018",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000020",
"id": "00000000-0000-0000-0000-000000000019",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000017",
"id": "00000000-0000-0000-0000-000000000016",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000018",
"id": "00000000-0000-0000-0000-000000000017",
},
},
},
@@ -107,281 +104,278 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
},
},
"noteRecordPage": {
"id": "00000000-0000-0000-0000-000000000064",
"id": "00000000-0000-0000-0000-000000000063",
"tabs": {
"files": {
"id": "00000000-0000-0000-0000-000000000072",
"id": "00000000-0000-0000-0000-000000000071",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000073",
"id": "00000000-0000-0000-0000-000000000072",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000065",
"id": "00000000-0000-0000-0000-000000000064",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000066",
"id": "00000000-0000-0000-0000-000000000065",
},
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000067",
"id": "00000000-0000-0000-0000-000000000066",
},
},
},
"note": {
"id": "00000000-0000-0000-0000-000000000068",
"id": "00000000-0000-0000-0000-000000000067",
"widgets": {
"noteRichText": {
"id": "00000000-0000-0000-0000-000000000069",
"id": "00000000-0000-0000-0000-000000000068",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000070",
"id": "00000000-0000-0000-0000-000000000069",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000071",
"id": "00000000-0000-0000-0000-000000000070",
},
},
},
},
},
"opportunityRecordPage": {
"id": "00000000-0000-0000-0000-000000000046",
"id": "00000000-0000-0000-0000-000000000045",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000062",
"id": "00000000-0000-0000-0000-000000000061",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000063",
"id": "00000000-0000-0000-0000-000000000062",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000060",
"id": "00000000-0000-0000-0000-000000000059",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000061",
"id": "00000000-0000-0000-0000-000000000060",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000058",
"id": "00000000-0000-0000-0000-000000000057",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000059",
"id": "00000000-0000-0000-0000-000000000058",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000047",
"id": "00000000-0000-0000-0000-000000000046",
"widgets": {
"company": {
"id": "00000000-0000-0000-0000-000000000050",
"id": "00000000-0000-0000-0000-000000000049",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000048",
"id": "00000000-0000-0000-0000-000000000047",
},
"owner": {
"id": "00000000-0000-0000-0000-000000000051",
"id": "00000000-0000-0000-0000-000000000050",
},
"pointOfContact": {
"id": "00000000-0000-0000-0000-000000000049",
"id": "00000000-0000-0000-0000-000000000048",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000056",
"id": "00000000-0000-0000-0000-000000000055",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000057",
"id": "00000000-0000-0000-0000-000000000056",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000054",
"id": "00000000-0000-0000-0000-000000000053",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000055",
"id": "00000000-0000-0000-0000-000000000054",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000052",
"id": "00000000-0000-0000-0000-000000000051",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000053",
"id": "00000000-0000-0000-0000-000000000052",
},
},
},
},
},
"personRecordPage": {
"id": "00000000-0000-0000-0000-000000000029",
"id": "00000000-0000-0000-0000-000000000028",
"tabs": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000044",
"id": "00000000-0000-0000-0000-000000000043",
"widgets": {
"calendar": {
"id": "00000000-0000-0000-0000-000000000045",
"id": "00000000-0000-0000-0000-000000000044",
},
},
},
"emails": {
"id": "00000000-0000-0000-0000-000000000042",
"id": "00000000-0000-0000-0000-000000000041",
"widgets": {
"emails": {
"id": "00000000-0000-0000-0000-000000000043",
"id": "00000000-0000-0000-0000-000000000042",
},
},
},
"files": {
"id": "00000000-0000-0000-0000-000000000040",
"id": "00000000-0000-0000-0000-000000000039",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000041",
"id": "00000000-0000-0000-0000-000000000040",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000030",
"id": "00000000-0000-0000-0000-000000000029",
"widgets": {
"company": {
"id": "00000000-0000-0000-0000-000000000032",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000031",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000030",
},
"pointOfContactForOpportunities": {
"id": "00000000-0000-0000-0000-000000000033",
"id": "00000000-0000-0000-0000-000000000032",
},
},
},
"notes": {
"id": "00000000-0000-0000-0000-000000000038",
"id": "00000000-0000-0000-0000-000000000037",
"widgets": {
"notes": {
"id": "00000000-0000-0000-0000-000000000039",
"id": "00000000-0000-0000-0000-000000000038",
},
},
},
"tasks": {
"id": "00000000-0000-0000-0000-000000000036",
"id": "00000000-0000-0000-0000-000000000035",
"widgets": {
"tasks": {
"id": "00000000-0000-0000-0000-000000000037",
"id": "00000000-0000-0000-0000-000000000036",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000034",
"id": "00000000-0000-0000-0000-000000000033",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000035",
"id": "00000000-0000-0000-0000-000000000034",
},
},
},
},
},
"taskRecordPage": {
"id": "00000000-0000-0000-0000-000000000074",
"id": "00000000-0000-0000-0000-000000000073",
"tabs": {
"files": {
"id": "00000000-0000-0000-0000-000000000083",
"id": "00000000-0000-0000-0000-000000000081",
"widgets": {
"files": {
"id": "00000000-0000-0000-0000-000000000084",
"id": "00000000-0000-0000-0000-000000000082",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000075",
"id": "00000000-0000-0000-0000-000000000074",
"widgets": {
"assignee": {
"id": "00000000-0000-0000-0000-000000000078",
},
"fields": {
"id": "00000000-0000-0000-0000-000000000076",
"id": "00000000-0000-0000-0000-000000000075",
},
"taskRichText": {
"id": "00000000-0000-0000-0000-000000000077",
"id": "00000000-0000-0000-0000-000000000076",
},
},
},
"note": {
"id": "00000000-0000-0000-0000-000000000079",
"id": "00000000-0000-0000-0000-000000000077",
"widgets": {
"taskRichText": {
"id": "00000000-0000-0000-0000-000000000080",
"id": "00000000-0000-0000-0000-000000000078",
},
},
},
"timeline": {
"id": "00000000-0000-0000-0000-000000000081",
"id": "00000000-0000-0000-0000-000000000079",
"widgets": {
"timeline": {
"id": "00000000-0000-0000-0000-000000000082",
"id": "00000000-0000-0000-0000-000000000080",
},
},
},
},
},
"workflowRecordPage": {
"id": "00000000-0000-0000-0000-000000000085",
"id": "00000000-0000-0000-0000-000000000083",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000086",
"id": "00000000-0000-0000-0000-000000000084",
"widgets": {
"workflow": {
"id": "00000000-0000-0000-0000-000000000087",
"id": "00000000-0000-0000-0000-000000000085",
},
},
},
},
},
"workflowRunRecordPage": {
"id": "00000000-0000-0000-0000-000000000094",
"id": "00000000-0000-0000-0000-000000000092",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000098",
"id": "00000000-0000-0000-0000-000000000096",
"widgets": {
"workflowRun": {
"id": "00000000-0000-0000-0000-000000000099",
"id": "00000000-0000-0000-0000-000000000097",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000095",
"id": "00000000-0000-0000-0000-000000000093",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000096",
"id": "00000000-0000-0000-0000-000000000094",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000097",
"id": "00000000-0000-0000-0000-000000000095",
},
},
},
},
},
"workflowVersionRecordPage": {
"id": "00000000-0000-0000-0000-000000000088",
"id": "00000000-0000-0000-0000-000000000086",
"tabs": {
"flow": {
"id": "00000000-0000-0000-0000-000000000092",
"id": "00000000-0000-0000-0000-000000000090",
"widgets": {
"workflowVersion": {
"id": "00000000-0000-0000-0000-000000000093",
"id": "00000000-0000-0000-0000-000000000091",
},
},
},
"home": {
"id": "00000000-0000-0000-0000-000000000089",
"id": "00000000-0000-0000-0000-000000000087",
"widgets": {
"fields": {
"id": "00000000-0000-0000-0000-000000000090",
"id": "00000000-0000-0000-0000-000000000088",
},
"workflow": {
"id": "00000000-0000-0000-0000-000000000091",
"id": "00000000-0000-0000-0000-000000000089",
},
},
},
@@ -31,21 +31,12 @@ const COMPANY_PAGE_TABS = {
fieldUniversalIdentifier:
STANDARD_OBJECTS.company.fields.people.universalIdentifier,
},
accountOwner: {
universalIdentifier: '20202020-ac01-4001-8001-c0aba11c0113',
title: 'Account Owner',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
fieldUniversalIdentifier:
STANDARD_OBJECTS.company.fields.accountOwner.universalIdentifier,
},
opportunities: {
universalIdentifier: '20202020-ac01-4001-8001-c0aba11c0114',
title: 'Opportunities',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.FOURTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
fieldUniversalIdentifier:
STANDARD_OBJECTS.company.fields.opportunities.universalIdentifier,
},
@@ -1,13 +1,10 @@
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import {
CONDITIONAL_DISPLAY_DEVICE_DESKTOP,
CONDITIONAL_DISPLAY_DEVICE_MOBILE,
GRID_POSITIONS,
TAB_PROPS,
VERTICAL_LIST_LAYOUT_POSITIONS,
WIDGET_PROPS,
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
import {
@@ -32,15 +29,6 @@ const TASK_PAGE_TABS = {
position: { layoutMode: TAB_PROPS.home.layoutMode, index: 1 },
conditionalDisplay: CONDITIONAL_DISPLAY_DEVICE_MOBILE,
},
assignee: {
universalIdentifier: '20202020-ac05-4005-8005-ba5ca11a5513',
title: 'Assignee',
type: WidgetType.FIELD,
gridPosition: GRID_POSITIONS.FULL_WIDTH,
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
fieldUniversalIdentifier:
STANDARD_OBJECTS.task.fields.assignee.universalIdentifier,
},
},
},
note: {
@@ -45,19 +45,6 @@ export const computeStandardBlocklistViewFields = (
},
}),
blocklistRecordPageFieldsHandle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'blocklist',
context: {
viewName: 'blocklistRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
blocklistRecordPageFieldsWorkspaceMember:
createStandardViewFieldFlatMetadata({
...args,
@@ -81,19 +81,6 @@ export const computeStandardCalendarChannelViewFields = (
},
}),
calendarChannelRecordPageFieldsHandle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'calendarChannel',
context: {
viewName: 'calendarChannelRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
calendarChannelRecordPageFieldsConnectedAccount:
createStandardViewFieldFlatMetadata({
...args,
@@ -245,5 +245,123 @@ export const computeStandardCompanyViewFields = (
viewFieldGroupName: 'other',
},
}),
companyRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
companyRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
companyRecordPageFieldsPeople: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'people',
fieldName: 'people',
position: 4,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsTaskTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'taskTargets',
fieldName: 'taskTargets',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'noteTargets',
fieldName: 'noteTargets',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsOpportunities: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'opportunities',
fieldName: 'opportunities',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 8,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 9,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
companyRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'company',
context: {
viewName: 'companyRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 10,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -69,21 +69,6 @@ export const computeStandardConnectedAccountViewFields = (
},
}),
connectedAccountRecordPageFieldsHandle: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'connectedAccount',
context: {
viewName: 'connectedAccountRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
},
),
connectedAccountRecordPageFieldsProvider:
createStandardViewFieldFlatMetadata({
...args,
@@ -33,19 +33,6 @@ export const computeStandardFavoriteFolderViewFields = (
},
}),
favoriteFolderRecordPageFieldsName: createStandardViewFieldFlatMetadata({
...args,
objectName: 'favoriteFolder',
context: {
viewName: 'favoriteFolderRecordPageFields',
viewFieldName: 'name',
fieldName: 'name',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
favoriteFolderRecordPageFieldsCreatedAt:
createStandardViewFieldFlatMetadata({
...args,
@@ -93,19 +93,6 @@ export const computeStandardMessageChannelViewFields = (
},
}),
messageChannelRecordPageFieldsHandle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'messageChannel',
context: {
viewName: 'messageChannelRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
messageChannelRecordPageFieldsConnectedAccount:
createStandardViewFieldFlatMetadata({
...args,
@@ -69,19 +69,6 @@ export const computeStandardMessageFolderViewFields = (
},
}),
messageFolderRecordPageFieldsName: createStandardViewFieldFlatMetadata({
...args,
objectName: 'messageFolder',
context: {
viewName: 'messageFolderRecordPageFields',
viewFieldName: 'name',
fieldName: 'name',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
messageFolderRecordPageFieldsMessageChannel:
createStandardViewFieldFlatMetadata({
...args,
@@ -93,20 +93,6 @@ export const computeStandardMessageParticipantViewFields = (
},
}),
messageParticipantRecordPageFieldsHandle:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'messageParticipant',
context: {
viewName: 'messageParticipantRecordPageFields',
viewFieldName: 'handle',
fieldName: 'handle',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
messageParticipantRecordPageFieldsMessage:
createStandardViewFieldFlatMetadata({
...args,
@@ -70,19 +70,6 @@ export const computeStandardNoteViewFields = (
}),
// noteRecordPageFields view fields
noteRecordPageFieldsTitle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'title',
fieldName: 'title',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
noteRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
@@ -122,5 +109,85 @@ export const computeStandardNoteViewFields = (
viewFieldGroupName: 'other',
},
}),
noteRecordPageFieldsBodyV2: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'bodyV2',
fieldName: 'bodyV2',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
noteRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
noteRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
noteRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
noteRecordPageFieldsTimelineActivities: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
noteRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'note',
context: {
viewName: 'noteRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 4,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -266,5 +266,103 @@ export const computeStandardOpportunityViewFields = (
viewFieldGroupName: 'other',
},
}),
opportunityRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
opportunityRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
opportunityRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
opportunityRecordPageFieldsTaskTargets: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'taskTargets',
fieldName: 'taskTargets',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
opportunityRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'noteTargets',
fieldName: 'noteTargets',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
opportunityRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 8,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
opportunityRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'opportunity',
context: {
viewName: 'opportunityRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 9,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -266,5 +266,152 @@ export const computeStandardPersonViewFields = (
viewFieldGroupName: 'other',
},
}),
personRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 4,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
personRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
personRecordPageFieldsAvatarFile: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'avatarFile',
fieldName: 'avatarFile',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
personRecordPageFieldsPointOfContactForOpportunities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'pointOfContactForOpportunities',
fieldName: 'pointOfContactForOpportunities',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsTaskTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'taskTargets',
fieldName: 'taskTargets',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'noteTargets',
fieldName: 'noteTargets',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 8,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 9,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsMessageParticipants:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'messageParticipants',
fieldName: 'messageParticipants',
position: 10,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsCalendarEventParticipants:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'calendarEventParticipants',
fieldName: 'calendarEventParticipants',
position: 11,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
personRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'person',
context: {
viewName: 'personRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 12,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -255,19 +255,6 @@ export const computeStandardTaskViewFields = (
}),
// taskRecordPageFields view fields
taskRecordPageFieldsTitle: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'title',
fieldName: 'title',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
taskRecordPageFieldsDueAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
@@ -346,5 +333,85 @@ export const computeStandardTaskViewFields = (
viewFieldGroupName: 'other',
},
}),
taskRecordPageFieldsBodyV2: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'bodyV2',
fieldName: 'bodyV2',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
taskRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
taskRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
taskRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'attachments',
fieldName: 'attachments',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
taskRecordPageFieldsTimelineActivities: createStandardViewFieldFlatMetadata(
{
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
},
),
taskRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'task',
context: {
viewName: 'taskRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 7,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -46,19 +46,6 @@ export const computeStandardWorkflowRunViewFields = (
}),
// workflowRunRecordPageFields view fields
workflowRunRecordPageFieldsName: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'name',
fieldName: 'name',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsStatus: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
@@ -151,5 +138,110 @@ export const computeStandardWorkflowRunViewFields = (
viewFieldGroupName: 'other',
},
}),
workflowRunRecordPageFieldsEnqueuedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'enqueuedAt',
fieldName: 'enqueuedAt',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
workflowRunRecordPageFieldsOutput: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'output',
fieldName: 'output',
position: 6,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsContext: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'context',
fieldName: 'context',
position: 7,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsState: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'state',
fieldName: 'state',
position: 8,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowRunRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowRunRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 9,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowRunRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowRun',
context: {
viewName: 'workflowRunRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 10,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -70,19 +70,6 @@ export const computeStandardWorkflowVersionViewFields = (
}),
// workflowVersionRecordPageFields view fields
workflowVersionRecordPageFieldsName: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'name',
fieldName: 'name',
position: 0,
isVisible: true,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowVersionRecordPageFieldsStatus: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
@@ -139,5 +126,101 @@ export const computeStandardWorkflowVersionViewFields = (
viewFieldGroupName: 'other',
},
}),
workflowVersionRecordPageFieldsSteps: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'steps',
fieldName: 'steps',
position: 0,
isVisible: false,
size: 150,
viewFieldGroupName: 'additional',
},
}),
workflowVersionRecordPageFieldsCreatedBy:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'createdBy',
fieldName: 'createdBy',
position: 1,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowVersionRecordPageFieldsUpdatedAt:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'updatedAt',
fieldName: 'updatedAt',
position: 2,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowVersionRecordPageFieldsUpdatedBy:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'updatedBy',
fieldName: 'updatedBy',
position: 3,
isVisible: false,
size: 150,
viewFieldGroupName: 'other',
},
}),
workflowVersionRecordPageFieldsRuns: createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'runs',
fieldName: 'runs',
position: 4,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowVersionRecordPageFieldsFavorites:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'favorites',
fieldName: 'favorites',
position: 5,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
workflowVersionRecordPageFieldsTimelineActivities:
createStandardViewFieldFlatMetadata({
...args,
objectName: 'workflowVersion',
context: {
viewName: 'workflowVersionRecordPageFields',
viewFieldName: 'timelineActivities',
fieldName: 'timelineActivities',
position: 6,
isVisible: false,
size: 150,
viewFieldGroupName: 'general',
},
}),
};
};
@@ -4,5 +4,4 @@ export const DEFAULT_FEATURE_FLAGS = [
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
] as const satisfies FeatureFlagKey[];
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { ViewType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
@@ -95,8 +96,9 @@ export class FlatViewFieldValidatorService {
}
if (
flatView.type !== ViewType.FIELDS_WIDGET &&
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
updatedFlatViewField.fieldMetadataUniversalIdentifier
updatedFlatViewField.fieldMetadataUniversalIdentifier
) {
const otherFlatViewFields =
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow(
@@ -127,6 +129,7 @@ export class FlatViewFieldValidatorService {
flatViewFieldMaps: optimisticFlatViewFieldMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
flatViewMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.viewField
@@ -177,11 +180,18 @@ export class FlatViewFieldValidatorService {
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
existingFlatViewField.fieldMetadataUniversalIdentifier
) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Label identifier view field cannot be deleted`,
userFriendlyMessage: msg`Label identifier view field cannot be deleted`,
const flatView = findFlatEntityByUniversalIdentifier({
universalIdentifier: existingFlatViewField.viewUniversalIdentifier,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView) || flatView.type !== ViewType.FIELDS_WIDGET) {
validationResult.errors.push({
code: ViewExceptionCode.INVALID_VIEW_DATA,
message: t`Label identifier view field cannot be deleted`,
userFriendlyMessage: msg`Label identifier view field cannot be deleted`,
});
}
}
return validationResult;
@@ -289,8 +299,9 @@ export class FlatViewFieldValidatorService {
}
if (
flatView.type !== ViewType.FIELDS_WIDGET &&
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
flatViewFieldToValidate.fieldMetadataUniversalIdentifier
flatViewFieldToValidate.fieldMetadataUniversalIdentifier
) {
validationResult.errors.push(
...validateLabelIdentifierFieldMetadataIdFlatViewField({
@@ -299,6 +310,7 @@ export class FlatViewFieldValidatorService {
}),
);
} else if (
flatView.type !== ViewType.FIELDS_WIDGET &&
otherFlatViewFields.some(
(flatViewField) =>
flatViewField.fieldMetadataUniversalIdentifier ===
@@ -0,0 +1,235 @@
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { createManyViewGroups } from 'test/integration/metadata/suites/view-group/utils/create-many-view-groups.util';
import { deleteOneViewGroup } from 'test/integration/metadata/suites/view-group/utils/delete-one-view-group.util';
import { destroyOneViewGroup } from 'test/integration/metadata/suites/view-group/utils/destroy-one-view-group.util';
import { updateManyViewGroups } from 'test/integration/metadata/suites/view-group/utils/update-many-view-groups.util';
import { createOneView } from 'test/integration/metadata/suites/view/utils/create-one-view.util';
import { isDefined } from 'twenty-shared/utils';
import { type CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
describe('View Group Resolver - Successful Update Many Operations - v2', () => {
let testSetup: {
testViewId: string;
testObjectMetadataId: string;
};
let createdViewGroupIds: string[] = [];
beforeAll(async () => {
const {
data: {
createOneObject: { id: objectMetadataId },
},
} = await createOneObjectMetadata({
expectToFail: false,
input: {
nameSingular: 'myUpdateManyGroupTestObjectV2',
namePlural: 'myUpdateManyGroupTestObjectsV2',
labelSingular: 'My Update Many Group Test Object v2',
labelPlural: 'My Update Many Group Test Objects v2',
icon: 'Icon123',
},
});
const { selectFieldMetadataId } =
await createOneSelectFieldMetadataForIntegrationTests({
input: {
objectMetadataId,
},
});
const {
data: {
createView: { id: testViewId },
},
} = await createOneView({
input: {
icon: 'icon123',
objectMetadataId,
name: 'TestViewForUpdateManyGroups',
mainGroupByFieldMetadataId: selectFieldMetadataId,
},
expectToFail: false,
});
testSetup = {
testViewId,
testObjectMetadataId: objectMetadataId,
};
});
afterAll(async () => {
await updateOneObjectMetadata({
input: {
idToUpdate: testSetup.testObjectMetadataId,
updatePayload: {
isActive: false,
},
},
});
await deleteOneObjectMetadata({
expectToFail: false,
input: { idToDelete: testSetup.testObjectMetadataId },
});
});
afterEach(async () => {
for (const viewGroupId of createdViewGroupIds) {
if (isDefined(viewGroupId)) {
const {
data: { deleteViewGroup },
} = await deleteOneViewGroup({
expectToFail: false,
input: {
id: viewGroupId,
},
});
expect(deleteViewGroup.deletedAt).not.toBeNull();
await destroyOneViewGroup({
expectToFail: false,
input: {
id: viewGroupId,
},
});
}
}
createdViewGroupIds = [];
});
it('should batch-update positions of multiple view groups at once', async () => {
const createInputs: CreateViewGroupInput[] = [
{
viewId: testSetup.testViewId,
position: 0,
isVisible: true,
fieldValue: 'Group A',
},
{
viewId: testSetup.testViewId,
position: 1,
isVisible: true,
fieldValue: 'Group B',
},
{
viewId: testSetup.testViewId,
position: 2,
isVisible: true,
fieldValue: 'Group C',
},
];
const {
data: { createManyViewGroups: createdGroups },
} = await createManyViewGroups({
inputs: createInputs,
expectToFail: false,
});
createdViewGroupIds = createdGroups.map(
(viewGroup: { id: string }) => viewGroup.id,
);
const {
data: { updateManyViewGroups: updatedGroups },
errors,
} = await updateManyViewGroups({
inputs: [
{ id: createdGroups[0].id, update: { position: 2 } },
{ id: createdGroups[1].id, update: { position: 0 } },
{ id: createdGroups[2].id, update: { position: 1 } },
],
expectToFail: false,
});
expect(errors).toBeUndefined();
expect(updatedGroups).toBeDefined();
expect(updatedGroups).toHaveLength(3);
expect(updatedGroups[0]).toMatchObject({
id: createdGroups[0].id,
position: 2,
});
expect(updatedGroups[1]).toMatchObject({
id: createdGroups[1].id,
position: 0,
});
expect(updatedGroups[2]).toMatchObject({
id: createdGroups[2].id,
position: 1,
});
});
it('should batch-update visibility of multiple view groups at once', async () => {
const createInputs: CreateViewGroupInput[] = [
{
viewId: testSetup.testViewId,
position: 0,
isVisible: true,
fieldValue: 'Visible Group',
},
{
viewId: testSetup.testViewId,
position: 1,
isVisible: true,
fieldValue: 'To Be Hidden Group',
},
];
const {
data: { createManyViewGroups: createdGroups },
} = await createManyViewGroups({
inputs: createInputs,
expectToFail: false,
});
createdViewGroupIds = createdGroups.map(
(viewGroup: { id: string }) => viewGroup.id,
);
const {
data: { updateManyViewGroups: updatedGroups },
errors,
} = await updateManyViewGroups({
inputs: [
{ id: createdGroups[0].id, update: { isVisible: false } },
{
id: createdGroups[1].id,
update: { isVisible: false, position: 5 },
},
],
expectToFail: false,
});
expect(errors).toBeUndefined();
expect(updatedGroups).toBeDefined();
expect(updatedGroups).toHaveLength(2);
expect(updatedGroups[0]).toMatchObject({
id: createdGroups[0].id,
isVisible: false,
});
expect(updatedGroups[1]).toMatchObject({
id: createdGroups[1].id,
isVisible: false,
position: 5,
});
});
it('should return empty array for empty inputs', async () => {
const {
data: { updateManyViewGroups: updatedGroups },
errors,
} = await updateManyViewGroups({
inputs: [],
expectToFail: false,
});
expect(errors).toBeUndefined();
expect(updatedGroups).toBeDefined();
expect(updatedGroups).toHaveLength(0);
});
});
@@ -93,6 +93,6 @@ describe('View side effect on object creation', () => {
expectToFail: false,
});
expect(createdViewFields.length).toBe(7);
expect(createdViewFields.length).toBe(5);
});
});
@@ -0,0 +1,23 @@
import gql from 'graphql-tag';
import { VIEW_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
import { type UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input';
export const updateManyViewGroupsQueryFactory = ({
gqlFields = VIEW_GROUP_GQL_FIELDS,
inputs,
}: {
gqlFields?: string;
inputs: UpdateViewGroupInput[];
}) => ({
query: gql`
mutation UpdateManyViewGroups($inputs: [UpdateViewGroupInput!]!) {
updateManyViewGroups(inputs: $inputs) {
${gqlFields}
}
}
`,
variables: {
inputs,
},
});
@@ -0,0 +1,45 @@
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { updateManyViewGroupsQueryFactory } from 'test/integration/metadata/suites/view-group/utils/update-many-view-groups-query-factory.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input';
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
export const updateManyViewGroups = async ({
inputs,
gqlFields,
expectToFail,
}: {
inputs: UpdateViewGroupInput[];
gqlFields?: string;
expectToFail?: boolean;
}): CommonResponseBody<{
updateManyViewGroups: ViewGroupEntity[];
}> => {
const graphqlOperation = updateManyViewGroupsQueryFactory({
inputs,
gqlFields,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage:
'View Groups batch update should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage:
'View Groups batch update has failed but should not',
});
}
return { data: response.body.data, errors: response.body.errors };
};
@@ -188,9 +188,6 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
handle: {
universalIdentifier: 'e22a1d19-c1bb-4265-ae48-2054513c21fe',
},
workspaceMember: {
universalIdentifier: 'f2f5732f-7435-44be-986b-4c4d834fdfeb',
},
@@ -405,9 +402,6 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
handle: {
universalIdentifier: 'cd1f641b-5746-49db-9a7e-82dd9a63593d',
},
connectedAccount: {
universalIdentifier: 'bdb40f41-f9ba-4b59-a8cf-878c23701ab3',
},
@@ -808,6 +802,33 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1210',
},
updatedAt: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1212',
},
updatedBy: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1213',
},
people: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1214',
},
taskTargets: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1215',
},
noteTargets: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1216',
},
opportunities: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1217',
},
favorites: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1218',
},
attachments: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c1219',
},
timelineActivities: {
universalIdentifier: '20202020-af01-4a01-8a01-c0aba11c121a',
},
},
},
},
@@ -914,9 +935,6 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
handle: {
universalIdentifier: '41d86e23-ceb2-41ab-975f-8ec5a1023ece',
},
provider: {
universalIdentifier: '83171d2a-0d11-42b1-991d-8d4346b02cff',
},
@@ -1213,9 +1231,6 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
name: {
universalIdentifier: 'cac4f0f7-3a6a-49b1-b86f-41b20f2455c0',
},
createdAt: {
universalIdentifier: 'a4e42591-844c-47d1-b72e-5ded3d541694',
},
@@ -1554,9 +1569,6 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
handle: {
universalIdentifier: 'a9cbb9a5-a6b4-417e-93ad-a5e578c222db',
},
connectedAccount: {
universalIdentifier: '19079cf6-2a9c-40b9-b6c2-58d63c6e37ad',
},
@@ -1670,9 +1682,6 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
name: {
universalIdentifier: '6fa8c474-ee22-47f1-b830-4f169ff82315',
},
messageChannel: {
universalIdentifier: '2fb6ff09-bed5-4b31-af0f-7fa3df5612da',
},
@@ -1791,9 +1800,6 @@ export const STANDARD_OBJECTS = {
role: {
universalIdentifier: '5d1f9a65-85cc-41b2-a8bf-8e2c97aab4b3',
},
handle: {
universalIdentifier: '97295fc0-cdb8-4d84-8c1b-327837255c0d',
},
displayName: {
universalIdentifier: 'c50748fe-9f54-4e09-b572-111f076ec7db',
},
@@ -2018,9 +2024,6 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
title: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115201',
},
createdAt: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115202',
},
@@ -2030,6 +2033,24 @@ export const STANDARD_OBJECTS = {
noteTargets: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115204',
},
bodyV2: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115205',
},
updatedAt: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115206',
},
updatedBy: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115207',
},
attachments: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115208',
},
timelineActivities: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a115209',
},
favorites: {
universalIdentifier: '20202020-af05-4a05-8a05-a0be5a11520a',
},
},
},
},
@@ -2278,6 +2299,27 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca3208',
},
updatedAt: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320a',
},
updatedBy: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320b',
},
favorites: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320c',
},
taskTargets: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320d',
},
noteTargets: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320e',
},
attachments: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca320f',
},
timelineActivities: {
universalIdentifier: '20202020-af03-4a03-8a03-0aa0b1ca3210',
},
},
},
},
@@ -2437,6 +2479,39 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12210',
},
updatedAt: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12212',
},
updatedBy: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12213',
},
avatarFile: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12214',
},
pointOfContactForOpportunities: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12215',
},
taskTargets: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12216',
},
noteTargets: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12217',
},
favorites: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12218',
},
attachments: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea12219',
},
messageParticipants: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea1221a',
},
calendarEventParticipants: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea1221b',
},
timelineActivities: {
universalIdentifier: '20202020-af02-4a02-8a02-ae0a1ea1221c',
},
},
},
},
@@ -2610,9 +2685,6 @@ export const STANDARD_OBJECTS = {
},
},
viewFields: {
title: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6201',
},
dueAt: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6202',
},
@@ -2631,6 +2703,24 @@ export const STANDARD_OBJECTS = {
taskTargets: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6207',
},
bodyV2: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6208',
},
updatedAt: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a6209',
},
updatedBy: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a620a',
},
attachments: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a620b',
},
timelineActivities: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a620c',
},
favorites: {
universalIdentifier: '20202020-af06-4a06-8a06-ba5ca11a620d',
},
},
},
},
@@ -3114,9 +3204,6 @@ export const STANDARD_OBJECTS = {
workflowRunRecordPageFields: {
universalIdentifier: '20202020-a011-4a11-8a11-a0bcf10abcf1',
viewFields: {
name: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcf5',
},
status: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcf6',
},
@@ -3138,6 +3225,30 @@ export const STANDARD_OBJECTS = {
createdBy: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcfc',
},
enqueuedAt: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcfd',
},
output: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcfe',
},
context: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abcff',
},
state: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd01',
},
updatedAt: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd02',
},
updatedBy: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd03',
},
favorites: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd04',
},
timelineActivities: {
universalIdentifier: '20202020-af11-4a11-8a11-a0bcf10abd05',
},
},
viewFieldGroups: {
general: {
@@ -3229,9 +3340,6 @@ export const STANDARD_OBJECTS = {
workflowVersionRecordPageFields: {
universalIdentifier: '20202020-a010-4a10-8a10-a0bcf10aaef1',
viewFields: {
name: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaef5',
},
status: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaef6',
},
@@ -3244,6 +3352,27 @@ export const STANDARD_OBJECTS = {
createdAt: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaef9',
},
steps: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefa',
},
createdBy: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefb',
},
updatedAt: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefc',
},
updatedBy: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefd',
},
runs: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaefe',
},
favorites: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaeff',
},
timelineActivities: {
universalIdentifier: '20202020-af10-4a10-8a10-a0bcf10aaf01',
},
},
viewFieldGroups: {
general: {
+39
View File
@@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# DB
*.sqlite
@@ -0,0 +1,48 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules"],
"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": "^_"
}],
"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"
}
}
+4
View File
@@ -0,0 +1,4 @@
# Twenty-Website
This is used for the marketing website (twenty.com).
This is not related in any way to the main app, which you can find in twenty-front and twenty-server.
@@ -0,0 +1,5 @@
import withLinaria, { type LinariaConfig } from 'next-with-linaria';
const nextConfig: LinariaConfig = { reactCompiler: true };
module.exports = withLinaria(nextConfig);
+27
View File
@@ -0,0 +1,27 @@
{
"name": "twenty-website-new",
"private": true,
"scripts": {
"nx": "NX_DEFAULT_PROJECT=twenty-website-new node ../../node_modules/nx/bin/nx.js",
"dev": "npx next dev",
"build": "npx next build",
"start": "npx next start"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@base-ui/utils": "^0.2.6",
"@linaria/core": "^7.0.0",
"@linaria/react": "^7.0.1",
"@wyw-in-js/babel-preset": "^0.8.1",
"next": "16.1.7",
"next-with-linaria": "^1.3.0",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0"
}
}
+48
View File
@@ -0,0 +1,48 @@
{
"name": "twenty-website-new",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-website-new/src",
"projectType": "application",
"tags": ["scope:website"],
"targets": {
"build": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/.next"],
"options": {
"cwd": "{projectRoot}",
"command": "npx next build"
},
"dependsOn": ["^build"]
},
"dev": {
"executor": "nx:run-commands",
"cache": false,
"options": {
"cwd": "{projectRoot}",
"command": "npx next dev"
}
},
"start": {
"executor": "nx:run-commands",
"cache": false,
"dependsOn": ["build"],
"options": {
"cwd": "{projectRoot}",
"command": "npx next start"
}
},
"lint": {},
"lint:diff-with-main": {},
"typecheck": {},
"fmt": {
"options": {
"files": "."
},
"configurations": {
"fix": {}
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

@@ -0,0 +1,12 @@
import { HeroDataType } from '@/sections/Hero/types';
export const HERO_DATA: HeroDataType = {
heading: [
{ text: 'Build', fontFamily: 'sans' },
{ text: ' your Enterprise CRM ', fontFamily: 'serif' },
{ text: 'at AI Speed', fontFamily: 'sans' },
],
body: {
text: 'Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves.',
},
};

Some files were not shown because too many files have changed in this diff Show More