Compare commits

..
Author SHA1 Message Date
Sonarly Claude Code 7e259d75e8 fix: skip built file copy in duplicateOneWithSource when build is not up to date
https://sonarly.com/issue/19030?type=bug

Creating a draft from a workflow version fails when a CODE step references a logic function whose built file (.mjs) doesn't exist in S3, because `duplicateOneWithSource` unconditionally tries to copy both source and built files without checking `isBuildUpToDate`.

Fix: **What changed:**

1. **`logic-function-from-source.service.ts`**: In `duplicateOneWithSource()`, replaced the single `copyResources()` call with two separate calls: `copySourceResources()` (always) and `copyBuiltResources()` (only when `existingLogicFunction.isBuildUpToDate` is true). This mirrors the guard pattern already used in `executeOneFromSource()` at line 362.

2. **`logic-function-resource.service.ts`**: Added two new methods `copySourceResources()` and `copyBuiltResources()` that handle source and built file copying independently, with their own typed parameter interfaces. The existing `copyResources()` method is preserved (though no longer called) to avoid breaking any future code that might reference it.

**Why this works:** When a logic function is created with custom source code (via `input.source`), only the source file is uploaded — no built `.mjs` file is created until `buildOneFromSource` is explicitly called. The `isBuildUpToDate` flag accurately tracks whether the built file exists. The duplicate inherits this flag (line 175), so the duplicated logic function correctly reflects that it also needs building.

**Monitoring improvement:** The fix itself eliminates the Sentry noise — by not attempting to copy non-existent built files, the S3 "No objects found" error no longer fires. No separate monitoring changes needed.
2026-03-27 14:47:39 +00:00
c96c034908 i18n - translations (#19040)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-03-27 14:53:33 +01:00
Abdul RahmanGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>Charles Bochet
5efe69f8d3 Migrate field permission to syncable entity (#18751)
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-03-27 14:47:27 +01:00
c2b058a6a7 fix: use workspace-generated id for core dual-write in message folder save (#19038)
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
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 <charles@twenty.com>
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 <rayan@example.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
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 <6399865+FelixMalfait@users.noreply.github.com>
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
456 changed files with 16782 additions and 7182 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",
@@ -343,6 +343,7 @@ type Field {
defaultValue: JSON
options: JSON
settings: JSON
objectMetadataId: UUID!
isLabelSyncedWithName: Boolean
morphId: UUID
createdAt: DateTime!
@@ -540,6 +541,7 @@ input FieldFilter {
isActive: BooleanFieldComparison
isSystem: BooleanFieldComparison
isUIReadOnly: BooleanFieldComparison
objectMetadataId: UUIDFilterComparison
}
input IndexFilter {
@@ -2273,6 +2275,11 @@ type FieldConnection {
edges: [FieldEdge!]!
}
type LogicFunctionLogs {
"""Execution Logs"""
logs: String!
}
type DeleteTwoFactorAuthenticationMethod {
"""Boolean that confirms query was dispatched"""
success: Boolean!
@@ -2593,11 +2600,6 @@ type UsageAnalytics {
userDailyUsage: UsageUserDaily
}
type LogicFunctionLogs {
"""Execution Logs"""
logs: String!
}
type FrontComponent {
id: UUID!
name: String!
@@ -3090,6 +3092,7 @@ enum AllMetadataName {
navigationMenuItem
permissionFlag
objectPermission
fieldPermission
frontComponent
webhook
}
@@ -3501,6 +3504,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!
@@ -4272,8 +4276,8 @@ input CreateFieldInput {
defaultValue: JSON
options: JSON
settings: JSON
isLabelSyncedWithName: Boolean
objectMetadataId: UUID!
isLabelSyncedWithName: Boolean
isRemoteCreation: Boolean
relationCreationPayload: JSON
morphRelationsCreationPayload: [JSON!]
@@ -4301,6 +4305,7 @@ input UpdateFieldInput {
defaultValue: JSON
options: JSON
settings: JSON
objectMetadataId: UUID
isLabelSyncedWithName: Boolean
morphRelationsUpdatePayload: [JSON!]
}
@@ -296,6 +296,7 @@ export interface Field {
defaultValue?: Scalars['JSON']
options?: Scalars['JSON']
settings?: Scalars['JSON']
objectMetadataId: Scalars['UUID']
isLabelSyncedWithName?: Scalars['Boolean']
morphId?: Scalars['UUID']
createdAt: Scalars['DateTime']
@@ -1937,6 +1938,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 +2305,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']
@@ -2676,7 +2677,7 @@ export interface CollectionHash {
__typename: 'CollectionHash'
}
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'frontComponent' | 'webhook'
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook'
export interface MinimalObjectMetadata {
id: Scalars['UUID']
@@ -2954,6 +2955,7 @@ export interface Mutation {
createViewGroup: ViewGroup
createManyViewGroups: ViewGroup[]
updateViewGroup: ViewGroup
updateManyViewGroups: ViewGroup[]
deleteViewGroup: ViewGroup
destroyViewGroup: ViewGroup
updateMessageFolder: MessageFolder
@@ -3360,6 +3362,7 @@ export interface FieldGenqlSelection{
defaultValue?: boolean | number
options?: boolean | number
settings?: boolean | number
objectMetadataId?: boolean | number
isLabelSyncedWithName?: boolean | number
morphId?: boolean | number
createdAt?: boolean | number
@@ -3476,7 +3479,7 @@ export interface ObjectGenqlSelection{
__scalar?: boolean | number
}
export interface FieldFilter {and?: (FieldFilter[] | null),or?: (FieldFilter[] | null),id?: (UUIDFilterComparison | null),isCustom?: (BooleanFieldComparison | null),isActive?: (BooleanFieldComparison | null),isSystem?: (BooleanFieldComparison | null),isUIReadOnly?: (BooleanFieldComparison | null)}
export interface FieldFilter {and?: (FieldFilter[] | null),or?: (FieldFilter[] | null),id?: (UUIDFilterComparison | null),isCustom?: (BooleanFieldComparison | null),isActive?: (BooleanFieldComparison | null),isSystem?: (BooleanFieldComparison | null),isUIReadOnly?: (BooleanFieldComparison | null),objectMetadataId?: (UUIDFilterComparison | null)}
export interface IndexFilter {and?: (IndexFilter[] | null),or?: (IndexFilter[] | null),id?: (UUIDFilterComparison | null),isCustom?: (BooleanFieldComparison | null)}
@@ -5096,6 +5099,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 +5511,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 +6210,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} })
@@ -6527,7 +6531,7 @@ export interface CreateOneFieldMetadataInput {
/** The record to create */
field: CreateFieldInput}
export interface CreateFieldInput {type: FieldMetadataType,name: Scalars['String'],label: Scalars['String'],description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),isCustom?: (Scalars['Boolean'] | null),isActive?: (Scalars['Boolean'] | null),isSystem?: (Scalars['Boolean'] | null),isUIReadOnly?: (Scalars['Boolean'] | null),isNullable?: (Scalars['Boolean'] | null),isUnique?: (Scalars['Boolean'] | null),defaultValue?: (Scalars['JSON'] | null),options?: (Scalars['JSON'] | null),settings?: (Scalars['JSON'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),objectMetadataId: Scalars['UUID'],isRemoteCreation?: (Scalars['Boolean'] | null),relationCreationPayload?: (Scalars['JSON'] | null),morphRelationsCreationPayload?: (Scalars['JSON'][] | null)}
export interface CreateFieldInput {type: FieldMetadataType,name: Scalars['String'],label: Scalars['String'],description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),isCustom?: (Scalars['Boolean'] | null),isActive?: (Scalars['Boolean'] | null),isSystem?: (Scalars['Boolean'] | null),isUIReadOnly?: (Scalars['Boolean'] | null),isNullable?: (Scalars['Boolean'] | null),isUnique?: (Scalars['Boolean'] | null),defaultValue?: (Scalars['JSON'] | null),options?: (Scalars['JSON'] | null),settings?: (Scalars['JSON'] | null),objectMetadataId: Scalars['UUID'],isLabelSyncedWithName?: (Scalars['Boolean'] | null),isRemoteCreation?: (Scalars['Boolean'] | null),relationCreationPayload?: (Scalars['JSON'] | null),morphRelationsCreationPayload?: (Scalars['JSON'][] | null)}
export interface UpdateOneFieldMetadataInput {
/** The id of the record to update */
@@ -6535,7 +6539,7 @@ id: Scalars['UUID'],
/** The record to update */
update: UpdateFieldInput}
export interface UpdateFieldInput {universalIdentifier?: (Scalars['String'] | null),name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),isSystem?: (Scalars['Boolean'] | null),isUIReadOnly?: (Scalars['Boolean'] | null),isNullable?: (Scalars['Boolean'] | null),isUnique?: (Scalars['Boolean'] | null),defaultValue?: (Scalars['JSON'] | null),options?: (Scalars['JSON'] | null),settings?: (Scalars['JSON'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),morphRelationsUpdatePayload?: (Scalars['JSON'][] | null)}
export interface UpdateFieldInput {universalIdentifier?: (Scalars['String'] | null),name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),isSystem?: (Scalars['Boolean'] | null),isUIReadOnly?: (Scalars['Boolean'] | null),isNullable?: (Scalars['Boolean'] | null),isUnique?: (Scalars['Boolean'] | null),defaultValue?: (Scalars['JSON'] | null),options?: (Scalars['JSON'] | null),settings?: (Scalars['JSON'] | null),objectMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),morphRelationsUpdatePayload?: (Scalars['JSON'][] | null)}
export interface DeleteOneFieldInput {
/** The id of the field to delete. */
@@ -8047,6 +8051,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 +8443,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"')
@@ -9425,6 +9429,7 @@ export const enumAllMetadataName = {
navigationMenuItem: 'navigationMenuItem' as const,
permissionFlag: 'permissionFlag' as const,
objectPermission: 'objectPermission' as const,
fieldPermission: 'fieldPermission' as const,
frontComponent: 'frontComponent' as const,
webhook: 'webhook' as const
}
@@ -63,8 +63,8 @@ export default {
210,
227,
244,
282,
283,
284,
299,
300,
325,
@@ -777,6 +777,9 @@ export default {
"settings": [
15
],
"objectMetadataId": [
3
],
"isLabelSyncedWithName": [
6
],
@@ -1164,6 +1167,9 @@ export default {
"isUIReadOnly": [
43
],
"objectMetadataId": [
42
],
"__typename": [
1
]
@@ -4458,6 +4464,14 @@ export default {
1
]
},
"LogicFunctionLogs": {
"logs": [
1
],
"__typename": [
1
]
},
"DeleteTwoFactorAuthenticationMethod": {
"success": [
6
@@ -4503,10 +4517,10 @@ export default {
},
"AuthTokenPair": {
"accessOrWorkspaceAgnosticToken": [
250
251
],
"refreshToken": [
250
251
],
"__typename": [
1
@@ -4514,7 +4528,7 @@ export default {
},
"AvailableWorkspacesAndAccessTokens": {
"tokens": [
251
252
],
"availableWorkspaces": [
224
@@ -4566,10 +4580,10 @@ export default {
},
"SignUp": {
"loginToken": [
250
251
],
"workspace": [
256
257
],
"__typename": [
1
@@ -4577,7 +4591,7 @@ export default {
},
"TransientToken": {
"transientToken": [
250
251
],
"__typename": [
1
@@ -4599,7 +4613,7 @@ export default {
},
"VerifyEmailAndGetLoginToken": {
"loginToken": [
250
251
],
"workspaceUrls": [
157
@@ -4618,7 +4632,7 @@ export default {
},
"AuthTokens": {
"tokens": [
251
252
],
"__typename": [
1
@@ -4626,7 +4640,7 @@ export default {
},
"LoginToken": {
"loginToken": [
250
251
],
"__typename": [
1
@@ -4656,10 +4670,10 @@ export default {
},
"Impersonate": {
"loginToken": [
250
251
],
"workspace": [
256
257
],
"__typename": [
1
@@ -4689,10 +4703,10 @@ export default {
},
"ApplicationTokenPair": {
"applicationAccessToken": [
250
251
],
"applicationRefreshToken": [
250
251
],
"__typename": [
1
@@ -4764,7 +4778,7 @@ export default {
1
],
"fields": [
271
272
],
"__typename": [
1
@@ -4861,10 +4875,10 @@ export default {
6
],
"objectPermissions": [
275
276
],
"fieldPermissions": [
276
277
],
"permissionFlags": [
1
@@ -4914,19 +4928,19 @@ export default {
1
],
"objects": [
272
],
"fields": [
271
],
"logicFunctions": [
273
],
"frontComponents": [
"fields": [
272
],
"logicFunctions": [
274
],
"frontComponents": [
275
],
"defaultRole": [
277
278
],
"sourcePackage": [
1
@@ -4986,13 +5000,13 @@ export default {
1
],
"driver": [
282
],
"status": [
283
],
"status": [
284
],
"verificationRecords": [
280
281
],
"verifiedAt": [
4
@@ -5039,7 +5053,7 @@ export default {
1
],
"location": [
285
286
],
"__typename": [
1
@@ -5067,13 +5081,13 @@ export default {
},
"ImapSmtpCaldavConnectionParameters": {
"IMAP": [
287
288
],
"SMTP": [
287
288
],
"CALDAV": [
287
288
],
"__typename": [
1
@@ -5093,7 +5107,7 @@ export default {
3
],
"connectionParameters": [
288
289
],
"__typename": [
1
@@ -5157,7 +5171,7 @@ export default {
1
],
"dailyUsage": [
293
294
],
"__typename": [
1
@@ -5165,13 +5179,13 @@ export default {
},
"UsageAnalytics": {
"usageByUser": [
292
293
],
"usageByOperationType": [
292
293
],
"timeSeries": [
293
294
],
"periodStart": [
4
@@ -5180,15 +5194,7 @@ export default {
4
],
"userDailyUsage": [
294
],
"__typename": [
1
]
},
"LogicFunctionLogs": {
"logs": [
1
295
],
"__typename": [
1
@@ -5235,7 +5241,7 @@ export default {
6
],
"applicationTokenPair": [
269
270
],
"__typename": [
1
@@ -6656,7 +6662,7 @@ export default {
}
],
"checkUserExists": [
264,
265,
{
"email": [
1,
@@ -6668,7 +6674,7 @@ export default {
}
],
"checkWorkspaceInviteHashIsValid": [
265,
266,
{
"inviteHash": [
1,
@@ -6686,7 +6692,7 @@ export default {
}
],
"validatePasswordResetToken": [
259,
260,
{
"passwordResetToken": [
1,
@@ -6769,7 +6775,7 @@ export default {
220
],
"getConnectedImapSmtpCaldavAccount": [
289,
290,
{
"id": [
3,
@@ -6778,7 +6784,7 @@ export default {
}
],
"getAutoCompleteAddress": [
284,
285,
{
"address": [
1,
@@ -6797,7 +6803,7 @@ export default {
}
],
"getAddressDetails": [
286,
287,
{
"placeId": [
1,
@@ -6889,19 +6895,19 @@ export default {
}
],
"getPostgresCredentials": [
291
292
],
"findManyPublicDomains": [
279
280
],
"getEmailingDomains": [
281
282
],
"findManyMarketplaceApps": [
278
279
],
"findOneMarketplaceApp": [
278,
279,
{
"universalIdentifier": [
1,
@@ -6924,7 +6930,7 @@ export default {
}
],
"getUsageAnalytics": [
295,
296,
{
"input": [
365
@@ -8130,6 +8136,15 @@ export default {
]
}
],
"updateManyViewGroups": [
56,
{
"inputs": [
450,
"[UpdateViewGroupInput!]!"
]
}
],
"deleteViewGroup": [
56,
{
@@ -8300,7 +8315,7 @@ export default {
}
],
"getAuthorizationUrlForSSO": [
254,
255,
{
"input": [
466,
@@ -8309,7 +8324,7 @@ export default {
}
],
"getLoginTokenFromCredentials": [
263,
264,
{
"email": [
1,
@@ -8335,7 +8350,7 @@ export default {
}
],
"signIn": [
252,
253,
{
"email": [
1,
@@ -8357,7 +8372,7 @@ export default {
}
],
"verifyEmailAndGetLoginToken": [
260,
261,
{
"emailVerificationToken": [
1,
@@ -8377,7 +8392,7 @@ export default {
}
],
"verifyEmailAndGetWorkspaceAgnosticToken": [
252,
253,
{
"emailVerificationToken": [
1,
@@ -8393,7 +8408,7 @@ export default {
}
],
"getAuthTokensFromOTP": [
262,
263,
{
"otp": [
1,
@@ -8413,7 +8428,7 @@ export default {
}
],
"signUp": [
252,
253,
{
"email": [
1,
@@ -8435,7 +8450,7 @@ export default {
}
],
"signUpInWorkspace": [
257,
258,
{
"email": [
1,
@@ -8466,13 +8481,13 @@ export default {
}
],
"signUpInNewWorkspace": [
257
],
"generateTransientToken": [
258
],
"generateTransientToken": [
259
],
"getAuthTokensFromLoginToken": [
262,
263,
{
"loginToken": [
1,
@@ -8485,7 +8500,7 @@ export default {
}
],
"authorizeApp": [
249,
250,
{
"clientId": [
1,
@@ -8507,7 +8522,7 @@ export default {
}
],
"renewToken": [
262,
263,
{
"appToken": [
1,
@@ -8516,7 +8531,7 @@ export default {
}
],
"generateApiKeyToken": [
261,
262,
{
"apiKeyId": [
3,
@@ -8529,7 +8544,7 @@ export default {
}
],
"emailPasswordResetLink": [
253,
254,
{
"email": [
1,
@@ -8541,7 +8556,7 @@ export default {
}
],
"updatePasswordViaResetToken": [
255,
256,
{
"passwordResetToken": [
1,
@@ -8642,7 +8657,7 @@ export default {
}
],
"initiateOTPProvisioning": [
247,
248,
{
"loginToken": [
1,
@@ -8655,10 +8670,10 @@ export default {
}
],
"initiateOTPProvisioningForAuthenticatedUser": [
247
248
],
"deleteTwoFactorAuthenticationMethod": [
246,
247,
{
"twoFactorAuthenticationMethodId": [
3,
@@ -8667,7 +8682,7 @@ export default {
}
],
"verifyTwoFactorAuthenticationMethodForAuthenticatedUser": [
248,
249,
{
"otp": [
1,
@@ -8773,7 +8788,7 @@ export default {
}
],
"impersonate": [
266,
267,
{
"userId": [
3,
@@ -8795,7 +8810,7 @@ export default {
}
],
"saveImapSmtpCaldavAccount": [
290,
291,
{
"accountOwnerId": [
3,
@@ -8998,13 +9013,13 @@ export default {
}
],
"enablePostgresProxy": [
291
292
],
"disablePostgresProxy": [
291
292
],
"createPublicDomain": [
279,
280,
{
"domain": [
1,
@@ -9031,14 +9046,14 @@ export default {
}
],
"createEmailingDomain": [
281,
282,
{
"domain": [
1,
"String!"
],
"driver": [
282,
283,
"EmailingDomainDriver!"
]
}
@@ -9053,7 +9068,7 @@ export default {
}
],
"verifyEmailingDomain": [
281,
282,
{
"id": [
1,
@@ -9130,7 +9145,7 @@ export default {
}
],
"createDevelopmentApplication": [
267,
268,
{
"universalIdentifier": [
1,
@@ -9143,7 +9158,7 @@ export default {
}
],
"generateApplicationToken": [
269,
270,
{
"applicationId": [
3,
@@ -9152,7 +9167,7 @@ export default {
}
],
"syncApplication": [
268,
269,
{
"manifest": [
15,
@@ -9161,7 +9176,7 @@ export default {
}
],
"uploadApplicationFile": [
270,
271,
{
"file": [
372,
@@ -9195,7 +9210,7 @@ export default {
}
],
"renewApplicationToken": [
269,
270,
{
"applicationRefreshToken": [
1,
@@ -10745,12 +10760,12 @@ export default {
"settings": [
15
],
"isLabelSyncedWithName": [
6
],
"objectMetadataId": [
3
],
"isLabelSyncedWithName": [
6
],
"isRemoteCreation": [
6
],
@@ -10815,6 +10830,9 @@ export default {
"settings": [
15
],
"objectMetadataId": [
3
],
"isLabelSyncedWithName": [
6
],
@@ -11455,7 +11473,7 @@ export default {
}
],
"logicFunctionLogs": [
296,
246,
{
"input": [
490,
File diff suppressed because one or more lines are too long
@@ -6686,6 +6686,11 @@ msgstr "Veldnaam"
msgid "Field on destination"
msgstr "Veld op bestemming"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "اسم الحقل"
msgid "Field on destination"
msgstr "حقل في الوجهة"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Nom del camp"
msgid "Field on destination"
msgstr "Camp a la destinació"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Název pole"
msgid "Field on destination"
msgstr "Pole v cíli"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Feltnavn"
msgid "Field on destination"
msgstr "Felt på destination"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Feldname"
msgid "Field on destination"
msgstr "Feld am Ziel"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Όνομα Πεδίου"
msgid "Field on destination"
msgstr "Πεδίο στον προορισμό"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
+5
View File
@@ -6681,6 +6681,11 @@ msgstr "Field Name"
msgid "Field on destination"
msgstr "Field on destination"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr "field permission"
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Nombre del campo"
msgid "Field on destination"
msgstr "Campo en destino"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Kentän nimi"
msgid "Field on destination"
msgstr "Kenttä kohteessa"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Nom du champ"
msgid "Field on destination"
msgstr "Champ sur la destination"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6686,6 +6686,11 @@ msgstr "שם שדה"
msgid "Field on destination"
msgstr "שדה ביעד"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Mező neve"
msgid "Field on destination"
msgstr "Mező a célállomáson"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Nome del campo"
msgid "Field on destination"
msgstr "Campo sulla destinazione"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "フィールド名"
msgid "Field on destination"
msgstr "宛先のフィールド"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "필드 이름"
msgid "Field on destination"
msgstr "대상 필드"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Veldnaam"
msgid "Field on destination"
msgstr "Veld bij bestemming"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Feltnavn"
msgid "Field on destination"
msgstr "Felt på destinasjon"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Nazwa pola"
msgid "Field on destination"
msgstr "Pole na miejscu docelowym"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6681,6 +6681,11 @@ msgstr ""
msgid "Field on destination"
msgstr ""
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Nome do campo"
msgid "Field on destination"
msgstr "Campo no destino"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Nome do Campo"
msgid "Field on destination"
msgstr "Campo no destino"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Nume câmp"
msgid "Field on destination"
msgstr "Câmp pe destinație"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
Binary file not shown.
@@ -6686,6 +6686,11 @@ msgstr "Име поља"
msgid "Field on destination"
msgstr "Поље на одредишту"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Fältnamn"
msgid "Field on destination"
msgstr "Fält på destinationen"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Alan Adı"
msgid "Field on destination"
msgstr "Hedefteki alan"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Назва поля"
msgid "Field on destination"
msgstr "Поле на призначенні"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "Tên Trường"
msgid "Field on destination"
msgstr "Trường ở điểm đến"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "字段名称"
msgid "Field on destination"
msgstr "目的地字段"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -6686,6 +6686,11 @@ msgstr "欄位名稱"
msgid "Field on destination"
msgstr "目的地資料欄位"
#. js-lingui-id: P8JCfj
#: src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts
msgid "field permission"
msgstr ""
#. js-lingui-id: RnX5oc
#: src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx
msgid "fields"
@@ -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));
}
};
@@ -32,6 +32,7 @@ export const useMetadataErrorHandler = () => {
logicFunction: t`logic function`,
permissionFlag: t`permission flag`,
objectPermission: t`object permission`,
fieldPermission: t`field permission`,
role: t`role`,
roleTarget: t`role target`,
agent: t`agent`,
@@ -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>
);
};
@@ -20,12 +20,14 @@ export type FieldMetadataItem = Omit<
| '__typename'
| 'applicationId'
| 'defaultValue'
| 'objectMetadataId'
| 'options'
| 'relation'
| 'morphRelations'
> & {
__typename?: string;
applicationId?: string;
objectMetadataId?: string;
defaultValue?: any;
options?: FieldMetadataItemOption[] | null;
relation?: FieldMetadataItemRelation | null;
@@ -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)}`,
);
}
}
}
}
@@ -0,0 +1,111 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, IsNull, type Repository } from 'typeorm';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@Command({
name: 'upgrade:1-20:identify-field-permission-metadata',
description:
'Identify field permission metadata (backfill universalIdentifier and applicationId)',
})
export class IdentifyFieldPermissionMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
private hasRunOnce = false;
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
options,
}: RunOnWorkspaceArgs): Promise<void> {
if (this.hasRunOnce) {
this.logger.log(
'Skipping has already been run once IdentifyFieldPermissionMetadataCommand',
);
return;
}
if (options.dryRun) {
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const repository = queryRunner.manager.getRepository(
FieldPermissionEntity,
);
const withNullApplicationId = await repository.find({
where: { applicationId: IsNull() },
relations: ['role'],
});
const toUpdate = withNullApplicationId.filter((fieldPermission) =>
isDefined(fieldPermission.role?.applicationId),
);
const toRemove = withNullApplicationId.filter(
(fieldPermission) => !isDefined(fieldPermission.role?.applicationId),
);
for (const fieldPermission of toUpdate) {
fieldPermission.applicationId = fieldPermission.role!.applicationId;
fieldPermission.universalIdentifier =
fieldPermission.universalIdentifier ?? v4();
}
if (toUpdate.length > 0) {
await repository.save(toUpdate);
}
if (toRemove.length > 0) {
await repository.remove(toRemove);
}
const withNullUniversalIdentifier = await repository.find({
where: { universalIdentifier: IsNull() },
});
for (const fieldPermission of withNullUniversalIdentifier) {
fieldPermission.universalIdentifier = v4();
}
if (withNullUniversalIdentifier.length > 0) {
await repository.save(withNullUniversalIdentifier);
}
await queryRunner.commitTransaction();
this.logger.log(
'Successfully run IdentifyFieldPermissionMetadataCommand',
);
this.hasRunOnce = true;
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Rolling back IdentifyFieldPermissionMetadataCommand: ${error.message}`,
);
throw error;
} finally {
await queryRunner.release();
}
}
}
@@ -0,0 +1,72 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, type 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 { makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773400000000-make-field-permission-universal-identifier-and-application-id-not-null.util';
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:make-field-permission-universal-identifier-and-application-id-not-nullable-migration',
description:
'Set NOT NULL on fieldPermission universalIdentifier and applicationId, add unique index and FK (run identify-field-permission-metadata first)',
})
export class MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
private hasRunOnce = false;
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
options,
}: RunOnWorkspaceArgs): Promise<void> {
if (this.hasRunOnce) {
this.logger.log(
'Skipping has already been run once MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
);
return;
}
if (options.dryRun) {
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries(
queryRunner,
);
await queryRunner.commitTransaction();
this.logger.log(
'Successfully run MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
);
this.hasRunOnce = true;
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Rolling back MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
);
throw error;
} finally {
await queryRunner.release();
}
}
}
@@ -2,14 +2,14 @@ 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 { IdentifyFieldPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.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 { MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-field-permission-universal-identifier-and-application-id-not-nullable-migration.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';
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command';
@@ -18,10 +18,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 +39,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
imports: [
TypeOrmModule.forFeature([
WorkspaceEntity,
ApplicationEntity,
ConnectedAccountEntity,
MessageChannelEntity,
CalendarChannelEntity,
@@ -58,7 +55,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
ApplicationRegistrationModule,
WorkspaceMigrationModule,
FeatureFlagModule,
SdkClientModule,
WorkflowCommonModule,
],
providers: [
@@ -66,13 +62,13 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyObjectPermissionMetadataCommand,
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyFieldPermissionMetadataCommand,
MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
BackfillCommandMenuItemsCommand,
BackfillFieldWidgetsCommand,
BackfillNavigationMenuItemTypeCommand,
BackfillPageLayoutsCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
BackfillSelectFieldOptionIdsCommand,
DeleteOrphanNavigationMenuItemsCommand,
GenerateApplicationSdkClientsCommand,
SeedCliApplicationRegistrationCommand,
MigrateRichTextToTextCommand,
MigrateMessagingInfrastructureToMetadataCommand,
@@ -84,13 +80,13 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyObjectPermissionMetadataCommand,
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
IdentifyFieldPermissionMetadataCommand,
MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
BackfillCommandMenuItemsCommand,
BackfillFieldWidgetsCommand,
BackfillNavigationMenuItemTypeCommand,
BackfillPageLayoutsCommand,
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
BackfillSelectFieldOptionIdsCommand,
DeleteOrphanNavigationMenuItemsCommand,
GenerateApplicationSdkClientsCommand,
SeedCliApplicationRegistrationCommand,
MigrateRichTextToTextCommand,
MigrateMessagingInfrastructureToMetadataCommand,
@@ -34,19 +34,19 @@ 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 { IdentifyFieldPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-field-permission-metadata.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 { MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-field-permission-universal-identifier-and-application-id-not-nullable-migration.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';
import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-permission-flag-universal-identifier-and-application-id-not-nullable-migration.command';
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';
@@ -103,15 +103,15 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly makePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly identifyObjectPermissionMetadataCommand: IdentifyObjectPermissionMetadataCommand,
protected readonly makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly identifyFieldPermissionMetadataCommand: IdentifyFieldPermissionMetadataCommand,
protected readonly makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
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,
@@ -168,18 +168,19 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.identifyObjectPermissionMetadataCommand,
this
.makeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this.identifyFieldPermissionMetadataCommand,
this
.makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this.backfillNavigationMenuItemTypeCommand,
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 = {
@@ -0,0 +1,25 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddUniversalIdentifierAndApplicationIdToFieldPermission1773400000000
implements MigrationInterface
{
name = 'AddUniversalIdentifierAndApplicationIdToFieldPermission1773400000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ADD "universalIdentifier" uuid`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ADD "applicationId" uuid`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" DROP COLUMN IF EXISTS "applicationId"`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" DROP COLUMN IF EXISTS "universalIdentifier"`,
);
}
}
@@ -0,0 +1,58 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
import { makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries } from 'src/database/typeorm/core/migrations/utils/1773400000000-make-field-permission-universal-identifier-and-application-id-not-null.util';
export class MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNull1773400000001
implements MigrationInterface
{
name =
'MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNull1773400000001';
public async up(queryRunner: QueryRunner): Promise<void> {
const savepointName =
'sp_make_field_permission_universal_identifier_and_application_id_not_null';
try {
await queryRunner.query(`SAVEPOINT ${savepointName}`);
await makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries(
queryRunner,
);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (e) {
try {
await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (rollbackError) {
// oxlint-disable-next-line no-console
console.error(
'Failed to rollback to savepoint in MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNull1773400000001',
rollbackError,
);
throw rollbackError;
}
// oxlint-disable-next-line no-console
console.error(
'Swallowing MakeFieldPermissionUniversalIdentifierAndApplicationIdNotNull1773400000001 error',
e,
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" DROP CONSTRAINT IF EXISTS "FK_71cc60c4a1c9f8a7c434d91d38c"`,
);
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."IDX_0dedb90c717e179ef653c512b9"`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ALTER COLUMN "applicationId" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ALTER COLUMN "universalIdentifier" DROP NOT NULL`,
);
}
}
@@ -0,0 +1,20 @@
import { type QueryRunner } from 'typeorm';
export const makeFieldPermissionUniversalIdentifierAndApplicationIdNotNullQueries =
async (queryRunner: QueryRunner): Promise<void> => {
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ALTER COLUMN "universalIdentifier" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ALTER COLUMN "applicationId" SET NOT NULL`,
);
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."IDX_0dedb90c717e179ef653c512b9"`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_0dedb90c717e179ef653c512b9" ON "core"."fieldPermission" ("workspaceId", "universalIdentifier")`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldPermission" ADD CONSTRAINT "FK_71cc60c4a1c9f8a7c434d91d38c" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
};

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