Compare commits

...
Author SHA1 Message Date
7f1814805d Fix backfill record page layout command (#19043)
Co-authored-by: Charles Bochet <[email protected]>
2026-03-27 16:56:30 +01:00
34b81adce8 i18n - translations (#19046)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-03-27 16:38:08 +01:00
Félix MalfaitandGitHub cd2e08b912 Enforce workspace count limit for multi-workspace setups (#19036)
## Summary
- Limits workspace creation to 5 workspaces per server when no valid
enterprise key is configured
- Enterprise key validity is checked first (synchronous, in-memory
cached) to avoid unnecessary DB queries on enterprise deployments
- Adds 4 unit tests covering: limit enforcement, enterprise key bypass,
below-limit creation, and performance (no enterprise check when
workspace count is zero)

## Test plan
- [x] All 11 unit tests pass (8 existing + 3 new behavioral + 1
performance assertion)
- [ ] Manual: verify workspace creation blocked at limit=5 without
enterprise key
- [ ] Manual: verify workspace creation allowed beyond limit with valid
enterprise key
- [ ] Manual: verify first workspace (bootstrap) creation is unaffected


Made with [Cursor](https://cursor.com)
2026-03-27 16:31:27 +01:00
Charles BochetandGitHub fb21f3ccf5 fix: resolve availabilityObjectMetadataUniversalIdentifier in backfill command (#19041)
## Summary

- Fixes workflow manual trigger command menu items losing their
`availabilityObjectMetadataId` during the 1-20 backfill upgrade command
- The migration runner's `transpileUniversalActionToFlatAction` resolves
`availabilityObjectMetadataId` **from**
`availabilityObjectMetadataUniversalIdentifier`, overwriting the
original value. The backfill was hardcoding the universal identifier to
`null`, causing all `RECORD_SELECTION` items to end up with `NULL`
`availabilityObjectMetadataId` after persistence.
- Now properly resolves `availabilityObjectMetadataUniversalIdentifier`
from `flatObjectMetadataMaps.universalIdentifierById` so the runner can
correctly derive the FK during INSERT.
2026-03-27 16:01:31 +01:00
Paul RastoinandGitHub 281bb6d783 Guard yarn database:migrate:prod (#19008)
## Motivations
A lot of self hosters hands up using the `yarn database:migrated:prod`
either manually or through AI assisted debug while they try to upgrade
an instance while their workspace is still blocked in a previous one
Leading to their whole database permanent corruption

## What happened
Replaced the direct call the the typeorm cli to a command calling it
programmatically, adding a layer of security in case a workspace seems
to be blocked in a previous version than the one just before the one
being installed ( e.g 1.0 when you try to upgrade from 1.1 to 1.2 )

For our cloud we still need a way to bypass this security explaining the
-f flag

## Remark
Centralized this logic and refactored creating new services
`WorkspaceVersionService` and `CoreEngineVersionService` that will
become useful for the upcoming upgrade refactor

Related to https://github.com/twentyhq/twenty-infra/pull/529
2026-03-27 14:39:18 +00:00
c96c034908 i18n - translations (#19040)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
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 <[email protected]>
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 <[email protected]>
2026-03-27 13:53:30 +01:00
Abdullah.andGitHub 22c9693ce5 First PR to bring in the new twenty website. (#19035)
This PR contains Menu, Hero, TrustedBy, Problem, ThreeCards and Footer
sections of the new website.

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

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

---------

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

  Two bugs related to the Files field:

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


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

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


 ## Reproduction


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

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


## After 


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

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

## Root cause

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

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

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

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

## Test plan

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


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

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

## Changes

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

## Related Issue

Fixes #18865

## Testing

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

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

---------

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

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

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

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

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

## Testing
- Not run (not requested)
2026-03-26 17:48:20 +01:00
499 changed files with 18406 additions and 7645 deletions
@@ -143,7 +143,6 @@ jobs:
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Seed current branch database with test data
run: |
@@ -296,7 +295,6 @@ jobs:
set_env_var "CLICKHOUSE_PASSWORD" "clickhousePassword"
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Seed main branch database with test data
run: |
-1
View File
@@ -122,7 +122,6 @@ jobs:
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
npx nx run twenty-server:database:init:prod
npx nx run twenty-server:database:migrate:prod
- name: Worker / Run
run: |
timeout 30s npx nx run twenty-server:worker || exit_code=$?
+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,
@@ -32,11 +32,11 @@ has_schema=$(PGPASSWORD=twenty psql -h localhost -U twenty -d default -tAc \
if [ "$has_schema" = "f" ]; then
echo "Database appears to be empty, running initial setup..."
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/database/scripts/setup-db.js
fi
# Always run migrations (idempotent — skips already-applied ones)
yarn database:migrate:prod
yarn database:migrate:prod --force
yarn command:prod cache:flush
yarn command:prod upgrade
-6
View File
@@ -38,12 +38,6 @@ RUN npx nx run twenty-server:lingui:extract && \
RUN npx nx run twenty-server:build
# Bundle setup-db script into a standalone JS file so the final image
# doesn't need tsx or the TypeScript source tree at runtime.
RUN npx esbuild packages/twenty-server/scripts/setup-db.ts \
--bundle --platform=node --outfile=packages/twenty-server/dist/scripts/setup-db.js \
--external:typeorm --external:dotenv --external:pg
# Clean server build output (type declarations and compiled tests are not needed at runtime;
# source maps are kept because twenty-infra extracts them from the image for Sentry uploads)
RUN find /app/packages/twenty-server/dist -name '*.d.ts' -delete \
+1 -2
View File
@@ -13,8 +13,7 @@ setup_and_migrate_db() {
has_schema=$(psql -tAc "SELECT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'core')" ${PG_DATABASE_URL})
if [ "$has_schema" = "f" ]; then
echo "Database appears to be empty, running migrations."
NODE_OPTIONS="--max-old-space-size=1500" node ./dist/scripts/setup-db.js
yarn database:migrate:prod
yarn database:init:prod
fi
yarn command:prod cache:flush
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,
+2 -2
View File
@@ -9,8 +9,8 @@
"start:prod": "node dist/main",
"command:prod": "node dist/command/command",
"worker:prod": "node dist/queue-worker/queue-worker",
"database:init:prod": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate:prod",
"database:migrate:prod": "npx -y typeorm migration:run -d dist/database/typeorm/core/core.datasource",
"database:init:prod": "node dist/database/scripts/setup-db.js && yarn database:migrate:prod --force",
"database:migrate:prod": "node dist/command/command run-typeorm-migration",
"clickhouse:migrate:prod": "node dist/database/clickHouse/migrations/run-migrations.js",
"typeorm": "../../node_modules/typeorm/.bin/typeorm"
},
+4 -4
View File
@@ -246,8 +246,8 @@
"no-seed": {
"cwd": "packages/twenty-server",
"commands": [
"nx ts-node-no-deps-transpile-only -- ./scripts/truncate-db.ts",
"nx ts-node-no-deps-transpile-only -- ./scripts/setup-db.ts",
"node dist/database/scripts/truncate-db.js",
"node dist/database/scripts/setup-db.js",
"nx database:migrate",
"nx command-no-deps -- cache:flush"
],
@@ -256,8 +256,8 @@
"seed": {
"cwd": "packages/twenty-server",
"commands": [
"nx ts-node-no-deps-transpile-only -- ./scripts/truncate-db.ts",
"nx ts-node-no-deps-transpile-only -- ./scripts/setup-db.ts",
"node dist/database/scripts/truncate-db.js",
"node dist/database/scripts/setup-db.js",
"nx database:migrate",
"nx command-no-deps -- cache:flush",
"nx command-no-deps -- workspace:seed:dev"
@@ -1,21 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`UpgradeCommandRunner Workspace upgrade should fail when APP_VERSION is not defined 1`] = `[Error: Cannot run upgrade command when APP_VERSION is not defined, please double check your env variables]`;
exports[`UpgradeCommandRunner Workspace upgrade should fail when all commands contains invalid semver keys 1`] = `[Error: No previous version found for version 2.0.0. Please review the "allCommands" record. Available versions are: invalid, 2.0.0]`;
exports[`UpgradeCommandRunner Workspace upgrade should fail when current version commands are not found 1`] = `[Error: No command found for version 42.0.0. Please check the commands record.]`;
exports[`UpgradeCommandRunner Workspace upgrade should fail when previous version is not found 1`] = `[Error: No previous version found for version 1.0.0. Please review the "allCommands" record. Available versions are: 1.0.0, 2.0.0]`;
exports[`UpgradeCommandRunner Workspace upgrade should fail when workspace version is not defined 1`] = `
[Error: Unable to run the upgrade command. Aborting the upgrade process.
Please ensure that all workspaces are on at least the previous minor version (1.0.0).
If any workspaces are not on the previous minor version, roll back to that version and run the upgrade command again.]
`;
exports[`UpgradeCommandRunner Workspace upgrade should fail when workspace version is not equal to fromVersion 1`] = `
[Error: Unable to run the upgrade command. Aborting the upgrade process.
Please ensure that all workspaces are on at least the previous minor version (1.0.0).
If any workspaces are not on the previous minor version, roll back to that version and run the upgrade command again.]
`;
@@ -7,35 +7,41 @@ import {
} from 'twenty-shared/testing';
import { type Repository } from 'typeorm';
import { UpgradeCommandRunner } from 'src/database/commands/command-runners/upgrade.command-runner';
import {
UpgradeCommandRunner,
type AllCommands,
} from 'src/database/commands/command-runners/upgrade.command-runner';
import { CoreMigrationRunnerService } from 'src/database/commands/core-migration-runner/services/core-migration-runner.service';
import { UPGRADE_COMMAND_SUPPORTED_VERSIONS } from 'src/engine/constants/upgrade-command-supported-versions.constant';
import { CoreEngineVersionService } from 'src/engine/core-engine-version/services/core-engine-version.service';
import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.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';
import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service';
const CURRENT_VERSION =
UPGRADE_COMMAND_SUPPORTED_VERSIONS[
UPGRADE_COMMAND_SUPPORTED_VERSIONS.length - 1
];
const PREVIOUS_VERSION =
UPGRADE_COMMAND_SUPPORTED_VERSIONS[
UPGRADE_COMMAND_SUPPORTED_VERSIONS.length - 2
];
class BasicUpgradeCommandRunner extends UpgradeCommandRunner {
allCommands = {
'1.0.0': [],
'2.0.0': [],
};
allCommands = Object.fromEntries(
UPGRADE_COMMAND_SUPPORTED_VERSIONS.map((version) => [version, []]),
) as unknown as AllCommands;
}
class InvalidUpgradeCommandRunner extends UpgradeCommandRunner {
allCommands = {
invalid: [],
'2.0.0': [],
};
}
type CommandRunnerValues =
| typeof BasicUpgradeCommandRunner
| typeof InvalidUpgradeCommandRunner;
type CommandRunnerValues = typeof BasicUpgradeCommandRunner;
const generateMockWorkspace = (overrides?: Partial<WorkspaceEntity>) =>
({
id: 'workspace-id',
version: '1.0.0',
version: PREVIOUS_VERSION,
createdAt: new Date(),
updatedAt: new Date(),
allowImpersonation: false,
@@ -73,12 +79,18 @@ const buildUpgradeCommandModule = async ({
twentyConfigService: TwentyConfigService,
globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
dataSourceService: DataSourceService,
coreEngineVersionService: CoreEngineVersionService,
workspaceVersionService: WorkspaceVersionService,
coreMigrationRunnerService: CoreMigrationRunnerService,
) => {
return new commandRunner(
workspaceRepository,
twentyConfigService,
globalWorkspaceOrmManager,
dataSourceService,
coreEngineVersionService,
workspaceVersionService,
coreMigrationRunnerService,
);
},
inject: [
@@ -86,6 +98,9 @@ const buildUpgradeCommandModule = async ({
TwentyConfigService,
GlobalWorkspaceOrmManager,
DataSourceService,
CoreEngineVersionService,
WorkspaceVersionService,
CoreMigrationRunnerService,
],
},
{
@@ -98,6 +113,7 @@ const buildUpgradeCommandModule = async ({
),
update: jest.fn(),
find: jest.fn().mockResolvedValue(workspaces),
exists: jest.fn().mockResolvedValue(workspaces.length > 0),
},
},
{
@@ -130,6 +146,12 @@ const buildUpgradeCommandModule = async ({
provide: DataSourceService,
useValue: mockDataSourceService,
},
CoreEngineVersionService,
WorkspaceVersionService,
{
provide: CoreMigrationRunnerService,
useValue: { run: jest.fn().mockResolvedValue(undefined) },
},
],
}).compile();
@@ -139,7 +161,7 @@ const buildUpgradeCommandModule = async ({
describe('UpgradeCommandRunner', () => {
let upgradeCommandRunner: BasicUpgradeCommandRunner;
let workspaceRepository: Repository<WorkspaceEntity>;
let runCoreMigrationsSpy: jest.SpyInstance;
let coreMigrationRunnerService: CoreMigrationRunnerService;
type BuildModuleAndSetupSpiesArgs = {
numberOfWorkspace?: number;
@@ -153,7 +175,7 @@ describe('UpgradeCommandRunner', () => {
workspaceOverride,
workspaces,
commandRunner = BasicUpgradeCommandRunner,
appVersion = '2.0.0',
appVersion = CURRENT_VERSION,
}: BuildModuleAndSetupSpiesArgs) => {
const generatedWorkspaces = Array.from(
{ length: numberOfWorkspace },
@@ -176,9 +198,8 @@ describe('UpgradeCommandRunner', () => {
jest.spyOn(upgradeCommandRunner['logger'], 'warn').mockImplementation();
jest.spyOn(upgradeCommandRunner, 'runOnWorkspace');
runCoreMigrationsSpy = jest
.spyOn(upgradeCommandRunner, 'runCoreMigrations')
.mockImplementation(() => Promise.resolve());
coreMigrationRunnerService = module.get(CoreMigrationRunnerService);
workspaceRepository = module.get<Repository<WorkspaceEntity>>(
getRepositoryToken(WorkspaceEntity),
@@ -190,12 +211,10 @@ describe('UpgradeCommandRunner', () => {
id: 'higher_version_workspace',
version: '42.42.42',
});
const appVersion = '2.0.0';
await buildModuleAndSetupSpies({
numberOfWorkspace: 0,
workspaces: [higherVersionWorkspace],
appVersion,
});
// @ts-expect-error legacy noImplicitAny
const passedParams = [];
@@ -221,11 +240,9 @@ describe('UpgradeCommandRunner', () => {
it('should run upgrade over several workspaces', async () => {
const numberOfWorkspace = 42;
const appVersion = '2.0.0';
await buildModuleAndSetupSpies({
numberOfWorkspace,
appVersion,
});
// @ts-expect-error legacy noImplicitAny
const passedParams = [];
@@ -240,7 +257,7 @@ describe('UpgradeCommandRunner', () => {
expect(workspaceRepository.update).toHaveBeenNthCalledWith(
numberOfWorkspace,
{ id: expect.any(String) },
{ version: appVersion },
{ version: CURRENT_VERSION },
);
expect(upgradeCommandRunner.migrationReport.success.length).toBe(42);
expect(upgradeCommandRunner.migrationReport.fail.length).toBe(0);
@@ -254,9 +271,9 @@ describe('UpgradeCommandRunner', () => {
title: 'even if workspace version and app version differ in patch',
context: {
input: {
appVersion: 'v2.0.0',
appVersion: `v${CURRENT_VERSION}`,
workspaceOverride: {
version: 'v1.0.12',
version: `v${PREVIOUS_VERSION.replace('.0', '.12')}`,
},
},
},
@@ -266,9 +283,9 @@ describe('UpgradeCommandRunner', () => {
'even if workspace version and app version differ in patch and semantic',
context: {
input: {
appVersion: 'v2.0.0',
appVersion: `v${CURRENT_VERSION}`,
workspaceOverride: {
version: '1.0.12',
version: PREVIOUS_VERSION.replace('.0', '.12'),
},
},
},
@@ -277,9 +294,9 @@ describe('UpgradeCommandRunner', () => {
title: 'even if app version contains a patch value',
context: {
input: {
appVersion: '2.0.24',
appVersion: CURRENT_VERSION.replace('.0', '.24'),
workspaceOverride: {
version: '1.0.12',
version: PREVIOUS_VERSION.replace('.0', '.12'),
},
},
},
@@ -303,7 +320,7 @@ describe('UpgradeCommandRunner', () => {
expect(failReport.length).toBe(0);
expect(successReport.length).toBe(1);
expect(runCoreMigrationsSpy).toHaveBeenCalledTimes(1);
expect(coreMigrationRunnerService.run).toHaveBeenCalledTimes(1);
const { workspaceId } = successReport[0];
expect(workspaceId).toBe('workspace_0');
@@ -316,19 +333,20 @@ describe('UpgradeCommandRunner', () => {
input: Omit<BuildModuleAndSetupSpiesArgs, 'numberOfWorkspace'>;
output?: {
failReportWorkspaceId: string;
expectedErrorMessage: string;
};
}>[] = [
{
title: 'when workspace version is not equal to fromVersion',
context: {
input: {
appVersion: '2.0.0',
workspaceOverride: {
version: '0.1.0',
},
},
output: {
failReportWorkspaceId: 'workspace_0',
expectedErrorMessage: `Unable to run the upgrade command. Aborting the upgrade process.\nPlease ensure that all workspaces are on at least the previous minor version (${PREVIOUS_VERSION}).\nIf any workspaces are not on the previous minor version, roll back to that version and run the upgrade command again.`,
},
},
},
@@ -342,6 +360,7 @@ describe('UpgradeCommandRunner', () => {
},
output: {
failReportWorkspaceId: 'workspace_0',
expectedErrorMessage: `Unable to run the upgrade command. Aborting the upgrade process.\nPlease ensure that all workspaces are on at least the previous minor version (${PREVIOUS_VERSION}).\nIf any workspaces are not on the previous minor version, roll back to that version and run the upgrade command again.`,
},
},
},
@@ -353,6 +372,8 @@ describe('UpgradeCommandRunner', () => {
},
output: {
failReportWorkspaceId: 'global',
expectedErrorMessage:
'APP_VERSION is not defined, please double check your env variables',
},
},
},
@@ -364,6 +385,8 @@ describe('UpgradeCommandRunner', () => {
},
output: {
failReportWorkspaceId: 'global',
expectedErrorMessage:
'No command found for version 42.0.0. Please check the commands record.',
},
},
},
@@ -371,15 +394,11 @@ describe('UpgradeCommandRunner', () => {
title: 'when previous version is not found',
context: {
input: {
appVersion: '1.0.0',
appVersion: UPGRADE_COMMAND_SUPPORTED_VERSIONS[0],
},
},
},
{
title: 'when all commands contains invalid semver keys',
context: {
input: {
commandRunner: InvalidUpgradeCommandRunner,
output: {
failReportWorkspaceId: 'global',
expectedErrorMessage: `No previous version found for version ${UPGRADE_COMMAND_SUPPORTED_VERSIONS[0]}. Available versions: ${UPGRADE_COMMAND_SUPPORTED_VERSIONS.join(', ')}`,
},
},
},
@@ -403,7 +422,7 @@ describe('UpgradeCommandRunner', () => {
const { workspaceId, error } = failReport[0];
expect(workspaceId).toBe(output?.failReportWorkspaceId ?? 'global');
expect(error).toMatchSnapshot();
expect(error).toEqual(new Error(output?.expectedErrorMessage ?? ''));
},
);
});
@@ -1,13 +1,9 @@
import { InjectRepository } from '@nestjs/typeorm';
import { exec } from 'child_process';
import { promisify } from 'util';
import chalk from 'chalk';
import { SemVer } from 'semver';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
@@ -17,22 +13,24 @@ import {
RunOnWorkspaceArgs,
WorkspacesMigrationCommandRunner,
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { CoreMigrationRunnerService } from 'src/database/commands/core-migration-runner/services/core-migration-runner.service';
import { type UpgradeCommandVersion } from 'src/engine/constants/upgrade-command-supported-versions.constant';
import { CoreEngineVersionService } from 'src/engine/core-engine-version/services/core-engine-version.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type 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';
import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service';
import {
type CompareVersionMajorAndMinorReturnType,
compareVersionMajorAndMinor,
} from 'src/utils/version/compare-version-minor-and-major';
import { getPreviousVersion } from 'src/utils/version/get-previous-version';
export type VersionCommands = (
| WorkspacesMigrationCommandRunner
| ActiveOrSuspendedWorkspacesMigrationCommandRunner
)[];
export type AllCommands = Record<string, VersionCommands>;
const execPromise = promisify(exec);
export type AllCommands = Record<UpgradeCommandVersion, VersionCommands>;
export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
private fromWorkspaceVersion: SemVer;
@@ -47,95 +45,13 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
protected readonly twentyConfigService: TwentyConfigService,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
protected readonly coreEngineVersionService: CoreEngineVersionService,
protected readonly workspaceVersionService: WorkspaceVersionService,
protected readonly coreMigrationRunnerService: CoreMigrationRunnerService,
) {
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
private async loadActiveOrSuspendedWorkspace() {
return await this.workspaceRepository.find({
select: ['id', 'version', 'displayName'],
where: {
activationStatus: In([
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
]),
},
order: {
id: 'ASC',
},
});
}
private async shouldSkipUpgradeIfFreshInstallation(): Promise<boolean> {
const activeWorkspaceOrSuspendedWorkspaceCount =
await this.loadActiveOrSuspendedWorkspace();
return activeWorkspaceOrSuspendedWorkspaceCount.length === 0;
}
async runCoreMigrations(): Promise<void> {
this.logger.log('Running global database migrations');
try {
this.logger.log('Running core datasource migrations...');
const coreResult = await execPromise(
'npx -y typeorm migration:run -d dist/database/typeorm/core/core.datasource',
);
this.logger.log(coreResult.stdout);
this.logger.log('Database migrations completed successfully');
} catch (error) {
this.logger.error('Error running database migrations:', error);
throw error;
}
}
private async workspacesThatAreBelowFromWorkspaceVersion(
fromWorkspaceVersion: SemVer,
): Promise<Pick<WorkspaceEntity, 'id' | 'displayName' | 'version'>[]> {
try {
const allActiveOrSuspendedWorkspaces =
await this.loadActiveOrSuspendedWorkspace();
if (allActiveOrSuspendedWorkspaces.length === 0) {
this.logger.log(
'No workspaces found. Running migrations for fresh installation.',
);
return [];
}
const workspacesThatAreBelowFromWorkspaceVersion =
allActiveOrSuspendedWorkspaces.filter((workspace) => {
if (!isDefined(workspace.version)) {
return true;
}
try {
const versionCompareResult = compareVersionMajorAndMinor(
workspace.version,
fromWorkspaceVersion.version,
);
return versionCompareResult === 'lower';
} catch (error) {
this.logger.error(
`Error checking workspace ${workspace.id} version: ${error.message}`,
);
return true;
}
});
return workspacesThatAreBelowFromWorkspaceVersion;
} catch (error) {
this.logger.error('Error checking workspaces below version:', error);
throw error;
}
}
private setUpgradeContextVersionsAndCommandsForCurrentAppVersion() {
const upgradeContextIsAlreadyDefined = [
this.currentAppVersion,
@@ -147,8 +63,9 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
return;
}
const currentAppVersion = this.retrieveCurrentAppVersion();
const currentVersionMajorMinor = `${currentAppVersion.major}.${currentAppVersion.minor}.0`;
const currentAppVersion = this.coreEngineVersionService.getCurrentVersion();
const currentVersionMajorMinor =
`${currentAppVersion.major}.${currentAppVersion.minor}.0` as UpgradeCommandVersion;
const currentCommands = this.allCommands[currentVersionMajorMinor];
if (!isDefined(currentCommands)) {
@@ -157,17 +74,8 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
);
}
const allCommandsVersions = Object.keys(this.allCommands);
const previousVersion = getPreviousVersion({
currentVersion: currentVersionMajorMinor,
versions: allCommandsVersions,
});
const previousVersion = this.coreEngineVersionService.getPreviousVersion();
if (!isDefined(previousVersion)) {
throw new Error(
`No previous version found for version ${currentAppVersion}. Please review the "allCommands" record. Available versions are: ${allCommandsVersions.join(', ')}`,
);
}
this.commands = currentCommands;
this.fromWorkspaceVersion = previousVersion;
this.currentAppVersion = currentAppVersion;
@@ -189,10 +97,12 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
try {
this.setUpgradeContextVersionsAndCommandsForCurrentAppVersion();
const shouldSkipUpgradeIfFreshInstallation =
await this.shouldSkipUpgradeIfFreshInstallation();
// On fresh installs there are no workspaces yet, so skip the
// per-workspace upgrade loop (core migrations already ran above).
const hasWorkspaces =
await this.workspaceVersionService.hasActiveOrSuspendedWorkspaces();
if (shouldSkipUpgradeIfFreshInstallation) {
if (!hasWorkspaces) {
this.logger.log(
chalk.blue('Fresh installation detected, skipping migration'),
);
@@ -201,8 +111,8 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi
}
const workspacesThatAreBelowFromWorkspaceVersion =
await this.workspacesThatAreBelowFromWorkspaceVersion(
this.fromWorkspaceVersion,
await this.workspaceVersionService.getWorkspacesBelowVersion(
this.fromWorkspaceVersion.version,
);
if (workspacesThatAreBelowFromWorkspaceVersion.length > 0) {
@@ -234,7 +144,7 @@ If any workspaces are not on the previous minor version, roll back to that versi
return;
}
await this.runCoreMigrations();
await this.coreMigrationRunnerService.run();
await super.runMigrationCommand(passedParams, options);
}
@@ -295,24 +205,6 @@ If any workspaces are not on the previous minor version, roll back to that versi
}
}
private retrieveCurrentAppVersion() {
const appVersion = this.twentyConfigService.get('APP_VERSION');
if (!isDefined(appVersion)) {
throw new Error(
'Cannot run upgrade command when APP_VERSION is not defined, please double check your env variables',
);
}
try {
return new SemVer(appVersion);
} catch {
throw new Error(
`Should never occur, APP_VERSION is invalid ${appVersion}`,
);
}
}
private async retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion(
workspaceId: string,
): Promise<CompareVersionMajorAndMinorReturnType> {
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { CoreMigrationRunnerService } from 'src/database/commands/core-migration-runner/services/core-migration-runner.service';
@Module({
providers: [CoreMigrationRunnerService],
exports: [CoreMigrationRunnerService],
})
export class CoreMigrationRunnerModule {}

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