Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4f5e4b55e | ||
|
|
fc546f90ff | ||
|
|
79d9e75b6e | ||
|
|
2c90edaa59 | ||
|
|
480b78c55f | ||
|
|
fbd6c9daea | ||
|
|
c2b058a6a7 | ||
|
|
22c9693ce5 | ||
|
|
50ea560e57 | ||
|
|
56056b885d | ||
|
|
db9da3194f | ||
|
|
68f5e70ade | ||
|
|
08077476f3 | ||
|
|
6f0ac88e20 | ||
|
|
17424320e3 | ||
|
|
6360fb3bce | ||
|
|
da1b1f1cbc | ||
|
|
31718d163c | ||
|
|
f47608de07 | ||
|
|
695518a15e |
@@ -52,3 +52,4 @@ mcp.json
|
||||
/.junie/
|
||||
TRANSLATION_QA_REPORT.md
|
||||
.playwright-mcp/
|
||||
.claude/
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
# Handoff: AI Chat Streaming — Alternating Messages Bug
|
||||
|
||||
## Branch
|
||||
`fix/ai-chat-thread-switching`
|
||||
|
||||
## Problem
|
||||
|
||||
During streaming, the UI flickers: the last assistant message alternates between two versions with different `parts` arrays. This continues until streaming finishes, at which point the message stabilizes.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
The bug comes from having **two independent sources writing messages to the Chat instance**, creating a tug-of-war during streaming.
|
||||
|
||||
### Source 1: AI SDK streaming (internal)
|
||||
|
||||
`useChat` subscribes to the Chat instance's messages via `useSyncExternalStore`. During streaming, the SDK calls `Chat.replaceMessage()` on every chunk, which:
|
||||
1. Calls `structuredClone(message)` on the updated last message
|
||||
2. Replaces it in the internal `#messages` array
|
||||
3. Fires all registered callbacks (throttled at 100ms via `experimental_throttle`)
|
||||
|
||||
### Source 2: GraphQL fetch (external mutation)
|
||||
|
||||
`AgentChatMessagesFetchEffect.handleDataLoaded` (line 76) directly mutates the Chat instance:
|
||||
```ts
|
||||
chatInstance.messages = uiMessages;
|
||||
```
|
||||
This triggers the Chat's `set messages()` setter, which creates `[...newMessages]` and fires ALL registered callbacks — including the throttled one that `useSyncExternalStore` uses.
|
||||
|
||||
### The guard that doesn't fully work
|
||||
|
||||
There IS a guard in `handleDataLoaded` (lines 68-74):
|
||||
```ts
|
||||
const isStreaming = chatInstance.status === 'streaming' || chatInstance.status === 'submitted';
|
||||
if (isStreaming) { return; }
|
||||
```
|
||||
|
||||
But this guard only protects against the **initial fetch** and **explicit refetch** during streaming. The problem occurs because:
|
||||
|
||||
1. `onStreamingComplete` dispatches `AGENT_CHAT_REFETCH_MESSAGES_EVENT_NAME`
|
||||
2. This triggers `refetchAgentChatMessages()` — a GraphQL network call
|
||||
3. The response arrives asynchronously, potentially AFTER the SDK has already updated internal state but BEFORE React has committed the final render
|
||||
4. More critically: Apollo's `useQuery` can update its cache and trigger `onDataLoaded` from cache operations (optimistic updates, cache writes from other queries) even during streaming
|
||||
|
||||
### The alternation mechanism
|
||||
|
||||
When both sources write to the Chat instance in rapid succession:
|
||||
1. SDK streaming updates `#messages` with the latest chunk → snapshot A (has new parts)
|
||||
2. Apollo cache/refetch triggers `handleDataLoaded` → `chatInstance.messages = uiMessages` → snapshot B (DB version, missing latest streamed parts)
|
||||
3. `useSyncExternalStore` detects the snapshot changed → re-render with B
|
||||
4. Next throttle tick, SDK has already updated internally → snapshot A again
|
||||
5. React renders with A, then B arrives again → alternation
|
||||
|
||||
The user sees the last message's `parts` flickering between the streaming version (more content) and the DB version (less content).
|
||||
|
||||
## Architecture Context
|
||||
|
||||
### How useChat works internally (`@ai-sdk/react`)
|
||||
|
||||
```
|
||||
useChat({ chat: instance, experimental_throttle: 100 })
|
||||
└── chatRef = useRef(instance)
|
||||
└── useSyncExternalStore(
|
||||
subscribe: throttle(onChange, 100ms), // only fires every 100ms
|
||||
getSnapshot: () => chatRef.current.messages // but reads LATEST on every render
|
||||
)
|
||||
```
|
||||
|
||||
Key detail: `getSnapshot` always reads the latest `chatRef.current.messages`. React can call this during ANY render — not just when the subscribe callback fires. If something else triggers a render (Jotai state update, parent re-render), React will read the current snapshot and may detect it differs from the last committed value, causing an additional re-render.
|
||||
|
||||
### Data flow (current)
|
||||
|
||||
```
|
||||
AI SDK streaming ──replaceMessage()──► Chat#messages ──useSyncExternalStore──► useChat.messages
|
||||
▲ │
|
||||
GraphQL fetch ──handleDataLoaded──► chatInstance.messages = uiMessages │
|
||||
▼
|
||||
setAgentChatMessages (Jotai)
|
||||
│
|
||||
agentChatMessagesComponentFamilyState
|
||||
│
|
||||
UI renders
|
||||
```
|
||||
|
||||
The problem is the two arrows writing to `Chat#messages`.
|
||||
|
||||
### Key files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `useAgentChat.ts` | Calls `useChat({ chat: agentChatInstanceForThread })`, returns messages/status |
|
||||
| `useAgentChatInstanceForThread.ts` | Creates/caches Chat instances per thread in Jotai atom family |
|
||||
| `AgentChatAiSdkStreamEffect.tsx` | Bridges useChat → Jotai: `setAgentChatMessages(chatState.messages)` |
|
||||
| `AgentChatMessagesFetchEffect.tsx` | Fetches historical messages from GraphQL, writes to Chat instance |
|
||||
| `agentChatInstanceByThreadIdFamilyState.ts` | Jotai atom family holding Chat instances keyed by `{ threadId }` |
|
||||
|
||||
### AI SDK internals (relevant)
|
||||
|
||||
| Method | What it does |
|
||||
|---|---|
|
||||
| `Chat.replaceMessage(index, message)` | `structuredClone`s the message, replaces at index, fires callbacks |
|
||||
| `Chat.messages` setter | Creates `[...newMessages]`, fires callbacks |
|
||||
| `Chat.messages` getter | Returns internal `#messages` reference |
|
||||
| `~registerMessagesCallback(onChange, throttleMs)` | Wraps onChange with `throttleit` if throttleMs provided |
|
||||
|
||||
## Logging
|
||||
|
||||
There are `console.log` statements in `AgentChatAiSdkStreamEffect.tsx` and a `lastMessageMap` (module-level Map) that captures every message snapshot with a timestamp. These were added for debugging this exact issue and will be kept as-is in the PR.
|
||||
|
||||
## Possible Fix Directions
|
||||
|
||||
1. **Make Chat instance the single source of truth during streaming**: Remove the `chatInstance.messages = uiMessages` path entirely. Instead, only seed messages at Chat construction time (before streaming starts). After streaming ends, refetch and set.
|
||||
|
||||
2. **Use setMessages from useChat instead of direct mutation**: The AI SDK's `useChat` returns a `setMessages` function that properly coordinates with the internal state. Replace `chatInstance.messages = uiMessages` with the SDK's own `setMessages`. This ensures the SDK controls the timing.
|
||||
|
||||
3. **Strengthen the guard**: Instead of checking `chatInstance.status`, use a dedicated atom that tracks whether the SDK is actively streaming for this thread. Set it before the stream starts, clear it after the effect processes the final message.
|
||||
|
||||
4. **Debounce/skip the fetch effect during streaming**: Don't refetch at all while streaming. Only fetch historical messages when switching TO a thread that isn't streaming.
|
||||
|
||||
## What's Working
|
||||
|
||||
- Thread creation (draft → first send → thread created)
|
||||
- Message sending and receiving (when not observing the alternation)
|
||||
- Thread switching (historical messages load correctly for non-streaming threads)
|
||||
- Backend resumable streams (Redis infrastructure, `GET /agent-chat/:threadId/stream`)
|
||||
- `onFinish` callback (title update, usage tracking)
|
||||
@@ -203,6 +203,7 @@
|
||||
"packages/twenty-utils",
|
||||
"packages/twenty-zapier",
|
||||
"packages/twenty-website",
|
||||
"packages/twenty-website-new",
|
||||
"packages/twenty-docs",
|
||||
"packages/twenty-e2e-testing",
|
||||
"packages/twenty-shared",
|
||||
|
||||
@@ -2273,6 +2273,11 @@ type FieldConnection {
|
||||
edges: [FieldEdge!]!
|
||||
}
|
||||
|
||||
type LogicFunctionLogs {
|
||||
"""Execution Logs"""
|
||||
logs: String!
|
||||
}
|
||||
|
||||
type DeleteTwoFactorAuthenticationMethod {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
@@ -2593,11 +2598,6 @@ type UsageAnalytics {
|
||||
userDailyUsage: UsageUserDaily
|
||||
}
|
||||
|
||||
type LogicFunctionLogs {
|
||||
"""Execution Logs"""
|
||||
logs: String!
|
||||
}
|
||||
|
||||
type FrontComponent {
|
||||
id: UUID!
|
||||
name: String!
|
||||
@@ -3501,6 +3501,7 @@ type Mutation {
|
||||
createViewGroup(input: CreateViewGroupInput!): ViewGroup!
|
||||
createManyViewGroups(inputs: [CreateViewGroupInput!]!): [ViewGroup!]!
|
||||
updateViewGroup(input: UpdateViewGroupInput!): ViewGroup!
|
||||
updateManyViewGroups(inputs: [UpdateViewGroupInput!]!): [ViewGroup!]!
|
||||
deleteViewGroup(input: DeleteViewGroupInput!): ViewGroup!
|
||||
destroyViewGroup(input: DestroyViewGroupInput!): ViewGroup!
|
||||
updateMessageFolder(input: UpdateMessageFolderInput!): MessageFolder!
|
||||
|
||||
@@ -1937,6 +1937,12 @@ export interface FieldConnection {
|
||||
__typename: 'FieldConnection'
|
||||
}
|
||||
|
||||
export interface LogicFunctionLogs {
|
||||
/** Execution Logs */
|
||||
logs: Scalars['String']
|
||||
__typename: 'LogicFunctionLogs'
|
||||
}
|
||||
|
||||
export interface DeleteTwoFactorAuthenticationMethod {
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success: Scalars['Boolean']
|
||||
@@ -2298,12 +2304,6 @@ export interface UsageAnalytics {
|
||||
__typename: 'UsageAnalytics'
|
||||
}
|
||||
|
||||
export interface LogicFunctionLogs {
|
||||
/** Execution Logs */
|
||||
logs: Scalars['String']
|
||||
__typename: 'LogicFunctionLogs'
|
||||
}
|
||||
|
||||
export interface FrontComponent {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
@@ -2954,6 +2954,7 @@ export interface Mutation {
|
||||
createViewGroup: ViewGroup
|
||||
createManyViewGroups: ViewGroup[]
|
||||
updateViewGroup: ViewGroup
|
||||
updateManyViewGroups: ViewGroup[]
|
||||
deleteViewGroup: ViewGroup
|
||||
destroyViewGroup: ViewGroup
|
||||
updateMessageFolder: MessageFolder
|
||||
@@ -5096,6 +5097,13 @@ export interface FieldConnectionGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface LogicFunctionLogsGenqlSelection{
|
||||
/** Execution Logs */
|
||||
logs?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface DeleteTwoFactorAuthenticationMethodGenqlSelection{
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success?: boolean | number
|
||||
@@ -5501,13 +5509,6 @@ export interface UsageAnalyticsGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface LogicFunctionLogsGenqlSelection{
|
||||
/** Execution Logs */
|
||||
logs?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface FrontComponentGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
@@ -6207,6 +6208,7 @@ export interface MutationGenqlSelection{
|
||||
createViewGroup?: (ViewGroupGenqlSelection & { __args: {input: CreateViewGroupInput} })
|
||||
createManyViewGroups?: (ViewGroupGenqlSelection & { __args: {inputs: CreateViewGroupInput[]} })
|
||||
updateViewGroup?: (ViewGroupGenqlSelection & { __args: {input: UpdateViewGroupInput} })
|
||||
updateManyViewGroups?: (ViewGroupGenqlSelection & { __args: {inputs: UpdateViewGroupInput[]} })
|
||||
deleteViewGroup?: (ViewGroupGenqlSelection & { __args: {input: DeleteViewGroupInput} })
|
||||
destroyViewGroup?: (ViewGroupGenqlSelection & { __args: {input: DestroyViewGroupInput} })
|
||||
updateMessageFolder?: (MessageFolderGenqlSelection & { __args: {input: UpdateMessageFolderInput} })
|
||||
@@ -8047,6 +8049,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const LogicFunctionLogs_possibleTypes: string[] = ['LogicFunctionLogs']
|
||||
export const isLogicFunctionLogs = (obj?: { __typename?: any } | null): obj is LogicFunctionLogs => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunctionLogs"')
|
||||
return LogicFunctionLogs_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const DeleteTwoFactorAuthenticationMethod_possibleTypes: string[] = ['DeleteTwoFactorAuthenticationMethod']
|
||||
export const isDeleteTwoFactorAuthenticationMethod = (obj?: { __typename?: any } | null): obj is DeleteTwoFactorAuthenticationMethod => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isDeleteTwoFactorAuthenticationMethod"')
|
||||
@@ -8431,14 +8441,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const LogicFunctionLogs_possibleTypes: string[] = ['LogicFunctionLogs']
|
||||
export const isLogicFunctionLogs = (obj?: { __typename?: any } | null): obj is LogicFunctionLogs => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunctionLogs"')
|
||||
return LogicFunctionLogs_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FrontComponent_possibleTypes: string[] = ['FrontComponent']
|
||||
export const isFrontComponent = (obj?: { __typename?: any } | null): obj is FrontComponent => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isFrontComponent"')
|
||||
|
||||
@@ -63,8 +63,8 @@ export default {
|
||||
210,
|
||||
227,
|
||||
244,
|
||||
282,
|
||||
283,
|
||||
284,
|
||||
299,
|
||||
300,
|
||||
325,
|
||||
@@ -4458,6 +4458,14 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"LogicFunctionLogs": {
|
||||
"logs": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"DeleteTwoFactorAuthenticationMethod": {
|
||||
"success": [
|
||||
6
|
||||
@@ -4503,10 +4511,10 @@ export default {
|
||||
},
|
||||
"AuthTokenPair": {
|
||||
"accessOrWorkspaceAgnosticToken": [
|
||||
250
|
||||
251
|
||||
],
|
||||
"refreshToken": [
|
||||
250
|
||||
251
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4514,7 +4522,7 @@ export default {
|
||||
},
|
||||
"AvailableWorkspacesAndAccessTokens": {
|
||||
"tokens": [
|
||||
251
|
||||
252
|
||||
],
|
||||
"availableWorkspaces": [
|
||||
224
|
||||
@@ -4566,10 +4574,10 @@ export default {
|
||||
},
|
||||
"SignUp": {
|
||||
"loginToken": [
|
||||
250
|
||||
251
|
||||
],
|
||||
"workspace": [
|
||||
256
|
||||
257
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4577,7 +4585,7 @@ export default {
|
||||
},
|
||||
"TransientToken": {
|
||||
"transientToken": [
|
||||
250
|
||||
251
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4599,7 +4607,7 @@ export default {
|
||||
},
|
||||
"VerifyEmailAndGetLoginToken": {
|
||||
"loginToken": [
|
||||
250
|
||||
251
|
||||
],
|
||||
"workspaceUrls": [
|
||||
157
|
||||
@@ -4618,7 +4626,7 @@ export default {
|
||||
},
|
||||
"AuthTokens": {
|
||||
"tokens": [
|
||||
251
|
||||
252
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4626,7 +4634,7 @@ export default {
|
||||
},
|
||||
"LoginToken": {
|
||||
"loginToken": [
|
||||
250
|
||||
251
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4656,10 +4664,10 @@ export default {
|
||||
},
|
||||
"Impersonate": {
|
||||
"loginToken": [
|
||||
250
|
||||
251
|
||||
],
|
||||
"workspace": [
|
||||
256
|
||||
257
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4689,10 +4697,10 @@ export default {
|
||||
},
|
||||
"ApplicationTokenPair": {
|
||||
"applicationAccessToken": [
|
||||
250
|
||||
251
|
||||
],
|
||||
"applicationRefreshToken": [
|
||||
250
|
||||
251
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4764,7 +4772,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"fields": [
|
||||
271
|
||||
272
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -4861,10 +4869,10 @@ export default {
|
||||
6
|
||||
],
|
||||
"objectPermissions": [
|
||||
275
|
||||
276
|
||||
],
|
||||
"fieldPermissions": [
|
||||
276
|
||||
277
|
||||
],
|
||||
"permissionFlags": [
|
||||
1
|
||||
@@ -4914,19 +4922,19 @@ export default {
|
||||
1
|
||||
],
|
||||
"objects": [
|
||||
272
|
||||
],
|
||||
"fields": [
|
||||
271
|
||||
],
|
||||
"logicFunctions": [
|
||||
273
|
||||
],
|
||||
"frontComponents": [
|
||||
"fields": [
|
||||
272
|
||||
],
|
||||
"logicFunctions": [
|
||||
274
|
||||
],
|
||||
"frontComponents": [
|
||||
275
|
||||
],
|
||||
"defaultRole": [
|
||||
277
|
||||
278
|
||||
],
|
||||
"sourcePackage": [
|
||||
1
|
||||
@@ -4986,13 +4994,13 @@ export default {
|
||||
1
|
||||
],
|
||||
"driver": [
|
||||
282
|
||||
],
|
||||
"status": [
|
||||
283
|
||||
],
|
||||
"status": [
|
||||
284
|
||||
],
|
||||
"verificationRecords": [
|
||||
280
|
||||
281
|
||||
],
|
||||
"verifiedAt": [
|
||||
4
|
||||
@@ -5039,7 +5047,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"location": [
|
||||
285
|
||||
286
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -5067,13 +5075,13 @@ export default {
|
||||
},
|
||||
"ImapSmtpCaldavConnectionParameters": {
|
||||
"IMAP": [
|
||||
287
|
||||
288
|
||||
],
|
||||
"SMTP": [
|
||||
287
|
||||
288
|
||||
],
|
||||
"CALDAV": [
|
||||
287
|
||||
288
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -5093,7 +5101,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"connectionParameters": [
|
||||
288
|
||||
289
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -5157,7 +5165,7 @@ export default {
|
||||
1
|
||||
],
|
||||
"dailyUsage": [
|
||||
293
|
||||
294
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -5165,13 +5173,13 @@ export default {
|
||||
},
|
||||
"UsageAnalytics": {
|
||||
"usageByUser": [
|
||||
292
|
||||
293
|
||||
],
|
||||
"usageByOperationType": [
|
||||
292
|
||||
293
|
||||
],
|
||||
"timeSeries": [
|
||||
293
|
||||
294
|
||||
],
|
||||
"periodStart": [
|
||||
4
|
||||
@@ -5180,15 +5188,7 @@ export default {
|
||||
4
|
||||
],
|
||||
"userDailyUsage": [
|
||||
294
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"LogicFunctionLogs": {
|
||||
"logs": [
|
||||
1
|
||||
295
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -5235,7 +5235,7 @@ export default {
|
||||
6
|
||||
],
|
||||
"applicationTokenPair": [
|
||||
269
|
||||
270
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -6656,7 +6656,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"checkUserExists": [
|
||||
264,
|
||||
265,
|
||||
{
|
||||
"email": [
|
||||
1,
|
||||
@@ -6668,7 +6668,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"checkWorkspaceInviteHashIsValid": [
|
||||
265,
|
||||
266,
|
||||
{
|
||||
"inviteHash": [
|
||||
1,
|
||||
@@ -6686,7 +6686,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"validatePasswordResetToken": [
|
||||
259,
|
||||
260,
|
||||
{
|
||||
"passwordResetToken": [
|
||||
1,
|
||||
@@ -6769,7 +6769,7 @@ export default {
|
||||
220
|
||||
],
|
||||
"getConnectedImapSmtpCaldavAccount": [
|
||||
289,
|
||||
290,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
@@ -6778,7 +6778,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"getAutoCompleteAddress": [
|
||||
284,
|
||||
285,
|
||||
{
|
||||
"address": [
|
||||
1,
|
||||
@@ -6797,7 +6797,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"getAddressDetails": [
|
||||
286,
|
||||
287,
|
||||
{
|
||||
"placeId": [
|
||||
1,
|
||||
@@ -6889,19 +6889,19 @@ export default {
|
||||
}
|
||||
],
|
||||
"getPostgresCredentials": [
|
||||
291
|
||||
292
|
||||
],
|
||||
"findManyPublicDomains": [
|
||||
279
|
||||
280
|
||||
],
|
||||
"getEmailingDomains": [
|
||||
281
|
||||
282
|
||||
],
|
||||
"findManyMarketplaceApps": [
|
||||
278
|
||||
279
|
||||
],
|
||||
"findOneMarketplaceApp": [
|
||||
278,
|
||||
279,
|
||||
{
|
||||
"universalIdentifier": [
|
||||
1,
|
||||
@@ -6924,7 +6924,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"getUsageAnalytics": [
|
||||
295,
|
||||
296,
|
||||
{
|
||||
"input": [
|
||||
365
|
||||
@@ -8130,6 +8130,15 @@ export default {
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateManyViewGroups": [
|
||||
56,
|
||||
{
|
||||
"inputs": [
|
||||
450,
|
||||
"[UpdateViewGroupInput!]!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"deleteViewGroup": [
|
||||
56,
|
||||
{
|
||||
@@ -8300,7 +8309,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"getAuthorizationUrlForSSO": [
|
||||
254,
|
||||
255,
|
||||
{
|
||||
"input": [
|
||||
466,
|
||||
@@ -8309,7 +8318,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"getLoginTokenFromCredentials": [
|
||||
263,
|
||||
264,
|
||||
{
|
||||
"email": [
|
||||
1,
|
||||
@@ -8335,7 +8344,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"signIn": [
|
||||
252,
|
||||
253,
|
||||
{
|
||||
"email": [
|
||||
1,
|
||||
@@ -8357,7 +8366,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"verifyEmailAndGetLoginToken": [
|
||||
260,
|
||||
261,
|
||||
{
|
||||
"emailVerificationToken": [
|
||||
1,
|
||||
@@ -8377,7 +8386,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"verifyEmailAndGetWorkspaceAgnosticToken": [
|
||||
252,
|
||||
253,
|
||||
{
|
||||
"emailVerificationToken": [
|
||||
1,
|
||||
@@ -8393,7 +8402,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"getAuthTokensFromOTP": [
|
||||
262,
|
||||
263,
|
||||
{
|
||||
"otp": [
|
||||
1,
|
||||
@@ -8413,7 +8422,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"signUp": [
|
||||
252,
|
||||
253,
|
||||
{
|
||||
"email": [
|
||||
1,
|
||||
@@ -8435,7 +8444,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"signUpInWorkspace": [
|
||||
257,
|
||||
258,
|
||||
{
|
||||
"email": [
|
||||
1,
|
||||
@@ -8466,13 +8475,13 @@ export default {
|
||||
}
|
||||
],
|
||||
"signUpInNewWorkspace": [
|
||||
257
|
||||
],
|
||||
"generateTransientToken": [
|
||||
258
|
||||
],
|
||||
"generateTransientToken": [
|
||||
259
|
||||
],
|
||||
"getAuthTokensFromLoginToken": [
|
||||
262,
|
||||
263,
|
||||
{
|
||||
"loginToken": [
|
||||
1,
|
||||
@@ -8485,7 +8494,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"authorizeApp": [
|
||||
249,
|
||||
250,
|
||||
{
|
||||
"clientId": [
|
||||
1,
|
||||
@@ -8507,7 +8516,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"renewToken": [
|
||||
262,
|
||||
263,
|
||||
{
|
||||
"appToken": [
|
||||
1,
|
||||
@@ -8516,7 +8525,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"generateApiKeyToken": [
|
||||
261,
|
||||
262,
|
||||
{
|
||||
"apiKeyId": [
|
||||
3,
|
||||
@@ -8529,7 +8538,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"emailPasswordResetLink": [
|
||||
253,
|
||||
254,
|
||||
{
|
||||
"email": [
|
||||
1,
|
||||
@@ -8541,7 +8550,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"updatePasswordViaResetToken": [
|
||||
255,
|
||||
256,
|
||||
{
|
||||
"passwordResetToken": [
|
||||
1,
|
||||
@@ -8642,7 +8651,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"initiateOTPProvisioning": [
|
||||
247,
|
||||
248,
|
||||
{
|
||||
"loginToken": [
|
||||
1,
|
||||
@@ -8655,10 +8664,10 @@ export default {
|
||||
}
|
||||
],
|
||||
"initiateOTPProvisioningForAuthenticatedUser": [
|
||||
247
|
||||
248
|
||||
],
|
||||
"deleteTwoFactorAuthenticationMethod": [
|
||||
246,
|
||||
247,
|
||||
{
|
||||
"twoFactorAuthenticationMethodId": [
|
||||
3,
|
||||
@@ -8667,7 +8676,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"verifyTwoFactorAuthenticationMethodForAuthenticatedUser": [
|
||||
248,
|
||||
249,
|
||||
{
|
||||
"otp": [
|
||||
1,
|
||||
@@ -8773,7 +8782,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"impersonate": [
|
||||
266,
|
||||
267,
|
||||
{
|
||||
"userId": [
|
||||
3,
|
||||
@@ -8795,7 +8804,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"saveImapSmtpCaldavAccount": [
|
||||
290,
|
||||
291,
|
||||
{
|
||||
"accountOwnerId": [
|
||||
3,
|
||||
@@ -8998,13 +9007,13 @@ export default {
|
||||
}
|
||||
],
|
||||
"enablePostgresProxy": [
|
||||
291
|
||||
292
|
||||
],
|
||||
"disablePostgresProxy": [
|
||||
291
|
||||
292
|
||||
],
|
||||
"createPublicDomain": [
|
||||
279,
|
||||
280,
|
||||
{
|
||||
"domain": [
|
||||
1,
|
||||
@@ -9031,14 +9040,14 @@ export default {
|
||||
}
|
||||
],
|
||||
"createEmailingDomain": [
|
||||
281,
|
||||
282,
|
||||
{
|
||||
"domain": [
|
||||
1,
|
||||
"String!"
|
||||
],
|
||||
"driver": [
|
||||
282,
|
||||
283,
|
||||
"EmailingDomainDriver!"
|
||||
]
|
||||
}
|
||||
@@ -9053,7 +9062,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"verifyEmailingDomain": [
|
||||
281,
|
||||
282,
|
||||
{
|
||||
"id": [
|
||||
1,
|
||||
@@ -9130,7 +9139,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"createDevelopmentApplication": [
|
||||
267,
|
||||
268,
|
||||
{
|
||||
"universalIdentifier": [
|
||||
1,
|
||||
@@ -9143,7 +9152,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"generateApplicationToken": [
|
||||
269,
|
||||
270,
|
||||
{
|
||||
"applicationId": [
|
||||
3,
|
||||
@@ -9152,7 +9161,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"syncApplication": [
|
||||
268,
|
||||
269,
|
||||
{
|
||||
"manifest": [
|
||||
15,
|
||||
@@ -9161,7 +9170,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"uploadApplicationFile": [
|
||||
270,
|
||||
271,
|
||||
{
|
||||
"file": [
|
||||
372,
|
||||
@@ -9195,7 +9204,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"renewApplicationToken": [
|
||||
269,
|
||||
270,
|
||||
{
|
||||
"applicationRefreshToken": [
|
||||
1,
|
||||
@@ -11455,7 +11464,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"logicFunctionLogs": [
|
||||
296,
|
||||
246,
|
||||
{
|
||||
"input": [
|
||||
490,
|
||||
|
||||
File diff suppressed because one or more lines are too long
+16
@@ -2,10 +2,15 @@ import { AIChatMessage } from '@/ai/components/AIChatMessage';
|
||||
import { agentChatErrorState } from '@/ai/states/agentChatErrorState';
|
||||
import { agentChatIsStreamingState } from '@/ai/states/agentChatIsStreamingState';
|
||||
import { agentChatLastMessageIdComponentSelector } from '@/ai/states/agentChatLastMessageIdComponentSelector';
|
||||
import { agentChatMessageComponentFamilySelector } from '@/ai/states/agentChatMessageComponentFamilySelector';
|
||||
import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const messageMap = new Map<string, ExtendedUIMessage>();
|
||||
|
||||
export const AIChatLastMessageWithStreamingState = () => {
|
||||
const lastMessageId = useAtomComponentSelectorValue(
|
||||
agentChatLastMessageIdComponentSelector,
|
||||
@@ -14,6 +19,17 @@ export const AIChatLastMessageWithStreamingState = () => {
|
||||
const agentChatIsStreaming = useAtomStateValue(agentChatIsStreamingState);
|
||||
const agentChatError = useAtomStateValue(agentChatErrorState);
|
||||
|
||||
const agentChatMessage = useAtomComponentFamilySelectorValue(
|
||||
agentChatMessageComponentFamilySelector,
|
||||
{ messageId: lastMessageId },
|
||||
);
|
||||
|
||||
messageMap.set(new Date().toISOString(), agentChatMessage!);
|
||||
|
||||
console.log({
|
||||
messageMap,
|
||||
});
|
||||
|
||||
if (!isDefined(lastMessageId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
import { AGENT_CHAT_ENSURE_THREAD_FOR_DRAFT_EVENT_NAME } from '@/ai/constants/AgentChatEnsureThreadForDraftEventName';
|
||||
import { AGENT_CHAT_REFETCH_MESSAGES_EVENT_NAME } from '@/ai/constants/AgentChatRefetchMessagesEventName';
|
||||
import { useAgentChat } from '@/ai/hooks/useAgentChat';
|
||||
import { useCreateAgentChatThread } from '@/ai/hooks/useCreateAgentChatThread';
|
||||
import { useEnsureAgentChatThreadExistsForDraft } from '@/ai/hooks/useEnsureAgentChatThreadExistsForDraft';
|
||||
import { useEnsureAgentChatThreadIdForSend } from '@/ai/hooks/useEnsureAgentChatThreadIdForSend';
|
||||
import { agentChatDisplayedThreadState } from '@/ai/states/agentChatDisplayedThreadState';
|
||||
import { agentChatErrorState } from '@/ai/states/agentChatErrorState';
|
||||
import { agentChatIsInitialScrollPendingOnThreadChangeState } from '@/ai/states/agentChatIsInitialScrollPendingOnThreadChangeState';
|
||||
import { agentChatIsLoadingState } from '@/ai/states/agentChatIsLoadingState';
|
||||
import { agentChatIsStreamingState } from '@/ai/states/agentChatIsStreamingState';
|
||||
import { normalizeAiSdkError } from '@/ai/utils/normalizeAiSdkError';
|
||||
import { agentChatMessagesComponentFamilyState } from '@/ai/states/agentChatMessagesComponentFamilyState';
|
||||
import { agentChatMessagesLoadingState } from '@/ai/states/agentChatMessagesLoadingState';
|
||||
import { agentChatThreadsLoadingState } from '@/ai/states/agentChatThreadsLoadingState';
|
||||
import { agentChatDisplayedThreadState } from '@/ai/states/agentChatDisplayedThreadState';
|
||||
import { agentChatFetchedMessagesComponentFamilyState } from '@/ai/states/agentChatFetchedMessagesComponentFamilyState';
|
||||
import { agentChatIsInitialScrollPendingOnThreadChangeState } from '@/ai/states/agentChatIsInitialScrollPendingOnThreadChangeState';
|
||||
import { mergeAgentChatFetchedAndStreamingMessages } from '@/ai/utils/mergeAgentChatFetchedAndStreamingMessages';
|
||||
import { AGENT_CHAT_REFETCH_MESSAGES_EVENT_NAME } from '@/ai/constants/AgentChatRefetchMessagesEventName';
|
||||
import { dispatchBrowserEvent } from '@/browser-event/utils/dispatchBrowserEvent';
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { normalizeAiSdkError } from '@/ai/utils/normalizeAiSdkError';
|
||||
import { useListenToBrowserEvent } from '@/browser-event/hooks/useListenToBrowserEvent';
|
||||
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
|
||||
import { dispatchBrowserEvent } from '@/browser-event/utils/dispatchBrowserEvent';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentFamilyState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
|
||||
const lastMessageMap = new Map<string, ExtendedUIMessage>();
|
||||
|
||||
export const AgentChatAiSdkStreamEffect = () => {
|
||||
const currentAIChatThread = useAtomStateValue(currentAIChatThreadState);
|
||||
const agentChatFetchedMessages = useAtomComponentFamilyStateValue(
|
||||
agentChatFetchedMessagesComponentFamilyState,
|
||||
{ threadId: currentAIChatThread },
|
||||
);
|
||||
|
||||
const { createChatThread } = useCreateAgentChatThread();
|
||||
|
||||
@@ -48,11 +44,7 @@ export const AgentChatAiSdkStreamEffect = () => {
|
||||
dispatchBrowserEvent(AGENT_CHAT_REFETCH_MESSAGES_EVENT_NAME);
|
||||
}, []);
|
||||
|
||||
const chatState = useAgentChat(
|
||||
agentChatFetchedMessages,
|
||||
ensureThreadIdForSend,
|
||||
onStreamingComplete,
|
||||
);
|
||||
const chatState = useAgentChat(ensureThreadIdForSend, onStreamingComplete);
|
||||
|
||||
const setAgentChatMessages = useSetAtomComponentFamilyState(
|
||||
agentChatMessagesComponentFamilyState,
|
||||
@@ -72,20 +64,24 @@ export const AgentChatAiSdkStreamEffect = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mergedMessages = mergeAgentChatFetchedAndStreamingMessages(
|
||||
agentChatFetchedMessages,
|
||||
chatState.messages,
|
||||
);
|
||||
setAgentChatMessages(mergedMessages);
|
||||
setAgentChatMessages(chatState.messages);
|
||||
|
||||
const lastMessage = chatState.messages.at(-1);
|
||||
lastMessageMap.set(new Date().toISOString(), lastMessage!);
|
||||
|
||||
console.log('AgentChatAiSdkStreamEffect - Updated messages', {
|
||||
messages: chatState.messages,
|
||||
lastMessage,
|
||||
lastMessageMap,
|
||||
});
|
||||
|
||||
if (currentAIChatThread !== agentChatDisplayedThread) {
|
||||
if (mergedMessages.length > 0) {
|
||||
if (chatState.messages.length > 0) {
|
||||
setAgentChatIsInitialScrollPendingOnThreadChange(true);
|
||||
}
|
||||
setAgentChatDisplayedThread(currentAIChatThread);
|
||||
}
|
||||
}, [
|
||||
agentChatFetchedMessages,
|
||||
chatState.messages,
|
||||
chatState.status,
|
||||
setAgentChatMessages,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AGENT_CHAT_REFETCH_MESSAGES_EVENT_NAME } from '@/ai/constants/AgentChatRefetchMessagesEventName';
|
||||
import { AGENT_CHAT_UNKNOWN_THREAD_ID } from '@/ai/constants/AgentChatUnknownThreadId';
|
||||
import { AGENT_CHAT_NEW_THREAD_DRAFT_KEY } from '@/ai/states/agentChatDraftsByThreadIdState';
|
||||
import { agentChatFetchedMessagesComponentFamilyState } from '@/ai/states/agentChatFetchedMessagesComponentFamilyState';
|
||||
import { agentChatInstanceByThreadIdFamilyState } from '@/ai/states/agentChatInstanceByThreadIdFamilyState';
|
||||
import { agentChatMessagesLoadingState } from '@/ai/states/agentChatMessagesLoadingState';
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { skipMessagesSkeletonUntilLoadedState } from '@/ai/states/skipMessagesSkeletonUntilLoadedState';
|
||||
@@ -12,7 +13,6 @@ import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages'
|
||||
import { useQueryWithCallbacks } from '@/apollo/hooks/useQueryWithCallbacks';
|
||||
import { useListenToBrowserEvent } from '@/browser-event/hooks/useListenToBrowserEvent';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentFamilyState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import {
|
||||
GetChatMessagesDocument,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
|
||||
export const AgentChatMessagesFetchEffect = () => {
|
||||
const currentAIChatThread = useAtomStateValue(currentAIChatThreadState);
|
||||
const store = useStore();
|
||||
|
||||
const isNewThread = useMemo(
|
||||
() =>
|
||||
@@ -37,11 +38,6 @@ export const AgentChatMessagesFetchEffect = () => {
|
||||
skipMessagesSkeletonUntilLoadedState,
|
||||
);
|
||||
|
||||
const setAgentChatFetchedMessages = useSetAtomComponentFamilyState(
|
||||
agentChatFetchedMessagesComponentFamilyState,
|
||||
{ threadId: currentAIChatThread },
|
||||
);
|
||||
|
||||
const handleFirstLoad = useCallback(
|
||||
(_data: GetChatMessagesQuery) => {
|
||||
setSkipMessagesSkeletonUntilLoaded(false);
|
||||
@@ -52,9 +48,34 @@ export const AgentChatMessagesFetchEffect = () => {
|
||||
const handleDataLoaded = useCallback(
|
||||
(data: GetChatMessagesQuery) => {
|
||||
const uiMessages = mapDBMessagesToUIMessages(data.chatMessages ?? []);
|
||||
setAgentChatFetchedMessages(uiMessages);
|
||||
|
||||
const threadId = store.get(currentAIChatThreadState.atom);
|
||||
|
||||
if (!isDefined(threadId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const threadAtom = agentChatInstanceByThreadIdFamilyState.atomFamily({
|
||||
threadId,
|
||||
});
|
||||
|
||||
const chatInstance = store.get(threadAtom);
|
||||
|
||||
if (chatInstance === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isStreaming =
|
||||
chatInstance.status === 'streaming' ||
|
||||
chatInstance.status === 'submitted';
|
||||
|
||||
if (isStreaming) {
|
||||
return;
|
||||
}
|
||||
|
||||
chatInstance.messages = uiMessages;
|
||||
},
|
||||
[setAgentChatFetchedMessages],
|
||||
[store],
|
||||
);
|
||||
|
||||
const handleLoadingChange = useCallback(
|
||||
|
||||
@@ -10,13 +10,14 @@ import { currentAIChatThreadTitleState } from '@/ai/states/currentAIChatThreadTi
|
||||
|
||||
import { AGENT_CHAT_RETRY_EVENT_NAME } from '@/ai/constants/AgentChatRetryEventName';
|
||||
import { AGENT_CHAT_STOP_EVENT_NAME } from '@/ai/constants/AgentChatStopEventName';
|
||||
import { AGENT_CHAT_UNKNOWN_THREAD_ID } from '@/ai/constants/AgentChatUnknownThreadId';
|
||||
import { useAgentChatInstanceForThread } from '@/ai/hooks/useAgentChatInstanceForThread';
|
||||
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
|
||||
import {
|
||||
AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
|
||||
agentChatDraftsByThreadIdState,
|
||||
} from '@/ai/states/agentChatDraftsByThreadIdState';
|
||||
import { agentChatInputState } from '@/ai/states/agentChatInputState';
|
||||
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { getTokenPair } from '@/apollo/utils/getTokenPair';
|
||||
import { renewToken } from '@/auth/services/AuthService';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
@@ -25,16 +26,14 @@ import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, 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>,
|
||||
onStreamingComplete?: () => void,
|
||||
) => {
|
||||
@@ -65,87 +64,53 @@ export const useAgentChat = (
|
||||
agentChatDraftsByThreadIdState,
|
||||
);
|
||||
|
||||
const retryFetchWithRenewedToken = async (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
) => {
|
||||
const tokenPair = getTokenPair();
|
||||
const retryFetchWithRenewedToken = useCallback(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const tokenPair = getTokenPair();
|
||||
|
||||
if (!isDefined(tokenPair)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const renewedTokens = await renewToken(
|
||||
`${REACT_APP_SERVER_BASE_URL}/metadata`,
|
||||
tokenPair,
|
||||
);
|
||||
|
||||
if (!isDefined(renewedTokens)) {
|
||||
setTokenPair(null);
|
||||
if (!isDefined(tokenPair)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renewedAccessToken =
|
||||
renewedTokens.accessOrWorkspaceAgnosticToken?.token;
|
||||
try {
|
||||
const renewedTokens = await renewToken(
|
||||
`${REACT_APP_SERVER_BASE_URL}/metadata`,
|
||||
tokenPair,
|
||||
);
|
||||
|
||||
if (!isDefined(renewedAccessToken)) {
|
||||
if (!isDefined(renewedTokens)) {
|
||||
setTokenPair(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const renewedAccessToken =
|
||||
renewedTokens.accessOrWorkspaceAgnosticToken?.token;
|
||||
|
||||
if (!isDefined(renewedAccessToken)) {
|
||||
setTokenPair(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
cookieStorage.setItem('tokenPair', JSON.stringify(renewedTokens));
|
||||
setTokenPair(renewedTokens);
|
||||
|
||||
const updatedHeaders = new Headers(init?.headers ?? {});
|
||||
updatedHeaders.set('Authorization', `Bearer ${renewedAccessToken}`);
|
||||
|
||||
return fetch(input, {
|
||||
...init,
|
||||
headers: updatedHeaders,
|
||||
});
|
||||
} catch {
|
||||
setTokenPair(null);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[setTokenPair],
|
||||
);
|
||||
|
||||
cookieStorage.setItem('tokenPair', JSON.stringify(renewedTokens));
|
||||
setTokenPair(renewedTokens);
|
||||
|
||||
const updatedHeaders = new Headers(init?.headers ?? {});
|
||||
updatedHeaders.set('Authorization', `Bearer ${renewedAccessToken}`);
|
||||
|
||||
return fetch(input, {
|
||||
...init,
|
||||
headers: updatedHeaders,
|
||||
});
|
||||
} catch {
|
||||
setTokenPair(null);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const { sendMessage, messages, status, error, regenerate, stop } = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: `${REST_API_BASE_URL}/agent-chat/stream`,
|
||||
headers: () => ({
|
||||
Authorization: `Bearer ${getTokenPair()?.accessOrWorkspaceAgnosticToken.token}`,
|
||||
}),
|
||||
fetch: async (input, init) => {
|
||||
const response = await fetch(input, init);
|
||||
|
||||
if (response.status === 401) {
|
||||
const retriedResponse = await retryFetchWithRenewedToken(input, init);
|
||||
|
||||
return retriedResponse ?? response;
|
||||
}
|
||||
|
||||
// For non-2xx responses, parse the error body and throw with the code
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json().catch(() => ({}));
|
||||
const error = new Error(
|
||||
errorBody.messages?.[0] ||
|
||||
`Request failed with status ${response.status}`,
|
||||
) as Error & { code?: string };
|
||||
|
||||
if (isDefined(errorBody.code)) {
|
||||
error.code = errorBody.code;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
messages: uiMessages,
|
||||
id: `${currentAIChatThread}-${uiMessages.length}`,
|
||||
experimental_throttle: 100,
|
||||
onFinish: ({ message }) => {
|
||||
const handleOnFinish = useCallback(
|
||||
({ message }: { message: ExtendedUIMessage }) => {
|
||||
type UsageMetadata = {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
@@ -186,7 +151,8 @@ export const useAgentChat = (
|
||||
);
|
||||
|
||||
setPendingThreadIdAfterFirstSend((pendingId) => {
|
||||
const threadIdForTitle = pendingId ?? currentAIChatThread;
|
||||
const threadIdForTitle =
|
||||
pendingId ?? store.get(currentAIChatThreadState.atom);
|
||||
if (isDefined(titlePart) && titlePart.type === 'data-thread-title') {
|
||||
setCurrentAIChatThreadTitle(titlePart.data.title);
|
||||
if (isDefined(threadIdForTitle)) {
|
||||
@@ -212,8 +178,47 @@ export const useAgentChat = (
|
||||
|
||||
onStreamingComplete?.();
|
||||
},
|
||||
[
|
||||
setAgentChatUsage,
|
||||
store,
|
||||
apolloClient.cache,
|
||||
setCurrentAIChatThreadTitle,
|
||||
setCurrentAIChatThread,
|
||||
onStreamingComplete,
|
||||
],
|
||||
);
|
||||
|
||||
const { agentChatInstanceForThread } = useAgentChatInstanceForThread({
|
||||
threadId: currentAIChatThread ?? AGENT_CHAT_UNKNOWN_THREAD_ID,
|
||||
onFinish: handleOnFinish,
|
||||
retryFetchWithRenewedToken,
|
||||
});
|
||||
|
||||
const {
|
||||
sendMessage,
|
||||
messages,
|
||||
status,
|
||||
error,
|
||||
regenerate,
|
||||
stop,
|
||||
resumeStream,
|
||||
} = useChat({
|
||||
chat: agentChatInstanceForThread,
|
||||
resume: true,
|
||||
experimental_throttle: 100,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const isRealThreadId =
|
||||
isDefined(currentAIChatThread) &&
|
||||
currentAIChatThread !== AGENT_CHAT_UNKNOWN_THREAD_ID &&
|
||||
currentAIChatThread !== AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
|
||||
|
||||
if (isRealThreadId) {
|
||||
resumeStream();
|
||||
}
|
||||
}, [currentAIChatThread, resumeStream]);
|
||||
|
||||
const isStreaming = status === 'streaming';
|
||||
const isLoading = isStreaming || agentChatSelectedFiles.length > 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Chat } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
import { useStore } from 'jotai';
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
|
||||
import { agentChatInstanceByThreadIdFamilyState } from '@/ai/states/agentChatInstanceByThreadIdFamilyState';
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { getTokenPair } from '@/apollo/utils/getTokenPair';
|
||||
|
||||
type UseAgentChatInstanceForThreadOptions = {
|
||||
threadId: string;
|
||||
onFinish: (options: { message: ExtendedUIMessage }) => void;
|
||||
retryFetchWithRenewedToken: (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
) => Promise<Response | null>;
|
||||
};
|
||||
|
||||
const createAgentChatInstanceForThread = ({
|
||||
threadId,
|
||||
onFinish,
|
||||
retryFetchWithRenewedToken,
|
||||
}: UseAgentChatInstanceForThreadOptions): Chat<ExtendedUIMessage> => {
|
||||
return new Chat<ExtendedUIMessage>({
|
||||
id: threadId,
|
||||
messages: [],
|
||||
transport: new DefaultChatTransport({
|
||||
api: `${REST_API_BASE_URL}/agent-chat/stream`,
|
||||
headers: () => ({
|
||||
Authorization: `Bearer ${getTokenPair()?.accessOrWorkspaceAgnosticToken.token}`,
|
||||
}),
|
||||
prepareReconnectToStreamRequest: ({ id }) => ({
|
||||
api: `${REST_API_BASE_URL}/agent-chat/${id}/stream`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${getTokenPair()?.accessOrWorkspaceAgnosticToken.token}`,
|
||||
},
|
||||
}),
|
||||
fetch: async (input, init) => {
|
||||
const response = await fetch(input, init);
|
||||
|
||||
if (response.status === 401) {
|
||||
const retriedResponse = await retryFetchWithRenewedToken(input, init);
|
||||
|
||||
return retriedResponse ?? response;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.json().catch(() => ({}));
|
||||
const error = new Error(
|
||||
errorBody.messages?.[0] ||
|
||||
`Request failed with status ${response.status}`,
|
||||
) as Error & { code?: string };
|
||||
|
||||
if (errorBody.code !== undefined) {
|
||||
error.code = errorBody.code;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
onFinish,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAgentChatInstanceForThread = ({
|
||||
threadId,
|
||||
onFinish,
|
||||
retryFetchWithRenewedToken,
|
||||
}: UseAgentChatInstanceForThreadOptions): {
|
||||
agentChatInstanceForThread: Chat<ExtendedUIMessage>;
|
||||
} => {
|
||||
const store = useStore();
|
||||
|
||||
const threadAtom = agentChatInstanceByThreadIdFamilyState.atomFamily({
|
||||
threadId,
|
||||
});
|
||||
|
||||
const existingInstance = store.get(threadAtom);
|
||||
|
||||
if (existingInstance !== null) {
|
||||
return { agentChatInstanceForThread: existingInstance };
|
||||
}
|
||||
|
||||
const newInstance = createAgentChatInstanceForThread({
|
||||
threadId,
|
||||
onFinish,
|
||||
retryFetchWithRenewedToken,
|
||||
});
|
||||
|
||||
store.set(threadAtom, newInstance);
|
||||
|
||||
return { agentChatInstanceForThread: newInstance };
|
||||
};
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { AgentChatComponentInstanceContext } from '@/ai/states/AgentChatComponentInstanceContext';
|
||||
import { createAtomComponentFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomComponentFamilyState';
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
|
||||
export const agentChatFetchedMessagesComponentFamilyState =
|
||||
createAtomComponentFamilyState<ExtendedUIMessage[], { threadId: string }>({
|
||||
key: 'agentChatFetchedMessagesComponentFamilyState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: AgentChatComponentInstanceContext,
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { type Chat } from '@ai-sdk/react';
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
|
||||
import { createAtomFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomFamilyState';
|
||||
|
||||
export const agentChatInstanceByThreadIdFamilyState = createAtomFamilyState<
|
||||
Chat<ExtendedUIMessage> | null,
|
||||
{ threadId: string }
|
||||
>({
|
||||
key: 'agentChatInstanceByThreadIdFamilyState',
|
||||
defaultValue: null,
|
||||
});
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
|
||||
export const mergeAgentChatFetchedAndStreamingMessages = (
|
||||
fetchedMessages: ExtendedUIMessage[],
|
||||
streamingMessages: ExtendedUIMessage[],
|
||||
): ExtendedUIMessage[] => {
|
||||
const fetchedMessageIds = new Set(
|
||||
fetchedMessages.map((message) => message.id),
|
||||
);
|
||||
|
||||
const streamingOnlyMessages = streamingMessages.filter(
|
||||
(message) => !fetchedMessageIds.has(message.id),
|
||||
);
|
||||
|
||||
return [...fetchedMessages, ...streamingOnlyMessages];
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+10
-2
@@ -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 };
|
||||
|
||||
+8
@@ -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;
|
||||
};
|
||||
+2
-2
@@ -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 ||
|
||||
|
||||
-69
@@ -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>
|
||||
);
|
||||
};
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
export const DROP_RESULT_OPTIONS = {
|
||||
reason: 'DROP' as const,
|
||||
combine: null,
|
||||
mode: 'FLUID' as const,
|
||||
type: 'DEFAULT' as const,
|
||||
};
|
||||
+3
-2
@@ -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 ||
|
||||
|
||||
+4
-5
@@ -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;
|
||||
|
||||
|
||||
+18
-24
@@ -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 = {
|
||||
|
||||
-20
@@ -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,
|
||||
};
|
||||
};
|
||||
-102
@@ -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>
|
||||
);
|
||||
};
|
||||
+4
-1
@@ -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 = () => {
|
||||
|
||||
+10
-3
@@ -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();
|
||||
},
|
||||
|
||||
+29
-13
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
+19
-18
@@ -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,
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
"react-dom": "18.3.1",
|
||||
"redis": "^4.7.0",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"resumable-stream": "^2.2.12",
|
||||
"rxjs": "7.8.1",
|
||||
"semver": "7.6.3",
|
||||
"sharp": "0.32.6",
|
||||
|
||||
-431
@@ -1,431 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import {
|
||||
CoreObjectNameSingular,
|
||||
FieldMetadataType,
|
||||
PageLayoutTabLayoutMode,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import {
|
||||
GRID_POSITIONS,
|
||||
VERTICAL_LIST_LAYOUT_POSITIONS,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
const HOME_TAB_POSITION = 10;
|
||||
|
||||
const isActivityTargetField = (
|
||||
fieldName: string,
|
||||
objectNameSingular: string,
|
||||
): boolean =>
|
||||
(objectNameSingular === CoreObjectNameSingular.Note &&
|
||||
fieldName === 'noteTargets') ||
|
||||
(objectNameSingular === CoreObjectNameSingular.Task &&
|
||||
fieldName === 'taskTargets');
|
||||
|
||||
const isJunctionRelationField = (field: FlatFieldMetadata): boolean => {
|
||||
const settings = field.settings as
|
||||
| { junctionTargetFieldId?: string }
|
||||
| undefined;
|
||||
|
||||
return (
|
||||
isDefined(settings?.junctionTargetFieldId) &&
|
||||
typeof settings?.junctionTargetFieldId === 'string' &&
|
||||
settings.junctionTargetFieldId.length > 0
|
||||
);
|
||||
};
|
||||
|
||||
const isRelationTargetAvailable = (
|
||||
targetObject: FlatObjectMetadata | undefined,
|
||||
): boolean => {
|
||||
if (!isDefined(targetObject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetObject.isRemote) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
targetObject.isSystem &&
|
||||
targetObject.nameSingular !== CoreObjectNameSingular.WorkspaceMember
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:backfill-field-widgets',
|
||||
description:
|
||||
'Backfill FIELD widgets for relation fields in existing page layouts',
|
||||
})
|
||||
export class BackfillFieldWidgetsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of FIELD widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
]);
|
||||
|
||||
// Build a set of fieldMetadataIds that already have a FIELD widget
|
||||
const existingFieldWidgetFieldIds = new Set<string>();
|
||||
|
||||
for (const widget of Object.values(
|
||||
flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (
|
||||
isDefined(widget) &&
|
||||
widget.configuration?.configurationType ===
|
||||
WidgetConfigurationType.FIELD &&
|
||||
isDefined(widget.configuration.fieldMetadataId)
|
||||
) {
|
||||
existingFieldWidgetFieldIds.add(widget.configuration.fieldMetadataId);
|
||||
}
|
||||
}
|
||||
|
||||
// Build object ID → objectMetadata map
|
||||
const objectById = new Map<string, FlatObjectMetadata>();
|
||||
|
||||
for (const obj of Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (isDefined(obj)) {
|
||||
objectById.set(obj.id, obj);
|
||||
}
|
||||
}
|
||||
|
||||
// Build object ID → RECORD_PAGE layout map
|
||||
const recordPageLayoutByObjectId = new Map<
|
||||
string,
|
||||
{ id: string; universalIdentifier: string }
|
||||
>();
|
||||
|
||||
for (const layout of Object.values(
|
||||
flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (
|
||||
isDefined(layout) &&
|
||||
layout.type === PageLayoutType.RECORD_PAGE &&
|
||||
isDefined(layout.objectMetadataId)
|
||||
) {
|
||||
recordPageLayoutByObjectId.set(layout.objectMetadataId, {
|
||||
id: layout.id,
|
||||
universalIdentifier: layout.universalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build pageLayoutId → home tab map
|
||||
const homeTabByPageLayoutId = new Map<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
widgetCount: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const tab of Object.values(
|
||||
flatPageLayoutTabMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (
|
||||
isDefined(tab) &&
|
||||
tab.position === HOME_TAB_POSITION &&
|
||||
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
|
||||
) {
|
||||
homeTabByPageLayoutId.set(tab.pageLayoutId, {
|
||||
id: tab.id,
|
||||
universalIdentifier: tab.universalIdentifier,
|
||||
widgetCount: tab.widgetIds?.length ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group fields by objectMetadataId
|
||||
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
|
||||
|
||||
// Map morphId → all field IDs sharing that morphId (for dedup)
|
||||
const fieldIdsByMorphId = new Map<string, string[]>();
|
||||
|
||||
for (const field of Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(field) || !isDefined(field.objectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const list = fieldsByObjectId.get(field.objectMetadataId) ?? [];
|
||||
|
||||
list.push(field);
|
||||
fieldsByObjectId.set(field.objectMetadataId, list);
|
||||
|
||||
if (
|
||||
field.type === FieldMetadataType.MORPH_RELATION &&
|
||||
isDefined(field.morphId)
|
||||
) {
|
||||
const morphFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
|
||||
|
||||
morphFieldIds.push(field.id);
|
||||
fieldIdsByMorphId.set(field.morphId, morphFieldIds);
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const standardWidgetsToCreate: FlatPageLayoutWidget[] = [];
|
||||
const customWidgetsToCreate: FlatPageLayoutWidget[] = [];
|
||||
const processedMorphIds = new Set<string>();
|
||||
|
||||
for (const object of objectById.values()) {
|
||||
const pageLayout = recordPageLayoutByObjectId.get(object.id);
|
||||
|
||||
if (!isDefined(pageLayout)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const homeTab = homeTabByPageLayoutId.get(pageLayout.id);
|
||||
|
||||
if (!isDefined(homeTab)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatApplication: FlatApplication = object.isCustom
|
||||
? workspaceCustomFlatApplication
|
||||
: twentyStandardFlatApplication;
|
||||
|
||||
const targetList: FlatPageLayoutWidget[] = object.isCustom
|
||||
? customWidgetsToCreate
|
||||
: standardWidgetsToCreate;
|
||||
|
||||
const fields = fieldsByObjectId.get(object.id) ?? [];
|
||||
let nextWidgetIndex = homeTab.widgetCount;
|
||||
|
||||
for (const field of fields) {
|
||||
if (
|
||||
field.type !== FieldMetadataType.RELATION &&
|
||||
field.type !== FieldMetadataType.MORPH_RELATION
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isActivityTargetField(field.name, object.nameSingular)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isJunctionRelationField(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.type === FieldMetadataType.RELATION) {
|
||||
const targetObject = isDefined(field.relationTargetObjectMetadataId)
|
||||
? objectById.get(field.relationTargetObjectMetadataId)
|
||||
: undefined;
|
||||
|
||||
if (!isRelationTargetAvailable(targetObject)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// For morph relations, skip if any sibling field (same morphId)
|
||||
// already has a widget or was already processed in this run
|
||||
if (
|
||||
field.type === FieldMetadataType.MORPH_RELATION &&
|
||||
isDefined(field.morphId)
|
||||
) {
|
||||
if (processedMorphIds.has(field.morphId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const siblingFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
|
||||
const alreadyHasWidget = siblingFieldIds.some((id) =>
|
||||
existingFieldWidgetFieldIds.has(id),
|
||||
);
|
||||
|
||||
if (alreadyHasWidget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedMorphIds.add(field.morphId);
|
||||
}
|
||||
|
||||
if (existingFieldWidgetFieldIds.has(field.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const widget: FlatPageLayoutWidget = {
|
||||
id: v4(),
|
||||
universalIdentifier: v4(),
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
pageLayoutTabId: homeTab.id,
|
||||
pageLayoutTabUniversalIdentifier: homeTab.universalIdentifier,
|
||||
title: field.label,
|
||||
type: WidgetType.FIELD,
|
||||
gridPosition: { ...GRID_POSITIONS.FULL_WIDTH },
|
||||
position: {
|
||||
...VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
|
||||
index: nextWidgetIndex,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: field.id,
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
},
|
||||
universalConfiguration: {
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: field.universalIdentifier,
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
},
|
||||
objectMetadataId: object.id,
|
||||
objectMetadataUniversalIdentifier: object.universalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
conditionalDisplay: null,
|
||||
overrides: null,
|
||||
};
|
||||
|
||||
targetList.push(widget);
|
||||
existingFieldWidgetFieldIds.add(field.id);
|
||||
nextWidgetIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
const totalWidgets =
|
||||
standardWidgetsToCreate.length + customWidgetsToCreate.length;
|
||||
|
||||
if (totalWidgets === 0) {
|
||||
this.logger.log(
|
||||
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${totalWidgets} FIELD widget(s) to create for workspace ${workspaceId} (${standardWidgetsToCreate.length} standard, ${customWidgetsToCreate.length} custom)`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (standardWidgetsToCreate.length > 0) {
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: standardWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create standard FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create standard FIELD widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (customWidgetsToCreate.length > 0) {
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: customWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create custom FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create custom FIELD widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+868
@@ -0,0 +1,868 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import {
|
||||
CoreObjectNameSingular,
|
||||
FeatureFlagKey,
|
||||
FieldMetadataType,
|
||||
PageLayoutTabLayoutMode,
|
||||
ViewType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
|
||||
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
|
||||
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
|
||||
import { FieldDisplayMode } from 'src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import {
|
||||
GRID_POSITIONS,
|
||||
VERTICAL_LIST_LAYOUT_POSITIONS,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
|
||||
const HOME_TAB_POSITION = 10;
|
||||
|
||||
const isActivityTargetField = (
|
||||
fieldName: string,
|
||||
objectNameSingular: string,
|
||||
): boolean =>
|
||||
(objectNameSingular === CoreObjectNameSingular.Note &&
|
||||
fieldName === 'noteTargets') ||
|
||||
(objectNameSingular === CoreObjectNameSingular.Task &&
|
||||
fieldName === 'taskTargets');
|
||||
|
||||
const isJunctionRelationField = (field: FlatFieldMetadata): boolean => {
|
||||
const settings = field.settings as
|
||||
| { junctionTargetFieldId?: string }
|
||||
| undefined;
|
||||
|
||||
return (
|
||||
isDefined(settings?.junctionTargetFieldId) &&
|
||||
typeof settings?.junctionTargetFieldId === 'string' &&
|
||||
settings.junctionTargetFieldId.length > 0
|
||||
);
|
||||
};
|
||||
|
||||
const isRelationTargetAvailable = (
|
||||
targetObject: FlatObjectMetadata | undefined,
|
||||
): boolean => {
|
||||
if (!isDefined(targetObject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetObject.isRemote) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
targetObject.isSystem &&
|
||||
targetObject.nameSingular !== CoreObjectNameSingular.WorkspaceMember
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:backfill-page-layouts-and-fields-widget-view-fields',
|
||||
description:
|
||||
'Backfill RECORD_PAGE page layouts, sync FIELDS_WIDGET view fields, create FIELD widgets, and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
})
|
||||
export class BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of page layouts and field widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isAlreadyEnabled) {
|
||||
this.logger.log(
|
||||
`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED already enabled for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would create page layouts, sync view fields, create FIELD widgets and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
await this.backfillStandardPageLayoutsAndSyncViews({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
});
|
||||
|
||||
await this.backfillCustomObjectPageLayouts({
|
||||
workspaceId,
|
||||
workspaceCustomFlatApplication,
|
||||
});
|
||||
|
||||
await this.backfillFieldWidgets({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
workspaceCustomFlatApplication,
|
||||
});
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled page layouts and field widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillStandardPageLayoutsAndSyncViews({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
shouldIncludeRecordPageLayouts: true,
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const recordPageLayoutUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const recordPageLayoutsFromStandard = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((pageLayout) => {
|
||||
if (pageLayout.type !== PageLayoutType.RECORD_PAGE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
recordPageLayoutUniversalIdentifiers.add(
|
||||
pageLayout.universalIdentifier,
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const tabUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const tabsFromStandard = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutTabMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((tab) => {
|
||||
if (
|
||||
!recordPageLayoutUniversalIdentifiers.has(
|
||||
tab.pageLayoutUniversalIdentifier,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tabUniversalIdentifiers.add(tab.universalIdentifier);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const widgetsFromStandard = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((widget) =>
|
||||
tabUniversalIdentifiers.has(widget.pageLayoutTabUniversalIdentifier),
|
||||
);
|
||||
|
||||
const {
|
||||
flatPageLayoutMaps: existingFlatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFieldMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
]);
|
||||
|
||||
// Filter out page layouts, tabs, and widgets that already exist in the workspace
|
||||
const pageLayoutsToCreate = recordPageLayoutsFromStandard.filter(
|
||||
(pageLayout) =>
|
||||
!isDefined(
|
||||
existingFlatPageLayoutMaps.byUniversalIdentifier[
|
||||
pageLayout.universalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const pageLayoutTabsToCreate = tabsFromStandard.filter(
|
||||
(tab) =>
|
||||
!isDefined(
|
||||
existingFlatPageLayoutTabMaps.byUniversalIdentifier[
|
||||
tab.universalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const pageLayoutWidgetsToCreate = widgetsFromStandard.filter(
|
||||
(widget) =>
|
||||
!isDefined(
|
||||
existingFlatPageLayoutWidgetMaps.byUniversalIdentifier[
|
||||
widget.universalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Collect all standard FIELDS_WIDGET view universal identifiers
|
||||
// This includes both new views to create AND existing views (created by 1-19)
|
||||
const allStandardFieldsWidgetViewUniversalIds = new Set<string>();
|
||||
|
||||
const viewsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatViewMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((view) => {
|
||||
if (view.type !== ViewType.FIELDS_WIDGET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
allStandardFieldsWidgetViewUniversalIds.add(view.universalIdentifier);
|
||||
|
||||
if (
|
||||
isDefined(
|
||||
existingFlatViewMaps.byUniversalIdentifier[
|
||||
view.universalIdentifier
|
||||
],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Delete all existing viewFields for standard FIELDS_WIDGET views and recreate from current standard.
|
||||
// This ensures positions, visibility, and groups match the current standard definition.
|
||||
const viewFieldsToDelete = Object.values(
|
||||
existingFlatViewFieldMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewField) =>
|
||||
allStandardFieldsWidgetViewUniversalIds.has(
|
||||
viewField.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const viewFieldsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewField) =>
|
||||
allStandardFieldsWidgetViewUniversalIds.has(
|
||||
viewField.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
// Same delete-and-recreate approach for viewFieldGroups
|
||||
const viewFieldGroupsToDelete = Object.values(
|
||||
existingFlatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewFieldGroup) =>
|
||||
allStandardFieldsWidgetViewUniversalIds.has(
|
||||
viewFieldGroup.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
const viewFieldGroupsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((viewFieldGroup) =>
|
||||
allStandardFieldsWidgetViewUniversalIds.has(
|
||||
viewFieldGroup.viewUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Creating ${pageLayoutsToCreate.length} page layouts, ${viewsToCreate.length} views, deleting ${viewFieldsToDelete.length} and creating ${viewFieldsToCreate.length} view fields, deleting ${viewFieldGroupsToDelete.length} and creating ${viewFieldGroupsToCreate.length} view field groups for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: pageLayoutsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: pageLayoutTabsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: pageLayoutWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: viewsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: viewFieldsToCreate,
|
||||
flatEntityToDelete: viewFieldsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewFieldGroup: {
|
||||
flatEntityToCreate: viewFieldGroupsToCreate,
|
||||
flatEntityToDelete: viewFieldGroupsToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create standard page layouts and sync view fields:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create standard page layouts and sync view fields for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async backfillCustomObjectPageLayouts({
|
||||
workspaceId,
|
||||
workspaceCustomFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceCustomFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatPageLayoutMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutMaps',
|
||||
]);
|
||||
|
||||
const existingPageLayouts = Object.values(
|
||||
flatPageLayoutMaps.byUniversalIdentifier,
|
||||
).filter(isDefined);
|
||||
|
||||
const objectIdsWithRecordPageLayout = new Set(
|
||||
existingPageLayouts
|
||||
.filter(
|
||||
(layout: FlatPageLayout) =>
|
||||
layout.type === PageLayoutType.RECORD_PAGE &&
|
||||
isDefined(layout.objectMetadataId),
|
||||
)
|
||||
.map((layout: FlatPageLayout) => layout.objectMetadataId),
|
||||
);
|
||||
|
||||
const customObjectsWithoutPageLayout = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.isCustom &&
|
||||
!objectMetadata.isRemote &&
|
||||
!objectIdsWithRecordPageLayout.has(objectMetadata.id),
|
||||
);
|
||||
|
||||
if (customObjectsWithoutPageLayout.length === 0) {
|
||||
this.logger.log(
|
||||
`No custom objects without page layouts found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Creating page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const allCustomPageLayoutsToCreate: FlatPageLayout[] = [];
|
||||
const allCustomPageLayoutTabsToCreate: FlatPageLayoutTab[] = [];
|
||||
const allCustomPageLayoutWidgetsToCreate: FlatPageLayoutWidget[] = [];
|
||||
const allCustomViewsToCreate: (UniversalFlatView & { id: string })[] = [];
|
||||
const allCustomViewFieldsToCreate: UniversalFlatViewField[] = [];
|
||||
|
||||
for (const customObject of customObjectsWithoutPageLayout) {
|
||||
const flatRecordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({
|
||||
objectMetadata: customObject,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
});
|
||||
|
||||
const objectFieldMetadatas = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((field) => field.objectMetadataId === customObject.id);
|
||||
|
||||
const viewFields = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: objectFieldMetadatas,
|
||||
viewUniversalIdentifier: flatRecordPageFieldsView.universalIdentifier,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
excludeLabelIdentifier: true,
|
||||
});
|
||||
|
||||
const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } =
|
||||
computeFlatDefaultRecordPageLayoutToCreate({
|
||||
objectMetadata: customObject,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
recordPageFieldsView: flatRecordPageFieldsView,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
allCustomPageLayoutsToCreate.push(...pageLayouts);
|
||||
allCustomPageLayoutTabsToCreate.push(...pageLayoutTabs);
|
||||
allCustomPageLayoutWidgetsToCreate.push(...pageLayoutWidgets);
|
||||
allCustomViewsToCreate.push(flatRecordPageFieldsView);
|
||||
allCustomViewFieldsToCreate.push(...viewFields);
|
||||
}
|
||||
|
||||
const customValidateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: allCustomPageLayoutsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: allCustomPageLayoutTabsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: allCustomPageLayoutWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: allCustomViewsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: allCustomViewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (customValidateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create custom object page layouts:\n${JSON.stringify(customValidateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create custom object page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillFieldWidgets({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
workspaceCustomFlatApplication,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardFlatApplication: FlatApplication;
|
||||
workspaceCustomFlatApplication: FlatApplication;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
]);
|
||||
|
||||
// Build a set of fieldMetadataIds that already have a FIELD widget
|
||||
const existingFieldWidgetFieldIds = new Set<string>();
|
||||
|
||||
for (const widget of Object.values(
|
||||
flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (
|
||||
isDefined(widget) &&
|
||||
widget.configuration?.configurationType ===
|
||||
WidgetConfigurationType.FIELD &&
|
||||
isDefined(widget.configuration.fieldMetadataId)
|
||||
) {
|
||||
existingFieldWidgetFieldIds.add(widget.configuration.fieldMetadataId);
|
||||
}
|
||||
}
|
||||
|
||||
// Build object ID → objectMetadata map
|
||||
const objectById = new Map<string, FlatObjectMetadata>();
|
||||
|
||||
for (const obj of Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (isDefined(obj)) {
|
||||
objectById.set(obj.id, obj);
|
||||
}
|
||||
}
|
||||
|
||||
// Build object ID → RECORD_PAGE layout map
|
||||
const recordPageLayoutByObjectId = new Map<
|
||||
string,
|
||||
{ id: string; universalIdentifier: string }
|
||||
>();
|
||||
|
||||
for (const layout of Object.values(
|
||||
flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (
|
||||
isDefined(layout) &&
|
||||
layout.type === PageLayoutType.RECORD_PAGE &&
|
||||
isDefined(layout.objectMetadataId)
|
||||
) {
|
||||
recordPageLayoutByObjectId.set(layout.objectMetadataId, {
|
||||
id: layout.id,
|
||||
universalIdentifier: layout.universalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build pageLayoutId → home tab map
|
||||
const homeTabByPageLayoutId = new Map<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
widgetCount: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const tab of Object.values(
|
||||
flatPageLayoutTabMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (
|
||||
isDefined(tab) &&
|
||||
tab.position === HOME_TAB_POSITION &&
|
||||
tab.layoutMode === PageLayoutTabLayoutMode.VERTICAL_LIST
|
||||
) {
|
||||
homeTabByPageLayoutId.set(tab.pageLayoutId, {
|
||||
id: tab.id,
|
||||
universalIdentifier: tab.universalIdentifier,
|
||||
widgetCount: tab.widgetIds?.length ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group fields by objectMetadataId
|
||||
const fieldsByObjectId = new Map<string, FlatFieldMetadata[]>();
|
||||
|
||||
// Map morphId → all field IDs sharing that morphId (for dedup)
|
||||
const fieldIdsByMorphId = new Map<string, string[]>();
|
||||
|
||||
for (const field of Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(field) || !isDefined(field.objectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const list = fieldsByObjectId.get(field.objectMetadataId) ?? [];
|
||||
|
||||
list.push(field);
|
||||
fieldsByObjectId.set(field.objectMetadataId, list);
|
||||
|
||||
if (
|
||||
field.type === FieldMetadataType.MORPH_RELATION &&
|
||||
isDefined(field.morphId)
|
||||
) {
|
||||
const morphFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
|
||||
|
||||
morphFieldIds.push(field.id);
|
||||
fieldIdsByMorphId.set(field.morphId, morphFieldIds);
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const standardWidgetsToCreate: FlatPageLayoutWidget[] = [];
|
||||
const customWidgetsToCreate: FlatPageLayoutWidget[] = [];
|
||||
const processedMorphIds = new Set<string>();
|
||||
|
||||
for (const object of objectById.values()) {
|
||||
const pageLayout = recordPageLayoutByObjectId.get(object.id);
|
||||
|
||||
if (!isDefined(pageLayout)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const homeTab = homeTabByPageLayoutId.get(pageLayout.id);
|
||||
|
||||
if (!isDefined(homeTab)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatApplication: FlatApplication = object.isCustom
|
||||
? workspaceCustomFlatApplication
|
||||
: twentyStandardFlatApplication;
|
||||
|
||||
const targetList: FlatPageLayoutWidget[] = object.isCustom
|
||||
? customWidgetsToCreate
|
||||
: standardWidgetsToCreate;
|
||||
|
||||
const fields = fieldsByObjectId.get(object.id) ?? [];
|
||||
let nextWidgetIndex = homeTab.widgetCount;
|
||||
|
||||
for (const field of fields) {
|
||||
if (
|
||||
field.type !== FieldMetadataType.RELATION &&
|
||||
field.type !== FieldMetadataType.MORPH_RELATION
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isActivityTargetField(field.name, object.nameSingular)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isJunctionRelationField(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.type === FieldMetadataType.RELATION) {
|
||||
const targetObject = isDefined(field.relationTargetObjectMetadataId)
|
||||
? objectById.get(field.relationTargetObjectMetadataId)
|
||||
: undefined;
|
||||
|
||||
if (!isRelationTargetAvailable(targetObject)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// For morph relations, skip if any sibling field (same morphId)
|
||||
// already has a widget or was already processed in this run
|
||||
if (
|
||||
field.type === FieldMetadataType.MORPH_RELATION &&
|
||||
isDefined(field.morphId)
|
||||
) {
|
||||
if (processedMorphIds.has(field.morphId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const siblingFieldIds = fieldIdsByMorphId.get(field.morphId) ?? [];
|
||||
const alreadyHasWidget = siblingFieldIds.some((id) =>
|
||||
existingFieldWidgetFieldIds.has(id),
|
||||
);
|
||||
|
||||
if (alreadyHasWidget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedMorphIds.add(field.morphId);
|
||||
}
|
||||
|
||||
if (existingFieldWidgetFieldIds.has(field.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const widget: FlatPageLayoutWidget = {
|
||||
id: v4(),
|
||||
universalIdentifier: v4(),
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
workspaceId,
|
||||
pageLayoutTabId: homeTab.id,
|
||||
pageLayoutTabUniversalIdentifier: homeTab.universalIdentifier,
|
||||
title: field.label,
|
||||
type: WidgetType.FIELD,
|
||||
gridPosition: { ...GRID_POSITIONS.FULL_WIDTH },
|
||||
position: {
|
||||
...VERTICAL_LIST_LAYOUT_POSITIONS.SECOND,
|
||||
index: nextWidgetIndex,
|
||||
},
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: field.id,
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
},
|
||||
universalConfiguration: {
|
||||
configurationType: WidgetConfigurationType.FIELD,
|
||||
fieldMetadataId: field.universalIdentifier,
|
||||
fieldDisplayMode: FieldDisplayMode.CARD,
|
||||
},
|
||||
objectMetadataId: object.id,
|
||||
objectMetadataUniversalIdentifier: object.universalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
conditionalDisplay: null,
|
||||
overrides: null,
|
||||
};
|
||||
|
||||
targetList.push(widget);
|
||||
existingFieldWidgetFieldIds.add(field.id);
|
||||
nextWidgetIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
const totalWidgets =
|
||||
standardWidgetsToCreate.length + customWidgetsToCreate.length;
|
||||
|
||||
if (totalWidgets === 0) {
|
||||
this.logger.log(
|
||||
`All FIELD widgets already exist for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${totalWidgets} FIELD widget(s) to create for workspace ${workspaceId} (${standardWidgetsToCreate.length} standard, ${customWidgetsToCreate.length} custom)`,
|
||||
);
|
||||
|
||||
if (standardWidgetsToCreate.length > 0) {
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: standardWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create standard FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create standard FIELD widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (customWidgetsToCreate.length > 0) {
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: customWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create custom FIELD widgets:\n${JSON.stringify(result, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create custom FIELD widgets for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created ${totalWidgets} FIELD widget(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-485
@@ -1,485 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { FeatureFlagKey, ViewType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
|
||||
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
|
||||
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:backfill-page-layouts',
|
||||
description:
|
||||
'Backfill RECORD_PAGE page layouts for legacy workspaces and enable the IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED feature flag',
|
||||
})
|
||||
export class BackfillPageLayoutsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Starting backfill of page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const isAlreadyEnabled = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isAlreadyEnabled) {
|
||||
this.logger.log(
|
||||
`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED already enabled for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
await this.backfillStandardObjectPageLayouts({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
isDryRun,
|
||||
});
|
||||
|
||||
await this.backfillCustomObjectPageLayouts({
|
||||
workspaceId,
|
||||
workspaceCustomFlatApplication,
|
||||
isDryRun,
|
||||
});
|
||||
|
||||
if (!isDryRun) {
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED],
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
isDryRun
|
||||
? `[DRY RUN] Would create page layouts and enable IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`
|
||||
: `Successfully created page layouts and enabled IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async backfillStandardObjectPageLayouts({
|
||||
workspaceId,
|
||||
twentyStandardFlatApplication,
|
||||
isDryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
twentyStandardFlatApplication: FlatApplication;
|
||||
isDryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
shouldIncludeRecordPageLayouts: true,
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const {
|
||||
flatPageLayoutMaps: existingFlatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps: existingFlatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatViewFieldGroupMaps: existingFlatViewFieldGroupMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFieldMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
]);
|
||||
|
||||
const recordPageLayoutUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const pageLayoutsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((pageLayout) => {
|
||||
if (pageLayout.type !== PageLayoutType.RECORD_PAGE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
recordPageLayoutUniversalIdentifiers.add(
|
||||
pageLayout.universalIdentifier,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(
|
||||
existingFlatPageLayoutMaps.byUniversalIdentifier[
|
||||
pageLayout.universalIdentifier
|
||||
],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const tabUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const pageLayoutTabsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutTabMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((tab) => {
|
||||
if (
|
||||
!recordPageLayoutUniversalIdentifiers.has(
|
||||
tab.pageLayoutUniversalIdentifier,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tabUniversalIdentifiers.add(tab.universalIdentifier);
|
||||
|
||||
if (
|
||||
isDefined(
|
||||
existingFlatPageLayoutTabMaps.byUniversalIdentifier[
|
||||
tab.universalIdentifier
|
||||
],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const pageLayoutWidgetsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(widget) =>
|
||||
tabUniversalIdentifiers.has(
|
||||
widget.pageLayoutTabUniversalIdentifier,
|
||||
) &&
|
||||
!isDefined(
|
||||
existingFlatPageLayoutWidgetMaps.byUniversalIdentifier[
|
||||
widget.universalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const viewUniversalIdentifiers = new Set<string>();
|
||||
|
||||
const viewsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatViewMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((view) => {
|
||||
if (view.type !== ViewType.FIELDS_WIDGET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(
|
||||
existingFlatViewMaps.byUniversalIdentifier[
|
||||
view.universalIdentifier
|
||||
],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
viewUniversalIdentifiers.add(view.universalIdentifier);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const viewFieldsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatViewFieldMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(viewField) =>
|
||||
viewUniversalIdentifiers.has(viewField.viewUniversalIdentifier) &&
|
||||
!isDefined(
|
||||
existingFlatViewFieldMaps.byUniversalIdentifier[
|
||||
viewField.universalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
const viewFieldGroupsToCreate = Object.values(
|
||||
standardAllFlatEntityMaps.flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(viewFieldGroup) =>
|
||||
viewUniversalIdentifiers.has(
|
||||
viewFieldGroup.viewUniversalIdentifier,
|
||||
) &&
|
||||
!isDefined(
|
||||
existingFlatViewFieldGroupMaps.byUniversalIdentifier[
|
||||
viewFieldGroup.universalIdentifier
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Found standard entities to create for workspace ${workspaceId}: ` +
|
||||
`${pageLayoutsToCreate.length} page layout(s), ` +
|
||||
`${pageLayoutTabsToCreate.length} tab(s), ` +
|
||||
`${pageLayoutWidgetsToCreate.length} widget(s), ` +
|
||||
`${viewsToCreate.length} view(s), ` +
|
||||
`${viewFieldsToCreate.length} view field(s), ` +
|
||||
`${viewFieldGroupsToCreate.length} view field group(s)`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: pageLayoutsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: pageLayoutTabsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: pageLayoutWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: viewsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: viewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewFieldGroup: {
|
||||
flatEntityToCreate: viewFieldGroupsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
twentyStandardFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create standard page layouts:\n${JSON.stringify(validateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create standard page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async backfillCustomObjectPageLayouts({
|
||||
workspaceId,
|
||||
workspaceCustomFlatApplication,
|
||||
isDryRun,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workspaceCustomFlatApplication: FlatApplication;
|
||||
isDryRun: boolean;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatPageLayoutMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutMaps',
|
||||
]);
|
||||
|
||||
const existingPageLayouts = Object.values(
|
||||
flatPageLayoutMaps.byUniversalIdentifier,
|
||||
).filter(isDefined);
|
||||
|
||||
const objectIdsWithRecordPageLayout = new Set(
|
||||
existingPageLayouts
|
||||
.filter(
|
||||
(layout: FlatPageLayout) =>
|
||||
layout.type === PageLayoutType.RECORD_PAGE &&
|
||||
isDefined(layout.objectMetadataId),
|
||||
)
|
||||
.map((layout: FlatPageLayout) => layout.objectMetadataId),
|
||||
);
|
||||
|
||||
const customObjectsWithoutPageLayout = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(objectMetadata) =>
|
||||
objectMetadata.isCustom &&
|
||||
!objectMetadata.isRemote &&
|
||||
!objectIdsWithRecordPageLayout.has(objectMetadata.id),
|
||||
);
|
||||
|
||||
if (customObjectsWithoutPageLayout.length === 0) {
|
||||
this.logger.log(
|
||||
`No custom objects without page layouts found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const allCustomPageLayoutsToCreate: FlatPageLayout[] = [];
|
||||
const allCustomPageLayoutTabsToCreate: FlatPageLayoutTab[] = [];
|
||||
const allCustomPageLayoutWidgetsToCreate: FlatPageLayoutWidget[] = [];
|
||||
const allCustomViewsToCreate: (UniversalFlatView & { id: string })[] = [];
|
||||
const allCustomViewFieldsToCreate: UniversalFlatViewField[] = [];
|
||||
|
||||
for (const customObject of customObjectsWithoutPageLayout) {
|
||||
const flatRecordPageFieldsView = computeFlatRecordPageFieldsViewToCreate({
|
||||
objectMetadata: customObject,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
});
|
||||
|
||||
const objectFieldMetadatas = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((field) => field.objectMetadataId === customObject.id);
|
||||
|
||||
const viewFields = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: objectFieldMetadatas,
|
||||
viewUniversalIdentifier: flatRecordPageFieldsView.universalIdentifier,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
customObject.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
});
|
||||
|
||||
const { pageLayouts, pageLayoutTabs, pageLayoutWidgets } =
|
||||
computeFlatDefaultRecordPageLayoutToCreate({
|
||||
objectMetadata: customObject,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
recordPageFieldsView: flatRecordPageFieldsView,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
allCustomPageLayoutsToCreate.push(...pageLayouts);
|
||||
allCustomPageLayoutTabsToCreate.push(...pageLayoutTabs);
|
||||
allCustomPageLayoutWidgetsToCreate.push(...pageLayoutWidgets);
|
||||
allCustomViewsToCreate.push(flatRecordPageFieldsView);
|
||||
allCustomViewFieldsToCreate.push(...viewFields);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Found custom entities to create for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}: ` +
|
||||
`${allCustomPageLayoutsToCreate.length} page layout(s), ` +
|
||||
`${allCustomPageLayoutTabsToCreate.length} tab(s), ` +
|
||||
`${allCustomPageLayoutWidgetsToCreate.length} widget(s), ` +
|
||||
`${allCustomViewsToCreate.length} view(s), ` +
|
||||
`${allCustomViewFieldsToCreate.length} view field(s)`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customValidateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayout: {
|
||||
flatEntityToCreate: allCustomPageLayoutsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: allCustomPageLayoutTabsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: allCustomPageLayoutWidgetsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: allCustomViewsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: allCustomViewFieldsToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (customValidateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create custom object page layouts:\n${JSON.stringify(customValidateAndBuildResult, null, 2)}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create custom object page layouts for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully created page layouts for ${customObjectsWithoutPageLayout.length} custom object(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-20:generate-application-sdk-clients',
|
||||
description:
|
||||
'Generate SDK client archives for all existing applications so drivers do not crash with ARCHIVE_NOT_FOUND',
|
||||
})
|
||||
export class GenerateApplicationSdkClientsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
private readonly sdkClientGenerationService: SdkClientGenerationService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const dryRun = options.dryRun ?? false;
|
||||
|
||||
const applications = await this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${applications.length} application(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
for (const application of applications) {
|
||||
if (dryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would generate SDK client for application ${application.universalIdentifier} (${application.id})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sdkClientGenerationService.generateSdkClientForApplication({
|
||||
workspaceId,
|
||||
applicationId: application.id,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to generate SDK client for application ${application.universalIdentifier} (${application.id}): ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-13
@@ -2,12 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
|
||||
import { BackfillFieldWidgetsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-field-widgets.command';
|
||||
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
|
||||
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
|
||||
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
|
||||
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
|
||||
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
|
||||
import { GenerateApplicationSdkClientsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-generate-application-sdk-clients.command';
|
||||
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
|
||||
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
|
||||
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
@@ -18,10 +16,8 @@ import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-vers
|
||||
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
|
||||
import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
@@ -41,7 +37,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
ApplicationEntity,
|
||||
ConnectedAccountEntity,
|
||||
MessageChannelEntity,
|
||||
CalendarChannelEntity,
|
||||
@@ -58,7 +53,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
ApplicationRegistrationModule,
|
||||
WorkspaceMigrationModule,
|
||||
FeatureFlagModule,
|
||||
SdkClientModule,
|
||||
WorkflowCommonModule,
|
||||
],
|
||||
providers: [
|
||||
@@ -67,12 +61,10 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
IdentifyObjectPermissionMetadataCommand,
|
||||
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillFieldWidgetsCommand,
|
||||
BackfillNavigationMenuItemTypeCommand,
|
||||
BackfillPageLayoutsCommand,
|
||||
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
BackfillSelectFieldOptionIdsCommand,
|
||||
DeleteOrphanNavigationMenuItemsCommand,
|
||||
GenerateApplicationSdkClientsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
@@ -85,12 +77,10 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
IdentifyObjectPermissionMetadataCommand,
|
||||
MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
|
||||
BackfillCommandMenuItemsCommand,
|
||||
BackfillFieldWidgetsCommand,
|
||||
BackfillNavigationMenuItemTypeCommand,
|
||||
BackfillPageLayoutsCommand,
|
||||
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
BackfillSelectFieldOptionIdsCommand,
|
||||
DeleteOrphanNavigationMenuItemsCommand,
|
||||
GenerateApplicationSdkClientsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
|
||||
+4
-10
@@ -34,11 +34,10 @@ import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgra
|
||||
import { FixInvalidStandardUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-fix-invalid-standard-universal-identifiers.command';
|
||||
import { SeedServerIdCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-seed-server-id.command';
|
||||
import { BackfillCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-command-menu-items.command';
|
||||
import { BackfillFieldWidgetsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-field-widgets.command';
|
||||
import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-navigation-menu-item-type.command';
|
||||
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
|
||||
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts.command';
|
||||
import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-page-layouts-and-fields-widget-view-fields.command';
|
||||
import { BackfillSelectFieldOptionIdsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-backfill-select-field-option-ids.command';
|
||||
import { DeleteOrphanNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-delete-orphan-navigation-menu-items.command';
|
||||
import { IdentifyObjectPermissionMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-object-permission-metadata.command';
|
||||
import { IdentifyPermissionFlagMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-identify-permission-flag-metadata.command';
|
||||
import { MakeObjectPermissionUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-object-permission-universal-identifier-and-application-id-not-nullable-migration.command';
|
||||
@@ -46,7 +45,6 @@ import { MakePermissionFlagUniversalIdentifierAndApplicationIdNotNullableMigrati
|
||||
import { MakeWorkflowSearchableCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-make-workflow-searchable.command';
|
||||
import { MigrateMessagingInfrastructureToMetadataCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-messaging-infrastructure-to-metadata.command';
|
||||
import { MigrateRichTextToTextCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-migrate-rich-text-to-text.command';
|
||||
import { GenerateApplicationSdkClientsCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-generate-application-sdk-clients.command';
|
||||
import { SeedCliApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-seed-cli-application-registration.command';
|
||||
import { UpdateStandardIndexViewNamesCommand } from 'src/database/commands/upgrade-version-command/1-20/1-20-update-standard-index-view-names.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
@@ -106,12 +104,10 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly backfillNavigationMenuItemTypeCommand: BackfillNavigationMenuItemTypeCommand,
|
||||
protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand,
|
||||
protected readonly deleteOrphanNavigationMenuItemsCommand: DeleteOrphanNavigationMenuItemsCommand,
|
||||
protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand,
|
||||
protected readonly generateApplicationSdkClientsCommand: GenerateApplicationSdkClientsCommand,
|
||||
protected readonly backfillPageLayoutsAndFieldsWidgetViewFieldsCommand: BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
protected readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand,
|
||||
protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand,
|
||||
protected readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand,
|
||||
protected readonly backfillFieldWidgetsCommand: BackfillFieldWidgetsCommand,
|
||||
protected readonly backfillSelectFieldOptionIdsCommand: BackfillSelectFieldOptionIdsCommand,
|
||||
protected readonly updateStandardIndexViewNamesCommand: UpdateStandardIndexViewNamesCommand,
|
||||
protected readonly makeWorkflowSearchableCommand: MakeWorkflowSearchableCommand,
|
||||
@@ -172,14 +168,12 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.migrateRichTextToTextCommand,
|
||||
this.deleteOrphanNavigationMenuItemsCommand,
|
||||
this.backfillCommandMenuItemsCommand,
|
||||
this.backfillPageLayoutsCommand,
|
||||
this.backfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
this.seedCliApplicationRegistrationCommand,
|
||||
this.migrateMessagingInfrastructureToMetadataCommand,
|
||||
this.backfillFieldWidgetsCommand,
|
||||
this.backfillSelectFieldOptionIdsCommand,
|
||||
this.updateStandardIndexViewNamesCommand,
|
||||
this.makeWorkflowSearchableCommand,
|
||||
this.generateApplicationSdkClientsCommand,
|
||||
];
|
||||
|
||||
this.allCommands = {
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddActiveStreamIdToAgentChatThread1774003611071
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddActiveStreamIdToAgentChatThread1774003611071';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agentChatThread" ADD "activeStreamId" character varying`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."agentChatThread" DROP COLUMN "activeStreamId"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
@@ -721,6 +721,7 @@ export class LambdaDriver implements LogicFunctionDriver {
|
||||
const sdkArchiveBuffer =
|
||||
await this.sdkClientArchiveService.downloadArchiveBuffer({
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -113,6 +113,7 @@ export class LocalDriver implements LogicFunctionDriver {
|
||||
|
||||
await this.sdkClientArchiveService.downloadAndExtractToPackage({
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier,
|
||||
targetPackagePath: sdkPackagePath,
|
||||
});
|
||||
|
||||
+17
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import crypto from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -40,6 +41,7 @@ type UpdateSourceFilesParams = Omit<
|
||||
'builtHandlerPath'
|
||||
> & {
|
||||
sourceHandlerCode: string;
|
||||
queryRunner?: QueryRunner;
|
||||
};
|
||||
|
||||
type GetSourceCodeParams = Identifier & {
|
||||
@@ -128,6 +130,7 @@ export class LogicFunctionResourceService {
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
sourceHandlerCode,
|
||||
queryRunner,
|
||||
}: UpdateSourceFilesParams): Promise<void> {
|
||||
await this.fileStorageService.writeFile({
|
||||
workspaceId,
|
||||
@@ -137,6 +140,20 @@ export class LogicFunctionResourceService {
|
||||
sourceFile: sourceHandlerCode,
|
||||
settings: { isTemporaryFile: false, toDelete: false },
|
||||
mimeType: 'application/typescript',
|
||||
queryRunner,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSourceFile({
|
||||
sourceHandlerPath,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
}: GetSourceCodeParams): Promise<void> {
|
||||
await this.fileStorageService.delete({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Source,
|
||||
resourcePath: sourceHandlerPath,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -59,6 +59,7 @@ export class SdkClientController {
|
||||
const fileBuffer =
|
||||
await this.sdkClientArchiveService.getClientModuleFromArchive({
|
||||
workspaceId: workspace.id,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
moduleName,
|
||||
});
|
||||
|
||||
+45
-41
@@ -1,11 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { createWriteStream } from 'fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { type Readable } from 'stream';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -16,12 +12,12 @@ import {
|
||||
FileStorageException,
|
||||
FileStorageExceptionCode,
|
||||
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
|
||||
import { type SdkModuleName } from 'src/engine/core-modules/sdk-client/constants/allowed-sdk-modules';
|
||||
import {
|
||||
SdkClientException,
|
||||
SdkClientExceptionCode,
|
||||
} from 'src/engine/core-modules/sdk-client/exceptions/sdk-client.exception';
|
||||
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
@@ -29,75 +25,74 @@ const SDK_CLIENT_ARCHIVE_NAME = 'twenty-client-sdk.zip';
|
||||
|
||||
@Injectable()
|
||||
export class SdkClientArchiveService {
|
||||
private readonly logger = new Logger(SdkClientArchiveService.name);
|
||||
|
||||
constructor(
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly sdkClientGenerationService: SdkClientGenerationService,
|
||||
) {}
|
||||
|
||||
async downloadAndExtractToPackage({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
targetPackagePath,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
targetPackagePath: string;
|
||||
}): Promise<void> {
|
||||
const temporaryDirManager = new TemporaryDirManager();
|
||||
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
try {
|
||||
const { sourceTemporaryDir } = await temporaryDirManager.init();
|
||||
const archivePath = join(sourceTemporaryDir, SDK_CLIENT_ARCHIVE_NAME);
|
||||
await fs.rm(targetPackagePath, { recursive: true, force: true });
|
||||
await fs.mkdir(targetPackagePath, { recursive: true });
|
||||
|
||||
const archiveStream = await this.readArchiveStream({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
const { default: unzipper } = await import('unzipper');
|
||||
const directory = await unzipper.Open.buffer(archiveBuffer);
|
||||
|
||||
await pipeline(archiveStream, createWriteStream(archivePath));
|
||||
|
||||
await fs.rm(targetPackagePath, { recursive: true, force: true });
|
||||
await fs.mkdir(targetPackagePath, { recursive: true });
|
||||
|
||||
const { default: unzipper } = await import('unzipper');
|
||||
const directory = await unzipper.Open.file(archivePath);
|
||||
|
||||
await directory.extract({ path: targetPackagePath });
|
||||
} finally {
|
||||
await temporaryDirManager.clean();
|
||||
}
|
||||
await directory.extract({ path: targetPackagePath });
|
||||
}
|
||||
|
||||
async downloadArchiveBuffer({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<Buffer> {
|
||||
const archiveStream = await this.readArchiveStream({
|
||||
return this.downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
return streamToBuffer(archiveStream);
|
||||
}
|
||||
|
||||
async getClientModuleFromArchive({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
moduleName,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
moduleName: SdkModuleName;
|
||||
}): Promise<Buffer> {
|
||||
const filePath = `dist/${moduleName}.mjs`;
|
||||
|
||||
const archiveBuffer = await this.downloadArchiveBuffer({
|
||||
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
@@ -135,32 +130,41 @@ export class SdkClientArchiveService {
|
||||
]);
|
||||
}
|
||||
|
||||
private async readArchiveStream({
|
||||
private async downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<Readable> {
|
||||
}): Promise<Buffer> {
|
||||
try {
|
||||
return await this.fileStorageService.readFile({
|
||||
const stream = await this.fileStorageService.readFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.GeneratedSdkClient,
|
||||
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
|
||||
});
|
||||
|
||||
return await streamToBuffer(stream);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FileStorageException &&
|
||||
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
!(error instanceof FileStorageException) ||
|
||||
error.code !== FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
) {
|
||||
throw new SdkClientException(
|
||||
`SDK client archive "${SDK_CLIENT_ARCHIVE_NAME}" not found for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}".`,
|
||||
SdkClientExceptionCode.ARCHIVE_NOT_FOUND,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
`SDK client archive missing for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}", generating on-the-fly`,
|
||||
);
|
||||
|
||||
return this.sdkClientGenerationService.generateSdkClientForApplication({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+10
-4
@@ -44,13 +44,13 @@ export class SdkClientGenerationService {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
}): Promise<Buffer> {
|
||||
const graphqlSchema = await this.workspaceSchemaFactory.createGraphQLSchema(
|
||||
{ id: workspaceId } as WorkspaceEntity,
|
||||
applicationId,
|
||||
);
|
||||
|
||||
await this.generateAndStore({
|
||||
const archiveBuffer = await this.generateAndStore({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
@@ -60,6 +60,8 @@ export class SdkClientGenerationService {
|
||||
this.logger.log(
|
||||
`Generated SDK client for application ${applicationUniversalIdentifier}`,
|
||||
);
|
||||
|
||||
return archiveBuffer;
|
||||
}
|
||||
|
||||
private async generateAndStore({
|
||||
@@ -72,7 +74,7 @@ export class SdkClientGenerationService {
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
schema: string;
|
||||
}): Promise<void> {
|
||||
}): Promise<Buffer> {
|
||||
const temporaryDirManager = new TemporaryDirManager();
|
||||
|
||||
try {
|
||||
@@ -101,12 +103,14 @@ export class SdkClientGenerationService {
|
||||
|
||||
await createZipFile(tempPackageRoot, archivePath);
|
||||
|
||||
const archiveBuffer = await fs.readFile(archivePath);
|
||||
|
||||
await this.fileStorageService.writeFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.GeneratedSdkClient,
|
||||
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
|
||||
sourceFile: await fs.readFile(archivePath),
|
||||
sourceFile: archiveBuffer,
|
||||
mimeType: 'application/zip',
|
||||
settings: { isTemporaryFile: false, toDelete: false },
|
||||
});
|
||||
@@ -119,6 +123,8 @@ export class SdkClientGenerationService {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
return archiveBuffer;
|
||||
} catch (error) {
|
||||
throw new SdkClientException(
|
||||
`Failed to generate SDK client for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
|
||||
+2
@@ -33,6 +33,7 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
||||
|
||||
describe('WorkspaceService', () => {
|
||||
@@ -119,6 +120,7 @@ describe('WorkspaceService', () => {
|
||||
PermissionsService,
|
||||
FileCorePictureService,
|
||||
AiModelRegistryService,
|
||||
PrefillLogicFunctionService,
|
||||
].map((service) => ({
|
||||
provide: service,
|
||||
useValue: {},
|
||||
|
||||
+19
-6
@@ -53,12 +53,14 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
|
||||
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-dashboards';
|
||||
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-opportunities';
|
||||
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
|
||||
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
|
||||
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-dashboards.util';
|
||||
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-opportunities.util';
|
||||
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-people.util';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util';
|
||||
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
||||
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-migration/constant/default-feature-flags';
|
||||
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
|
||||
@@ -111,6 +113,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly dnsManagerService: DnsManagerService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly subdomainManagerService: SubdomainManagerService,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
@@ -681,6 +684,14 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
},
|
||||
);
|
||||
|
||||
await this.prefillLogicFunctionService.ensureSeeded({
|
||||
workspaceId,
|
||||
definitions:
|
||||
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
|
||||
workspaceId,
|
||||
),
|
||||
});
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
@@ -694,6 +705,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
|
||||
await prefillWorkflows(
|
||||
queryRunner.manager,
|
||||
workspaceId,
|
||||
schemaName,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
@@ -720,6 +732,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
|
||||
@@ -41,12 +41,14 @@ import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||
import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeORMModule,
|
||||
TypeOrmModule.forFeature([BillingSubscriptionEntity, WorkspaceEntity]),
|
||||
MetricsModule,
|
||||
StandardObjectsPrefillModule,
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
AuditModule,
|
||||
@@ -79,6 +81,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
ApplicationModule,
|
||||
EnterpriseModule,
|
||||
StandardObjectsPrefillModule,
|
||||
],
|
||||
services: [WorkspaceService],
|
||||
resolvers: workspaceAutoResolverOpts,
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { type ToolUIPart } from 'ai';
|
||||
import { type ExtendedUIMessage, type ExtendedUIMessagePart } from 'twenty-shared/ai';
|
||||
|
||||
import { type AgentMessagePartEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message-part.entity';
|
||||
import { type AgentMessageEntity } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
|
||||
|
||||
const mapAgentMessagePartEntityToUIMessagePart = (
|
||||
part: AgentMessagePartEntity,
|
||||
): ExtendedUIMessagePart | null => {
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
return { type: 'text', text: part.textContent ?? '' };
|
||||
case 'reasoning':
|
||||
return { type: 'reasoning', text: part.reasoningContent ?? '', state: 'done' as const };
|
||||
case 'step-start':
|
||||
return { type: 'step-start' };
|
||||
case 'source-url':
|
||||
return {
|
||||
type: 'source-url',
|
||||
sourceId: part.sourceUrlSourceId ?? '',
|
||||
url: part.sourceUrlUrl ?? '',
|
||||
title: part.sourceUrlTitle ?? undefined,
|
||||
providerMetadata: part.providerMetadata ?? undefined,
|
||||
};
|
||||
default: {
|
||||
if (part.type.includes('tool-') && part.toolCallId) {
|
||||
return {
|
||||
type: part.type as `tool-${string}`,
|
||||
toolCallId: part.toolCallId,
|
||||
input: part.toolInput ?? {},
|
||||
output: part.toolOutput ?? undefined,
|
||||
errorText: part.errorMessage ?? undefined,
|
||||
state: part.state ?? undefined,
|
||||
} as ToolUIPart;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const mapAgentMessageEntitiesToUIMessages = (
|
||||
messageEntities: AgentMessageEntity[],
|
||||
): ExtendedUIMessage[] => {
|
||||
return messageEntities.map((message) => ({
|
||||
id: message.id,
|
||||
role: message.role as ExtendedUIMessage['role'],
|
||||
parts: (message.parts ?? [])
|
||||
.sort((partA, partB) => partA.orderIndex - partB.orderIndex)
|
||||
.map(mapAgentMessagePartEntityToUIMessagePart)
|
||||
.filter((part): part is ExtendedUIMessagePart => part !== null),
|
||||
metadata: { createdAt: message.createdAt.toISOString() },
|
||||
}));
|
||||
};
|
||||
@@ -36,6 +36,7 @@ import { AgentChatController } from './controllers/agent-chat.controller';
|
||||
import { AgentChatThreadDTO } from './dtos/agent-chat-thread.dto';
|
||||
import { AgentChatThreadEntity } from './entities/agent-chat-thread.entity';
|
||||
import { AgentChatResolver } from './resolvers/agent-chat.resolver';
|
||||
import { AgentChatResumableStreamService } from './services/agent-chat-resumable-stream.service';
|
||||
import { AgentChatStreamingService } from './services/agent-chat-streaming.service';
|
||||
import { AgentChatService } from './services/agent-chat.service';
|
||||
import { AgentTitleGenerationService } from './services/agent-title-generation.service';
|
||||
@@ -99,6 +100,7 @@ import { SystemPromptBuilderService } from './services/system-prompt-builder.ser
|
||||
controllers: [AgentChatController],
|
||||
providers: [
|
||||
AgentChatResolver,
|
||||
AgentChatResumableStreamService,
|
||||
AgentChatService,
|
||||
AgentChatStreamingService,
|
||||
AgentTitleGenerationService,
|
||||
|
||||
+67
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Logger,
|
||||
Param,
|
||||
Post,
|
||||
Res,
|
||||
UseFilters,
|
||||
@@ -9,8 +12,12 @@ import {
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { UI_MESSAGE_STREAM_HEADERS } from 'ai';
|
||||
import type { Response } from 'express';
|
||||
import type { ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { Repository } from 'typeorm';
|
||||
|
||||
import { RestApiExceptionFilter } from 'src/engine/api/rest/rest-api-exception.filter';
|
||||
import {
|
||||
@@ -33,6 +40,8 @@ import {
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AgentRestApiExceptionFilter } from 'src/engine/metadata-modules/ai/ai-agent/filters/agent-api-exception.filter';
|
||||
import type { BrowsingContextType } from 'src/engine/metadata-modules/ai/ai-agent/types/browsingContext.type';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
import { AgentChatResumableStreamService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat-resumable-stream.service';
|
||||
import { AgentChatStreamingService } from 'src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
|
||||
@@ -44,11 +53,16 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
|
||||
BillingRestApiExceptionFilter,
|
||||
)
|
||||
export class AgentChatController {
|
||||
private readonly logger = new Logger(AgentChatController.name);
|
||||
|
||||
constructor(
|
||||
private readonly agentStreamingService: AgentChatStreamingService,
|
||||
private readonly resumableStreamService: AgentChatResumableStreamService,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
@InjectRepository(AgentChatThreadEntity)
|
||||
private readonly threadRepository: Repository<AgentChatThreadEntity>,
|
||||
) {}
|
||||
|
||||
@Post('stream')
|
||||
@@ -103,4 +117,57 @@ export class AgentChatController {
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':threadId/stream')
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.AI))
|
||||
async resumeAgentChatStream(
|
||||
@Param('threadId') threadId: string,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@Res() response: Response,
|
||||
) {
|
||||
console.log('Received request to resume stream for threadId:', threadId);
|
||||
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
if (!uuidRegex.test(threadId)) {
|
||||
response.status(204).end();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const thread = await this.threadRepository.findOne({
|
||||
where: { id: threadId, userWorkspaceId },
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`resume requested — thread found: ${!!thread} | activeStreamId: ${thread?.activeStreamId}`,
|
||||
);
|
||||
|
||||
if (!isDefined(thread) || !isDefined(thread.activeStreamId)) {
|
||||
response.status(204).end();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const resumedNodeReadable =
|
||||
await this.resumableStreamService.resumeExistingStreamAsNodeReadable(
|
||||
thread.activeStreamId,
|
||||
);
|
||||
|
||||
if (!isDefined(resumedNodeReadable)) {
|
||||
this.logger.log(
|
||||
'resumeExistingStream returned null — Redis stream already done or expired',
|
||||
);
|
||||
response.status(204).end();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log('resuming stream from Redis');
|
||||
|
||||
response.writeHead(200, UI_MESSAGE_STREAM_HEADERS);
|
||||
|
||||
resumedNodeReadable.pipe(response);
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -51,6 +51,9 @@ export class AgentChatThreadEntity {
|
||||
@Column({ type: 'bigint', default: 0 })
|
||||
totalOutputCredits: number;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
activeStreamId: string | null;
|
||||
|
||||
@OneToMany(() => AgentTurnEntity, (turn) => turn.thread)
|
||||
turns: EntityRelation<AgentTurnEntity[]>;
|
||||
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||
import { Readable } from 'stream';
|
||||
import { type ReadableStream as NodeWebReadableStream } from 'stream/web';
|
||||
|
||||
import type { Redis } from 'ioredis';
|
||||
import { createResumableStreamContext } from 'resumable-stream/ioredis';
|
||||
|
||||
import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentChatResumableStreamService implements OnModuleDestroy {
|
||||
private streamContext: ReturnType<typeof createResumableStreamContext>;
|
||||
private publisher: Redis;
|
||||
private subscriber: Redis;
|
||||
|
||||
constructor(private readonly redisClientService: RedisClientService) {
|
||||
const redisClient = this.redisClientService.getClient();
|
||||
|
||||
this.publisher = redisClient.duplicate();
|
||||
this.subscriber = redisClient.duplicate();
|
||||
|
||||
this.streamContext = createResumableStreamContext({
|
||||
waitUntil: (callback) => {
|
||||
void Promise.resolve(callback);
|
||||
},
|
||||
publisher: this.publisher,
|
||||
subscriber: this.subscriber,
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.publisher.quit();
|
||||
await this.subscriber.quit();
|
||||
}
|
||||
|
||||
async createResumableStream(
|
||||
streamId: string,
|
||||
streamFactory: () => ReadableStream<string>,
|
||||
) {
|
||||
const resumableStream = await this.streamContext.createNewResumableStream(
|
||||
streamId,
|
||||
streamFactory,
|
||||
);
|
||||
|
||||
if (!resumableStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = resumableStream.getReader();
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
while (true) {
|
||||
const { done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore errors from background stream consumption
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
async resumeExistingStreamAsNodeReadable(
|
||||
streamId: string,
|
||||
): Promise<Readable | null> {
|
||||
const webStream = await this.streamContext.resumeExistingStream(streamId);
|
||||
|
||||
if (!webStream) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Readable.fromWeb(webStream as NodeWebReadableStream);
|
||||
}
|
||||
}
|
||||
+50
-11
@@ -1,7 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { createUIMessageStream, pipeUIMessageStreamToResponse } from 'ai';
|
||||
import {
|
||||
createUIMessageStream,
|
||||
generateId,
|
||||
pipeUIMessageStreamToResponse,
|
||||
} from 'ai';
|
||||
import { type Response } from 'express';
|
||||
import {
|
||||
type CodeExecutionData,
|
||||
@@ -11,6 +15,7 @@ import { type Repository } from 'typeorm';
|
||||
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentMessageRole } from 'src/engine/metadata-modules/ai/ai-agent-execution/entities/agent-message.entity';
|
||||
import { mapAgentMessageEntitiesToUIMessages } from 'src/engine/metadata-modules/ai/ai-agent-execution/utils/mapAgentMessageEntitiesToUIMessages';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
@@ -19,10 +24,11 @@ import { type BrowsingContextType } from 'src/engine/metadata-modules/ai/ai-agen
|
||||
import { computeCostBreakdown } from 'src/engine/metadata-modules/ai/ai-billing/utils/compute-cost-breakdown.util';
|
||||
import { convertDollarsToBillingCredits } from 'src/engine/metadata-modules/ai/ai-billing/utils/convert-dollars-to-billing-credits.util';
|
||||
import { extractCacheCreationTokens } from 'src/engine/metadata-modules/ai/ai-billing/utils/extract-cache-creation-tokens.util';
|
||||
import { toDisplayCredits } from 'src/engine/core-modules/usage/utils/to-display-credits.util';
|
||||
import { type AIModelConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-config.type';
|
||||
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/ai/ai-chat/entities/agent-chat-thread.entity';
|
||||
|
||||
import { toDisplayCredits } from 'src/engine/core-modules/usage/utils/to-display-credits.util';
|
||||
import { AIModelConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-config.type';
|
||||
import { AgentChatResumableStreamService } from './agent-chat-resumable-stream.service';
|
||||
import { AgentChatService } from './agent-chat.service';
|
||||
import { ChatExecutionService } from './chat-execution.service';
|
||||
|
||||
@@ -38,11 +44,14 @@ export type StreamAgentChatOptions = {
|
||||
|
||||
@Injectable()
|
||||
export class AgentChatStreamingService {
|
||||
private readonly logger = new Logger(AgentChatStreamingService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(AgentChatThreadEntity)
|
||||
private readonly threadRepository: Repository<AgentChatThreadEntity>,
|
||||
private readonly agentChatService: AgentChatService,
|
||||
private readonly chatExecutionService: ChatExecutionService,
|
||||
private readonly resumableStreamService: AgentChatResumableStreamService,
|
||||
) {}
|
||||
|
||||
async streamAgentChat({
|
||||
@@ -68,12 +77,25 @@ export class AgentChatStreamingService {
|
||||
);
|
||||
}
|
||||
|
||||
// Fire user-message save without awaiting to avoid delaying time-to-first-letter.
|
||||
// The promise is awaited inside onFinish where we need the turnId.
|
||||
const lastUserMessage = messages[messages.length - 1];
|
||||
const lastUserText =
|
||||
lastUserMessage?.parts.find((part) => part.type === 'text')?.text ?? '';
|
||||
|
||||
const historicalMessageEntities =
|
||||
await this.agentChatService.getMessagesForThread(
|
||||
thread.id,
|
||||
userWorkspaceId,
|
||||
);
|
||||
|
||||
const historicalUIMessages = mapAgentMessageEntitiesToUIMessages(
|
||||
historicalMessageEntities,
|
||||
);
|
||||
|
||||
const messagesWithHistory: ExtendedUIMessage[] = [
|
||||
...historicalUIMessages,
|
||||
...(lastUserMessage ? [lastUserMessage] : []),
|
||||
];
|
||||
|
||||
const userMessagePromise = this.agentChatService.addMessage({
|
||||
threadId: thread.id,
|
||||
uiMessage: {
|
||||
@@ -113,7 +135,7 @@ export class AgentChatStreamingService {
|
||||
await this.chatExecutionService.streamChat({
|
||||
workspace,
|
||||
userWorkspaceId,
|
||||
messages,
|
||||
messages: messagesWithHistory,
|
||||
browsingContext,
|
||||
onCodeExecutionUpdate,
|
||||
modelId,
|
||||
@@ -212,6 +234,7 @@ export class AgentChatStreamingService {
|
||||
`"totalOutputCredits" + ${streamUsage.outputCredits}`,
|
||||
contextWindowTokens: modelConfig.contextWindowTokens,
|
||||
conversationSize: lastStepConversationSize,
|
||||
activeStreamId: null,
|
||||
});
|
||||
|
||||
const generatedTitle = await titlePromise;
|
||||
@@ -233,10 +256,26 @@ export class AgentChatStreamingService {
|
||||
pipeUIMessageStreamToResponse({
|
||||
stream: uiStream,
|
||||
response,
|
||||
// Consume the stream independently so onFinish fires even if
|
||||
// the client disconnects (e.g., page refresh mid-stream)
|
||||
consumeSseStream: ({ stream }) => {
|
||||
stream.pipeTo(new WritableStream()).catch(() => {});
|
||||
consumeSseStream: async ({ stream }) => {
|
||||
try {
|
||||
const streamId = generateId();
|
||||
|
||||
this.logger.log(`consumeSseStream called, streamId: ${streamId}`);
|
||||
await this.resumableStreamService.createResumableStream(
|
||||
streamId,
|
||||
() => stream,
|
||||
);
|
||||
this.logger.log(
|
||||
'createResumableStream done, writing activeStreamId to DB',
|
||||
);
|
||||
|
||||
await this.threadRepository.update(thread.id, {
|
||||
activeStreamId: streamId,
|
||||
});
|
||||
this.logger.log(`activeStreamId written to DB: ${streamId}`);
|
||||
} catch (error) {
|
||||
this.logger.error('consumeSseStream error:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
+953
-953
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -118,11 +118,11 @@ export class MessageFolderDataAccessService {
|
||||
): Promise<void> {
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
await workspaceRepository.save(data);
|
||||
const savedData = await workspaceRepository.save(data);
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
const coreData = await this.toCore(workspaceId, data);
|
||||
const coreData = await this.toCore(workspaceId, savedData);
|
||||
|
||||
await this.coreRepository.save(
|
||||
coreData as unknown as MessageFolderEntity,
|
||||
|
||||
+1
@@ -455,6 +455,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier:
|
||||
flatRecordPageFieldsViewToCreate.universalIdentifier,
|
||||
excludeLabelIdentifier: true,
|
||||
});
|
||||
|
||||
flatDefaultRecordPageLayoutsToCreate =
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
|
||||
import { computeFlatViewFieldsToCreate } from '../compute-flat-view-fields-to-create.util';
|
||||
|
||||
const makeFieldMetadata = (
|
||||
overrides: Partial<UniversalFlatFieldMetadata> & {
|
||||
name: string;
|
||||
type: FieldMetadataType;
|
||||
},
|
||||
): UniversalFlatFieldMetadata => {
|
||||
const universalIdentifier = overrides.universalIdentifier ?? v4();
|
||||
|
||||
return {
|
||||
universalIdentifier,
|
||||
objectMetadataUniversalIdentifier: 'object-uid',
|
||||
applicationUniversalIdentifier: 'app-uid',
|
||||
name: overrides.name,
|
||||
label: overrides.label ?? overrides.name,
|
||||
type: overrides.type,
|
||||
isCustom: overrides.isCustom ?? false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isUIReadOnly: false,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: null,
|
||||
description: null,
|
||||
icon: null,
|
||||
options: null,
|
||||
settings: null,
|
||||
standardOverrides: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
} as unknown as UniversalFlatFieldMetadata;
|
||||
};
|
||||
|
||||
const flatApplication = {
|
||||
universalIdentifier: 'app-uid',
|
||||
id: 'app-id',
|
||||
} as never;
|
||||
|
||||
const viewUniversalIdentifier = 'view-uid';
|
||||
|
||||
describe('computeFlatViewFieldsToCreate', () => {
|
||||
it('should exclude TS_VECTOR fields', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
fields[0].universalIdentifier,
|
||||
);
|
||||
});
|
||||
|
||||
it('should exclude POSITION fields', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
fields[0].universalIdentifier,
|
||||
);
|
||||
});
|
||||
|
||||
it('should exclude RELATION and MORPH_RELATION fields', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'company',
|
||||
type: FieldMetadataType.RELATION,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'target',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
fields[0].universalIdentifier,
|
||||
);
|
||||
});
|
||||
|
||||
it('should exclude deletedAt field', () => {
|
||||
const fields = [
|
||||
makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
}),
|
||||
makeFieldMetadata({
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
}),
|
||||
];
|
||||
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
fields[0].universalIdentifier,
|
||||
);
|
||||
});
|
||||
|
||||
it('should place label identifier field first', () => {
|
||||
const labelField = makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
const otherField = makeFieldMetadata({
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
});
|
||||
const fields = [otherField, labelField];
|
||||
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: fields,
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
labelField.universalIdentifier,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
labelField.universalIdentifier,
|
||||
);
|
||||
expect(result[0].position).toBe(0);
|
||||
expect(result[1].fieldMetadataUniversalIdentifier).toBe(
|
||||
otherField.universalIdentifier,
|
||||
);
|
||||
expect(result[1].position).toBe(1);
|
||||
});
|
||||
|
||||
it('should exclude label identifier when excludeLabelIdentifier is true', () => {
|
||||
const labelField = makeFieldMetadata({
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
const otherField = makeFieldMetadata({
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
});
|
||||
|
||||
const result = computeFlatViewFieldsToCreate({
|
||||
objectFlatFieldMetadatas: [labelField, otherField],
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
labelField.universalIdentifier,
|
||||
excludeLabelIdentifier: true,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
otherField.universalIdentifier,
|
||||
);
|
||||
});
|
||||
});
|
||||
+8
@@ -11,22 +11,30 @@ export const computeFlatViewFieldsToCreate = ({
|
||||
viewUniversalIdentifier,
|
||||
flatApplication,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
excludeLabelIdentifier = false,
|
||||
}: {
|
||||
flatApplication: FlatApplication;
|
||||
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
|
||||
viewUniversalIdentifier: string;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
excludeLabelIdentifier?: boolean;
|
||||
}): UniversalFlatViewField[] => {
|
||||
const createdAt = new Date().toISOString();
|
||||
const defaultViewFields = objectFlatFieldMetadatas
|
||||
.filter(
|
||||
(field) =>
|
||||
field.name !== 'deletedAt' &&
|
||||
field.type !== FieldMetadataType.TS_VECTOR &&
|
||||
field.type !== FieldMetadataType.POSITION &&
|
||||
field.type !== FieldMetadataType.MORPH_RELATION &&
|
||||
field.type !== FieldMetadataType.RELATION &&
|
||||
// Include 'id' only if it's the label identifier (e.g., for junction tables)
|
||||
(field.name !== 'id' ||
|
||||
field.universalIdentifier ===
|
||||
labelIdentifierFieldMetadataUniversalIdentifier) &&
|
||||
// Exclude label identifier field when requested (e.g., for FIELDS_WIDGET views)
|
||||
(!excludeLabelIdentifier ||
|
||||
field.universalIdentifier !==
|
||||
labelIdentifierFieldMetadataUniversalIdentifier),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
|
||||
+13
@@ -84,6 +84,19 @@ export class ViewGroupResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => [ViewGroupDTO])
|
||||
@UseGuards(UpdateViewGroupPermissionGuard)
|
||||
async updateManyViewGroups(
|
||||
@Args('inputs', { type: () => [UpdateViewGroupInput] })
|
||||
updateViewGroupInputs: UpdateViewGroupInput[],
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ViewGroupDTO[]> {
|
||||
return await this.viewGroupService.updateMany({
|
||||
updateViewGroupInputs,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => ViewGroupDTO)
|
||||
@UseGuards(DeleteViewGroupPermissionGuard)
|
||||
async deleteViewGroup(
|
||||
|
||||
+43
-13
@@ -7,6 +7,7 @@ import { IsNull, Repository } from 'typeorm';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { fromCreateViewGroupInputToFlatViewGroupToCreate } from 'src/engine/metadata-modules/flat-view-group/utils/from-create-view-group-input-to-flat-view-group-to-create.util';
|
||||
@@ -152,6 +153,32 @@ export class ViewGroupService {
|
||||
workspaceId: string;
|
||||
updateViewGroupInput: UpdateViewGroupInput;
|
||||
}): Promise<ViewGroupDTO> {
|
||||
const [updatedViewGroup] = await this.updateMany({
|
||||
updateViewGroupInputs: [updateViewGroupInput],
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!isDefined(updatedViewGroup)) {
|
||||
throw new ViewGroupException(
|
||||
'Failed to update view group',
|
||||
ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedViewGroup;
|
||||
}
|
||||
|
||||
async updateMany({
|
||||
updateViewGroupInputs,
|
||||
workspaceId,
|
||||
}: {
|
||||
updateViewGroupInputs: UpdateViewGroupInput[];
|
||||
workspaceId: string;
|
||||
}): Promise<ViewGroupDTO[]> {
|
||||
if (updateViewGroupInputs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
@@ -167,11 +194,13 @@ export class ViewGroupService {
|
||||
},
|
||||
);
|
||||
|
||||
const optimisticallyUpdatedFlatViewGroup =
|
||||
fromUpdateViewGroupInputToFlatViewGroupToUpdateOrThrow({
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
updateViewGroupInput,
|
||||
});
|
||||
const flatViewGroupsToUpdate = updateViewGroupInputs.map(
|
||||
(updateViewGroupInput) =>
|
||||
fromUpdateViewGroupInputToFlatViewGroupToUpdateOrThrow({
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
updateViewGroupInput,
|
||||
}),
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
@@ -180,7 +209,7 @@ export class ViewGroupService {
|
||||
viewGroup: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [optimisticallyUpdatedFlatViewGroup],
|
||||
flatEntityToUpdate: flatViewGroupsToUpdate,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
@@ -193,7 +222,7 @@ export class ViewGroupService {
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while updating view group',
|
||||
'Multiple validation errors occurred while updating view groups',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -205,12 +234,13 @@ export class ViewGroupService {
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatViewGroupToViewGroupDto(
|
||||
findFlatEntityByUniversalIdentifierOrThrow({
|
||||
universalIdentifier:
|
||||
optimisticallyUpdatedFlatViewGroup.universalIdentifier,
|
||||
flatEntityMaps: recomputedExistingFlatViewGroupMaps,
|
||||
}),
|
||||
return updateViewGroupInputs.map(({ id }) =>
|
||||
fromFlatViewGroupToViewGroupDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedExistingFlatViewGroupMaps,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+14
-2
@@ -121,8 +121,10 @@ import {
|
||||
WORKSPACE_MEMBER_DATA_SEED_COLUMNS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
|
||||
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util';
|
||||
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
type RecordSeedConfig = {
|
||||
@@ -304,6 +306,7 @@ export class DevSeederDataService {
|
||||
private readonly timelineActivitySeederService: TimelineActivitySeederService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
|
||||
) {}
|
||||
|
||||
public async seed({
|
||||
@@ -331,6 +334,14 @@ export class DevSeederDataService {
|
||||
const { seeds: attachmentSeeds, fileSeedMetadata: attachmentFileMeta } =
|
||||
generateAttachmentSeedsForWorkspace(workspaceId);
|
||||
|
||||
await this.prefillLogicFunctionService.ensureSeeded({
|
||||
workspaceId,
|
||||
definitions:
|
||||
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
|
||||
workspaceId,
|
||||
),
|
||||
});
|
||||
|
||||
await this.coreDataSource.transaction(
|
||||
async (entityManager: WorkspaceEntityManager) => {
|
||||
await this.seedRecordsInBatches({
|
||||
@@ -359,6 +370,7 @@ export class DevSeederDataService {
|
||||
|
||||
await prefillWorkflows(
|
||||
entityManager,
|
||||
workspaceId,
|
||||
schemaName,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
|
||||
@@ -26,6 +26,7 @@ import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/da
|
||||
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
|
||||
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
|
||||
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
|
||||
import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill.module';
|
||||
import { TwentyStandardApplicationModule } from 'src/engine/workspace-manager/twenty-standard-application/twenty-standard-application.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@@ -48,6 +49,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
TypeOrmModule.forFeature([WorkspaceEntity, ObjectMetadataEntity]),
|
||||
ObjectPermissionModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
StandardObjectsPrefillModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceMigrationModule,
|
||||
TwentyStandardApplicationModule,
|
||||
|
||||
-302
@@ -1,302 +0,0 @@
|
||||
import { FieldActorSource } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type EntityManager } from 'typeorm';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
|
||||
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
|
||||
|
||||
export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
|
||||
export const QUICK_LEAD_WORKFLOW_VERSION_ID =
|
||||
'ac67974f-c524-4288-9d88-af8515400b68';
|
||||
|
||||
export const prefillWorkflows = async (
|
||||
entityManager: EntityManager,
|
||||
schemaName: string,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
) => {
|
||||
const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
|
||||
const companyObjectMetadataId = objectIdByNameSingular['company'];
|
||||
const personObjectMetadataId = objectIdByNameSingular['person'];
|
||||
|
||||
if (
|
||||
!isDefined(companyObjectMetadataId) ||
|
||||
!isDefined(personObjectMetadataId)
|
||||
) {
|
||||
throw new Error('Company or person object metadata not found');
|
||||
}
|
||||
|
||||
const companyObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: companyObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const personObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: personObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) {
|
||||
throw new Error('Company or person object metadata not found');
|
||||
}
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.workflow`, [
|
||||
'id',
|
||||
'name',
|
||||
'lastPublishedVersionId',
|
||||
'statuses',
|
||||
'position',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
'createdByContext',
|
||||
'updatedBySource',
|
||||
'updatedByWorkspaceMemberId',
|
||||
'updatedByName',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: QUICK_LEAD_WORKFLOW_ID,
|
||||
name: 'Quick Lead',
|
||||
lastPublishedVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
|
||||
statuses: ['ACTIVE'],
|
||||
position: 1,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
createdByContext: {},
|
||||
updatedBySource: FieldActorSource.SYSTEM,
|
||||
updatedByWorkspaceMemberId: null,
|
||||
updatedByName: 'System',
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.workflowVersion`, [
|
||||
'id',
|
||||
'name',
|
||||
'trigger',
|
||||
'steps',
|
||||
'status',
|
||||
'position',
|
||||
'workflowId',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: QUICK_LEAD_WORKFLOW_VERSION_ID,
|
||||
name: 'v1',
|
||||
trigger: JSON.stringify({
|
||||
name: 'Launch manually',
|
||||
type: 'MANUAL',
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
icon: 'IconUserPlus',
|
||||
availability: { type: 'GLOBAL', locations: undefined },
|
||||
},
|
||||
nextStepIds: ['6e089bc9-aabd-435f-865f-f31c01c8f4a7'],
|
||||
}),
|
||||
steps: JSON.stringify([
|
||||
{
|
||||
id: '6e089bc9-aabd-435f-865f-f31c01c8f4a7',
|
||||
name: 'Quick Lead Form',
|
||||
type: 'FORM',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: [
|
||||
{
|
||||
id: '14d669f0-5249-4fa4-b0bb-f8bd408328d5',
|
||||
name: 'firstName',
|
||||
type: 'TEXT',
|
||||
label: 'First name',
|
||||
placeholder: 'Tim',
|
||||
},
|
||||
{
|
||||
id: '4eb6ce85-d231-4aef-9837-744490c026d0',
|
||||
name: 'lastName',
|
||||
type: 'TEXT',
|
||||
label: 'Last Name',
|
||||
placeholder: 'Apple',
|
||||
},
|
||||
{
|
||||
id: 'adbf0e9f-1427-49be-b4fb-092b34d97350',
|
||||
name: 'email',
|
||||
type: 'TEXT',
|
||||
label: 'Email',
|
||||
placeholder: '[email protected]',
|
||||
},
|
||||
{
|
||||
id: '4ffc7992-9e65-4a4d-9baf-b52e62f2c273',
|
||||
name: 'jobTitle',
|
||||
type: 'TEXT',
|
||||
label: 'Job title',
|
||||
placeholder: 'CEO',
|
||||
},
|
||||
{
|
||||
id: '42f11926-04ea-4924-94a4-2293cc748362',
|
||||
name: 'companyName',
|
||||
type: 'TEXT',
|
||||
label: 'Company name',
|
||||
placeholder: 'Apple',
|
||||
},
|
||||
{
|
||||
id: 'd6ca80ee-26cd-466d-91bf-984d7205451c',
|
||||
name: 'companyDomain',
|
||||
type: 'TEXT',
|
||||
label: 'Company domain',
|
||||
placeholder: 'https://www.apple.com',
|
||||
},
|
||||
],
|
||||
outputSchema: {
|
||||
email: {
|
||||
type: 'TEXT',
|
||||
label: 'Email',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
jobTitle: {
|
||||
type: 'TEXT',
|
||||
label: 'Job title',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
lastName: {
|
||||
type: 'TEXT',
|
||||
label: 'Last Name',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
firstName: {
|
||||
type: 'TEXT',
|
||||
label: 'First name',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
companyName: {
|
||||
type: 'TEXT',
|
||||
label: 'Company name',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
companyDomain: {
|
||||
type: 'TEXT',
|
||||
label: 'Company domain',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['0715b6cd-7cc1-4b98-971b-00f54dfe643b'],
|
||||
},
|
||||
{
|
||||
id: '0715b6cd-7cc1-4b98-971b-00f54dfe643b',
|
||||
name: 'Create Company',
|
||||
type: 'CREATE_RECORD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
objectName: 'company',
|
||||
objectRecord: {
|
||||
name: '{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyName}}',
|
||||
domainName: {
|
||||
primaryLinkUrl:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyDomain}}',
|
||||
primaryLinkLabel: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
outputSchema: {
|
||||
object: {
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
label: 'Company',
|
||||
value: 'A company',
|
||||
isLeaf: true,
|
||||
fieldIdName: 'id',
|
||||
nameSingular: 'company',
|
||||
},
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: generateObjectRecordFields({
|
||||
objectMetadataInfo: {
|
||||
flatObjectMetadata: companyObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['6f553ea7-b00e-4371-9d88-d8298568a246'],
|
||||
},
|
||||
{
|
||||
id: '6f553ea7-b00e-4371-9d88-d8298568a246',
|
||||
name: 'Create Person',
|
||||
type: 'CREATE_RECORD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
objectName: 'person',
|
||||
objectRecord: {
|
||||
name: {
|
||||
lastName:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.lastName}}',
|
||||
firstName:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.firstName}}',
|
||||
},
|
||||
emails: {
|
||||
primaryEmail:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.email}}',
|
||||
additionalEmails: [],
|
||||
},
|
||||
companyId: '{{0715b6cd-7cc1-4b98-971b-00f54dfe643b.id}}',
|
||||
},
|
||||
},
|
||||
outputSchema: {
|
||||
fields: generateObjectRecordFields({
|
||||
objectMetadataInfo: {
|
||||
flatObjectMetadata: personObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: null,
|
||||
},
|
||||
]),
|
||||
status: 'ACTIVE',
|
||||
position: 1,
|
||||
workflowId: QUICK_LEAD_WORKFLOW_ID,
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
};
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
|
||||
import { type PrefilledWorkflowCodeStepLogicFunctionDefinition } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
|
||||
|
||||
@Injectable()
|
||||
export class PrefillLogicFunctionService {
|
||||
constructor(
|
||||
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
async ensureSeeded({
|
||||
workspaceId,
|
||||
definitions,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
definitions: PrefilledWorkflowCodeStepLogicFunctionDefinition[];
|
||||
}) {
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatLogicFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
for (const definition of definitions) {
|
||||
const existingLogicFunction = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: definition.id,
|
||||
flatEntityMaps: flatLogicFunctionMaps,
|
||||
});
|
||||
|
||||
if (isDefined(existingLogicFunction)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.logicFunctionFromSourceService.createOneFromSource({
|
||||
workspaceId,
|
||||
input: {
|
||||
id: definition.id,
|
||||
name: definition.name,
|
||||
description: definition.description,
|
||||
toolInputSchema: definition.toolInputSchema,
|
||||
source: {
|
||||
sourceHandlerCode: definition.sourceHandlerCode,
|
||||
toolInputSchema: definition.toolInputSchema,
|
||||
handlerName: 'main',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
import { type DataSource, type EntityManager } from 'typeorm';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
|
||||
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
|
||||
|
||||
export const standardObjectsPrefillData = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
) => {
|
||||
dataSource.transaction(async (entityManager: EntityManager) => {
|
||||
await prefillCompanies(entityManager, schemaName);
|
||||
|
||||
await prefillPeople(entityManager, schemaName);
|
||||
|
||||
await prefillWorkflows(
|
||||
entityManager,
|
||||
schemaName,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
await prefillWorkflowCommandMenuItems(entityManager, workspaceId);
|
||||
});
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
|
||||
@Module({
|
||||
imports: [LogicFunctionModule, WorkspaceManyOrAllFlatEntityMapsCacheModule],
|
||||
providers: [PrefillLogicFunctionService],
|
||||
exports: [PrefillLogicFunctionService],
|
||||
})
|
||||
export class StandardObjectsPrefillModule {}
|
||||
+2
-2
@@ -7,14 +7,14 @@ import {
|
||||
FIGMA_ID,
|
||||
NOTION_ID,
|
||||
STRIPE_ID,
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
|
||||
import {
|
||||
BRIAN_CHESKY_ID,
|
||||
DARIO_AMODEI_ID,
|
||||
DYLAN_FIELD_ID,
|
||||
IVAN_ZHAO_ID,
|
||||
PATRICK_COLLISON_ID,
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-people.util';
|
||||
|
||||
export const OPPORTUNITY_STRIPE_PLATFORM_MIGRATION_ID =
|
||||
'822639e5-9bf7-40f1-8882-a11140362339';
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
FIGMA_ID,
|
||||
NOTION_ID,
|
||||
STRIPE_ID,
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
|
||||
|
||||
export const BRIAN_CHESKY_ID = 'a2e78a5e-338b-46df-8811-fa08c7d19d35'; // Airbnb
|
||||
export const DARIO_AMODEI_ID = '93c72d2e-e65c-44c4-99ad-f87f50349dcf'; // Anthropic
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
|
||||
const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE =
|
||||
'd41b0a2d-aa97-44dc-ad5d-89774164af27';
|
||||
|
||||
export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds = (
|
||||
workspaceId: string,
|
||||
) => ({
|
||||
extractDomainLogicFunctionId: uuidv5(
|
||||
`${workspaceId}:create-company-when-adding-new-person:extract-domain`,
|
||||
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
|
||||
),
|
||||
findMatchingCompanyByDomainLogicFunctionId: uuidv5(
|
||||
`${workspaceId}:create-company-when-adding-new-person:find-matching-company-by-domain`,
|
||||
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
|
||||
),
|
||||
isPersonalEmailLogicFunctionId: uuidv5(
|
||||
`${workspaceId}:create-company-when-adding-new-person:is-personal-email`,
|
||||
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
|
||||
),
|
||||
});
|
||||
|
||||
const EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['email'],
|
||||
};
|
||||
|
||||
const IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
primaryEmail: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['primaryEmail'],
|
||||
};
|
||||
|
||||
const FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companies: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['companies', 'domain'],
|
||||
};
|
||||
|
||||
const EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
|
||||
'ac.uk',
|
||||
'co.in',
|
||||
'co.jp',
|
||||
'co.kr',
|
||||
'co.nz',
|
||||
'co.uk',
|
||||
'co.za',
|
||||
'com.ar',
|
||||
'com.au',
|
||||
'com.br',
|
||||
'com.cn',
|
||||
'com.hk',
|
||||
'com.mx',
|
||||
'com.sg',
|
||||
'com.tr',
|
||||
'com.tw',
|
||||
'com.ua',
|
||||
'gov.uk',
|
||||
'ne.jp',
|
||||
'net.au',
|
||||
'org.au',
|
||||
'org.uk',
|
||||
]);
|
||||
|
||||
const getRegistrableDomain = (host) => {
|
||||
const normalizedHost = host.toLowerCase().replace(/^www\\./, '');
|
||||
const parts = normalizedHost.split('.').filter(Boolean);
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return normalizedHost;
|
||||
}
|
||||
|
||||
const lastTwo = parts.slice(-2).join('.');
|
||||
|
||||
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
|
||||
return lastTwo;
|
||||
};
|
||||
|
||||
export const main = async (params) => {
|
||||
const email =
|
||||
typeof params?.email === 'string' ? params.email.trim().toLowerCase() : '';
|
||||
const domainFromEmail = email.split('@')[1] ?? '';
|
||||
const domain = domainFromEmail ? getRegistrableDomain(domainFromEmail) : '';
|
||||
|
||||
return {
|
||||
url: domain ? \`https://\${domain}\` : '',
|
||||
domain,
|
||||
};
|
||||
};`;
|
||||
|
||||
const IS_PERSONAL_EMAIL_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
|
||||
'ac.uk',
|
||||
'co.in',
|
||||
'co.jp',
|
||||
'co.kr',
|
||||
'co.nz',
|
||||
'co.uk',
|
||||
'co.za',
|
||||
'com.ar',
|
||||
'com.au',
|
||||
'com.br',
|
||||
'com.cn',
|
||||
'com.hk',
|
||||
'com.mx',
|
||||
'com.sg',
|
||||
'com.tr',
|
||||
'com.tw',
|
||||
'com.ua',
|
||||
'gov.uk',
|
||||
'ne.jp',
|
||||
'net.au',
|
||||
'org.au',
|
||||
'org.uk',
|
||||
]);
|
||||
|
||||
const PERSONAL_EMAIL_DOMAINS = new Set([
|
||||
'aol.com',
|
||||
'att.net',
|
||||
'btinternet.com',
|
||||
'comcast.net',
|
||||
'fastmail.com',
|
||||
'free.fr',
|
||||
'gmx.com',
|
||||
'gmx.de',
|
||||
'googlemail.com',
|
||||
'hotmail.com',
|
||||
'hotmail.fr',
|
||||
'icloud.com',
|
||||
'laposte.net',
|
||||
'live.com',
|
||||
'mac.com',
|
||||
'mail.com',
|
||||
'me.com',
|
||||
'msn.com',
|
||||
'orange.fr',
|
||||
'outlook.com',
|
||||
'outlook.fr',
|
||||
'pm.me',
|
||||
'proton.me',
|
||||
'protonmail.com',
|
||||
'sfr.fr',
|
||||
'verizon.net',
|
||||
'wanadoo.fr',
|
||||
'yahoo.co.jp',
|
||||
'yahoo.co.uk',
|
||||
'yahoo.com',
|
||||
'yandex.com',
|
||||
'yandex.ru',
|
||||
'zoho.com',
|
||||
'gmail.com',
|
||||
]);
|
||||
|
||||
const getRegistrableDomain = (host) => {
|
||||
const normalizedHost = host.toLowerCase().replace(/^www\\./, '');
|
||||
const parts = normalizedHost.split('.').filter(Boolean);
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return normalizedHost;
|
||||
}
|
||||
|
||||
const lastTwo = parts.slice(-2).join('.');
|
||||
|
||||
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
|
||||
return lastTwo;
|
||||
};
|
||||
|
||||
export const main = async (params) => {
|
||||
const email =
|
||||
typeof params?.primaryEmail === 'string'
|
||||
? params.primaryEmail.trim().toLowerCase()
|
||||
: '';
|
||||
|
||||
if (!email || !email.includes('@')) {
|
||||
return { isPersonal: true };
|
||||
}
|
||||
|
||||
const host = email.split('@')[1] ?? '';
|
||||
const registrableDomain = host ? getRegistrableDomain(host) : '';
|
||||
|
||||
return {
|
||||
isPersonal:
|
||||
!registrableDomain ||
|
||||
PERSONAL_EMAIL_DOMAINS.has(host) ||
|
||||
PERSONAL_EMAIL_DOMAINS.has(registrableDomain),
|
||||
};
|
||||
};`;
|
||||
|
||||
const FIND_MATCHING_COMPANY_BY_DOMAIN_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
|
||||
'ac.uk',
|
||||
'co.in',
|
||||
'co.jp',
|
||||
'co.kr',
|
||||
'co.nz',
|
||||
'co.uk',
|
||||
'co.za',
|
||||
'com.ar',
|
||||
'com.au',
|
||||
'com.br',
|
||||
'com.cn',
|
||||
'com.hk',
|
||||
'com.mx',
|
||||
'com.sg',
|
||||
'com.tr',
|
||||
'com.tw',
|
||||
'com.ua',
|
||||
'gov.uk',
|
||||
'ne.jp',
|
||||
'net.au',
|
||||
'org.au',
|
||||
'org.uk',
|
||||
]);
|
||||
|
||||
const normalizeHost = (value) => {
|
||||
if (typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^https?:\\/\\//, '')
|
||||
.replace(/^www\\./, '')
|
||||
.split('/')[0]
|
||||
.split('?')[0]
|
||||
.split('#')[0]
|
||||
.split(':')[0];
|
||||
};
|
||||
|
||||
const getRegistrableDomain = (value) => {
|
||||
const normalizedHost = normalizeHost(value);
|
||||
const parts = normalizedHost.split('.').filter(Boolean);
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return normalizedHost;
|
||||
}
|
||||
|
||||
const lastTwo = parts.slice(-2).join('.');
|
||||
|
||||
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
|
||||
return lastTwo;
|
||||
};
|
||||
|
||||
export const main = async (params) => {
|
||||
const domain = getRegistrableDomain(params?.domain);
|
||||
const companies = Array.isArray(params?.companies) ? params.companies : [];
|
||||
|
||||
const matchingCompany = companies.find((company) => {
|
||||
const companyDomain = getRegistrableDomain(
|
||||
company?.domainName?.primaryLinkUrl,
|
||||
);
|
||||
|
||||
return companyDomain !== '' && companyDomain === domain;
|
||||
});
|
||||
|
||||
const companyId =
|
||||
typeof matchingCompany?.id === 'string' ? matchingCompany.id : '';
|
||||
|
||||
return {
|
||||
companyId,
|
||||
hasMatch: companyId !== '',
|
||||
};
|
||||
};`;
|
||||
|
||||
export type PrefilledWorkflowCodeStepLogicFunctionDefinition = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
sourceHandlerCode: string;
|
||||
toolInputSchema: object;
|
||||
};
|
||||
|
||||
export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions =
|
||||
(workspaceId: string): PrefilledWorkflowCodeStepLogicFunctionDefinition[] => {
|
||||
const {
|
||||
extractDomainLogicFunctionId,
|
||||
findMatchingCompanyByDomainLogicFunctionId,
|
||||
isPersonalEmailLogicFunctionId,
|
||||
} =
|
||||
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds(workspaceId);
|
||||
|
||||
return [
|
||||
{
|
||||
id: extractDomainLogicFunctionId,
|
||||
name: 'Extract domain from email',
|
||||
description:
|
||||
'Extracts a normalized company domain and URL from a person email address.',
|
||||
sourceHandlerCode: EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE,
|
||||
toolInputSchema: EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
{
|
||||
id: findMatchingCompanyByDomainLogicFunctionId,
|
||||
name: 'Find matching company by domain',
|
||||
description:
|
||||
'Finds an existing company whose website matches a normalized registrable domain.',
|
||||
sourceHandlerCode:
|
||||
FIND_MATCHING_COMPANY_BY_DOMAIN_LOGIC_FUNCTION_SOURCE,
|
||||
toolInputSchema: FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
{
|
||||
id: isPersonalEmailLogicFunctionId,
|
||||
name: 'Is this a personal email?',
|
||||
description:
|
||||
'Detects whether an email address belongs to a common personal email provider.',
|
||||
sourceHandlerCode: IS_PERSONAL_EMAIL_LOGIC_FUNCTION_SOURCE,
|
||||
toolInputSchema: IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
];
|
||||
};
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { type EntityManager } from 'typeorm';
|
||||
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
|
||||
import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
const QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =
|
||||
+762
@@ -0,0 +1,762 @@
|
||||
import { FieldActorSource } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type EntityManager } from 'typeorm';
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
|
||||
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event';
|
||||
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
|
||||
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
|
||||
|
||||
export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
|
||||
export const QUICK_LEAD_WORKFLOW_VERSION_ID =
|
||||
'ac67974f-c524-4288-9d88-af8515400b68';
|
||||
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID =
|
||||
'887c6c06-fbc5-4b45-8d6b-f7b6b0f40b12';
|
||||
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID =
|
||||
'0f276d7e-a950-41ab-ad98-35e80753dc58';
|
||||
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_AUTOMATED_TRIGGER_ID =
|
||||
'c54f5990-13a3-4c3b-b75d-df09e7843036';
|
||||
|
||||
export const prefillWorkflows = async (
|
||||
entityManager: EntityManager,
|
||||
workspaceId: string,
|
||||
schemaName: string,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
) => {
|
||||
const {
|
||||
extractDomainLogicFunctionId,
|
||||
findMatchingCompanyByDomainLogicFunctionId,
|
||||
isPersonalEmailLogicFunctionId,
|
||||
} = getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds(workspaceId);
|
||||
|
||||
const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
|
||||
const companyObjectMetadataId = objectIdByNameSingular['company'];
|
||||
const personObjectMetadataId = objectIdByNameSingular['person'];
|
||||
|
||||
if (
|
||||
!isDefined(companyObjectMetadataId) ||
|
||||
!isDefined(personObjectMetadataId)
|
||||
) {
|
||||
throw new Error('Company or person object metadata not found');
|
||||
}
|
||||
|
||||
const companyObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: companyObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const personObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: personObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) {
|
||||
throw new Error('Company or person object metadata not found');
|
||||
}
|
||||
|
||||
const companyDomainNameFieldMetadata = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
).find(
|
||||
(fieldMetadata) =>
|
||||
fieldMetadata?.objectMetadataId === companyObjectMetadataId &&
|
||||
fieldMetadata?.name === 'domainName',
|
||||
);
|
||||
|
||||
if (!isDefined(companyDomainNameFieldMetadata)) {
|
||||
throw new Error('Company domainName field metadata not found');
|
||||
}
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.workflow`, [
|
||||
'id',
|
||||
'name',
|
||||
'lastPublishedVersionId',
|
||||
'statuses',
|
||||
'position',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
'createdByContext',
|
||||
'updatedBySource',
|
||||
'updatedByWorkspaceMemberId',
|
||||
'updatedByName',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: QUICK_LEAD_WORKFLOW_ID,
|
||||
name: 'Quick Lead',
|
||||
lastPublishedVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
|
||||
statuses: ['ACTIVE'],
|
||||
position: 1,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
createdByContext: {},
|
||||
updatedBySource: FieldActorSource.SYSTEM,
|
||||
updatedByWorkspaceMemberId: null,
|
||||
updatedByName: 'System',
|
||||
},
|
||||
{
|
||||
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
|
||||
name: 'Create company when adding a new person',
|
||||
lastPublishedVersionId:
|
||||
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID,
|
||||
statuses: ['ACTIVE'],
|
||||
position: 2,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
createdByContext: {},
|
||||
updatedBySource: FieldActorSource.SYSTEM,
|
||||
updatedByWorkspaceMemberId: null,
|
||||
updatedByName: 'System',
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.workflowVersion`, [
|
||||
'id',
|
||||
'name',
|
||||
'trigger',
|
||||
'steps',
|
||||
'status',
|
||||
'position',
|
||||
'workflowId',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: QUICK_LEAD_WORKFLOW_VERSION_ID,
|
||||
name: 'v1',
|
||||
trigger: JSON.stringify({
|
||||
name: 'Launch manually',
|
||||
type: 'MANUAL',
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
icon: 'IconUserPlus',
|
||||
availability: { type: 'GLOBAL', locations: undefined },
|
||||
},
|
||||
nextStepIds: ['6e089bc9-aabd-435f-865f-f31c01c8f4a7'],
|
||||
}),
|
||||
steps: JSON.stringify([
|
||||
{
|
||||
id: '6e089bc9-aabd-435f-865f-f31c01c8f4a7',
|
||||
name: 'Quick Lead Form',
|
||||
type: 'FORM',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: [
|
||||
{
|
||||
id: '14d669f0-5249-4fa4-b0bb-f8bd408328d5',
|
||||
name: 'firstName',
|
||||
type: 'TEXT',
|
||||
label: 'First name',
|
||||
placeholder: 'Tim',
|
||||
},
|
||||
{
|
||||
id: '4eb6ce85-d231-4aef-9837-744490c026d0',
|
||||
name: 'lastName',
|
||||
type: 'TEXT',
|
||||
label: 'Last Name',
|
||||
placeholder: 'Apple',
|
||||
},
|
||||
{
|
||||
id: 'adbf0e9f-1427-49be-b4fb-092b34d97350',
|
||||
name: 'email',
|
||||
type: 'TEXT',
|
||||
label: 'Email',
|
||||
placeholder: '[email protected]',
|
||||
},
|
||||
{
|
||||
id: '4ffc7992-9e65-4a4d-9baf-b52e62f2c273',
|
||||
name: 'jobTitle',
|
||||
type: 'TEXT',
|
||||
label: 'Job title',
|
||||
placeholder: 'CEO',
|
||||
},
|
||||
{
|
||||
id: '42f11926-04ea-4924-94a4-2293cc748362',
|
||||
name: 'companyName',
|
||||
type: 'TEXT',
|
||||
label: 'Company name',
|
||||
placeholder: 'Apple',
|
||||
},
|
||||
{
|
||||
id: 'd6ca80ee-26cd-466d-91bf-984d7205451c',
|
||||
name: 'companyDomain',
|
||||
type: 'TEXT',
|
||||
label: 'Company domain',
|
||||
placeholder: 'https://www.apple.com',
|
||||
},
|
||||
],
|
||||
outputSchema: {
|
||||
email: {
|
||||
type: 'TEXT',
|
||||
label: 'Email',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
jobTitle: {
|
||||
type: 'TEXT',
|
||||
label: 'Job title',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
lastName: {
|
||||
type: 'TEXT',
|
||||
label: 'Last Name',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
firstName: {
|
||||
type: 'TEXT',
|
||||
label: 'First name',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
companyName: {
|
||||
type: 'TEXT',
|
||||
label: 'Company name',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
companyDomain: {
|
||||
type: 'TEXT',
|
||||
label: 'Company domain',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['0715b6cd-7cc1-4b98-971b-00f54dfe643b'],
|
||||
},
|
||||
{
|
||||
id: '0715b6cd-7cc1-4b98-971b-00f54dfe643b',
|
||||
name: 'Create Company',
|
||||
type: 'CREATE_RECORD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
objectName: 'company',
|
||||
objectRecord: {
|
||||
name: '{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyName}}',
|
||||
domainName: {
|
||||
primaryLinkUrl:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyDomain}}',
|
||||
primaryLinkLabel: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
outputSchema: {
|
||||
object: {
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
label: 'Company',
|
||||
value: 'A company',
|
||||
isLeaf: true,
|
||||
fieldIdName: 'id',
|
||||
nameSingular: 'company',
|
||||
},
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: generateObjectRecordFields({
|
||||
objectMetadataInfo: {
|
||||
flatObjectMetadata: companyObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['6f553ea7-b00e-4371-9d88-d8298568a246'],
|
||||
},
|
||||
{
|
||||
id: '6f553ea7-b00e-4371-9d88-d8298568a246',
|
||||
name: 'Create Person',
|
||||
type: 'CREATE_RECORD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
objectName: 'person',
|
||||
objectRecord: {
|
||||
name: {
|
||||
lastName:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.lastName}}',
|
||||
firstName:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.firstName}}',
|
||||
},
|
||||
emails: {
|
||||
primaryEmail:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.email}}',
|
||||
additionalEmails: [],
|
||||
},
|
||||
companyId: '{{0715b6cd-7cc1-4b98-971b-00f54dfe643b.id}}',
|
||||
},
|
||||
},
|
||||
outputSchema: {
|
||||
fields: generateObjectRecordFields({
|
||||
objectMetadataInfo: {
|
||||
flatObjectMetadata: personObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: null,
|
||||
},
|
||||
]),
|
||||
status: 'ACTIVE',
|
||||
position: 1,
|
||||
workflowId: QUICK_LEAD_WORKFLOW_ID,
|
||||
},
|
||||
{
|
||||
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID,
|
||||
name: 'v1',
|
||||
trigger: JSON.stringify({
|
||||
name: 'Record is created or updated',
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'person.upserted',
|
||||
fields: ['emails'],
|
||||
outputSchema: generateFakeObjectRecordEvent(
|
||||
{
|
||||
flatObjectMetadata: personObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
DatabaseEventAction.UPSERTED,
|
||||
),
|
||||
},
|
||||
nextStepIds: ['c30d7cbe-00e0-4966-bc1a-99b0a11a2cca'],
|
||||
}),
|
||||
steps: JSON.stringify([
|
||||
{
|
||||
id: 'c30d7cbe-00e0-4966-bc1a-99b0a11a2cca',
|
||||
name: 'Is this a personal email?',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
position: {
|
||||
x: 227.25,
|
||||
y: 130,
|
||||
},
|
||||
settings: {
|
||||
input: {
|
||||
logicFunctionId: isPersonalEmailLogicFunctionId,
|
||||
logicFunctionInput: {
|
||||
primaryEmail:
|
||||
'{{trigger.properties.after.emails.primaryEmail}}',
|
||||
},
|
||||
},
|
||||
outputSchema: {
|
||||
isPersonal: {
|
||||
type: 'boolean',
|
||||
label: 'isPersonal',
|
||||
value: true,
|
||||
isLeaf: true,
|
||||
},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['01f3db05-aae5-4e4b-b361-96684f09c704'],
|
||||
},
|
||||
{
|
||||
id: '01f3db05-aae5-4e4b-b361-96684f09c704',
|
||||
name: 'If business email',
|
||||
type: 'FILTER',
|
||||
valid: false,
|
||||
position: {
|
||||
x: 249.25,
|
||||
y: 260,
|
||||
},
|
||||
settings: {
|
||||
input: {
|
||||
stepFilters: [
|
||||
{
|
||||
id: '0e595385-9e18-4869-abfd-cf72952b124c',
|
||||
type: 'boolean',
|
||||
value: 'false',
|
||||
operand: 'IS',
|
||||
isFullRecord: false,
|
||||
stepOutputKey:
|
||||
'{{c30d7cbe-00e0-4966-bc1a-99b0a11a2cca.isPersonal}}',
|
||||
stepFilterGroupId: '5204d5f5-7b23-428c-9f84-c37971d497d3',
|
||||
positionInStepFilterGroup: 0,
|
||||
},
|
||||
],
|
||||
stepFilterGroups: [
|
||||
{
|
||||
id: '5204d5f5-7b23-428c-9f84-c37971d497d3',
|
||||
logicalOperator: 'AND',
|
||||
},
|
||||
],
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['1b01193b-8300-4d79-940b-44464bf45505'],
|
||||
},
|
||||
{
|
||||
id: '1b01193b-8300-4d79-940b-44464bf45505',
|
||||
name: 'Extract domain from email',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
position: {
|
||||
x: 219.75,
|
||||
y: 390,
|
||||
},
|
||||
settings: {
|
||||
input: {
|
||||
logicFunctionId: extractDomainLogicFunctionId,
|
||||
logicFunctionInput: {
|
||||
email: '{{trigger.properties.after.emails.primaryEmail}}',
|
||||
},
|
||||
},
|
||||
outputSchema: {
|
||||
url: {
|
||||
icon: 'IconVariable',
|
||||
type: 'string',
|
||||
label: 'url',
|
||||
value: 'https://twenty.com',
|
||||
isLeaf: true,
|
||||
},
|
||||
domain: {
|
||||
icon: 'IconVariable',
|
||||
type: 'string',
|
||||
label: 'domain',
|
||||
value: 'twenty.com',
|
||||
isLeaf: true,
|
||||
},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['becb3acf-79bb-4672-8a42-3696e94957b5'],
|
||||
},
|
||||
{
|
||||
id: 'becb3acf-79bb-4672-8a42-3696e94957b5',
|
||||
name: 'Search Company',
|
||||
type: 'FIND_RECORDS',
|
||||
valid: false,
|
||||
position: {
|
||||
x: 247.75,
|
||||
y: 520,
|
||||
},
|
||||
settings: {
|
||||
input: {
|
||||
limit: 25,
|
||||
filter: {
|
||||
recordFilters: [
|
||||
{
|
||||
id: 'a9b917a0-5c4c-4e8f-bf91-160d0b888693',
|
||||
type: 'LINKS',
|
||||
label: 'Domain Name',
|
||||
value: '{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
|
||||
operand: 'CONTAINS',
|
||||
displayValue:
|
||||
'{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
|
||||
subFieldName: 'primaryLinkUrl',
|
||||
fieldMetadataId: companyDomainNameFieldMetadata.id,
|
||||
recordFilterGroupId:
|
||||
'194e151c-cf46-4e8f-a48b-649c36082dfa',
|
||||
},
|
||||
],
|
||||
recordFilterGroups: [
|
||||
{
|
||||
id: '194e151c-cf46-4e8f-a48b-649c36082dfa',
|
||||
logicalOperator: 'AND',
|
||||
},
|
||||
],
|
||||
},
|
||||
objectName: 'company',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['9d0b6ef2-aad2-4853-92e1-95f2abf10d5b'],
|
||||
},
|
||||
{
|
||||
id: '9d0b6ef2-aad2-4853-92e1-95f2abf10d5b',
|
||||
name: 'Find exact company match',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
position: {
|
||||
x: 247.75,
|
||||
y: 650,
|
||||
},
|
||||
settings: {
|
||||
input: {
|
||||
logicFunctionId: findMatchingCompanyByDomainLogicFunctionId,
|
||||
logicFunctionInput: {
|
||||
companies: '{{becb3acf-79bb-4672-8a42-3696e94957b5.all}}',
|
||||
domain: '{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
|
||||
},
|
||||
},
|
||||
outputSchema: {
|
||||
companyId: {
|
||||
type: 'string',
|
||||
label: 'companyId',
|
||||
value: '00000000-0000-0000-0000-000000000000',
|
||||
isLeaf: true,
|
||||
},
|
||||
hasMatch: {
|
||||
type: 'boolean',
|
||||
label: 'hasMatch',
|
||||
value: true,
|
||||
isLeaf: true,
|
||||
},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['0c99a900-656a-40e8-977e-5a7357be33b9'],
|
||||
},
|
||||
{
|
||||
id: '0c99a900-656a-40e8-977e-5a7357be33b9',
|
||||
name: 'If a company already exists',
|
||||
type: 'IF_ELSE',
|
||||
valid: false,
|
||||
position: {
|
||||
x: 216.25,
|
||||
y: 780,
|
||||
},
|
||||
settings: {
|
||||
input: {
|
||||
branches: [
|
||||
{
|
||||
id: '1344c151-15ff-40e2-a1a3-925fabaf5b1c',
|
||||
nextStepIds: ['ffdd4271-75d4-4805-b1f8-2167a113c3b2'],
|
||||
filterGroupId: 'f5c41047-2a6e-49fb-968a-fa7789a90ee5',
|
||||
},
|
||||
{
|
||||
id: 'fe6dd152-1103-4324-af51-3ff994d1f8a7',
|
||||
nextStepIds: ['ddafb9db-a94f-40b9-a5c9-becce857edf7'],
|
||||
},
|
||||
],
|
||||
stepFilters: [
|
||||
{
|
||||
id: '290cc6a3-08fd-4be5-b42e-966d0bb90ff7',
|
||||
type: 'boolean',
|
||||
value: 'true',
|
||||
operand: 'IS',
|
||||
isFullRecord: false,
|
||||
stepOutputKey:
|
||||
'{{9d0b6ef2-aad2-4853-92e1-95f2abf10d5b.hasMatch}}',
|
||||
stepFilterGroupId: 'f5c41047-2a6e-49fb-968a-fa7789a90ee5',
|
||||
positionInStepFilterGroup: 0,
|
||||
},
|
||||
],
|
||||
stepFilterGroups: [
|
||||
{
|
||||
id: 'f5c41047-2a6e-49fb-968a-fa7789a90ee5',
|
||||
logicalOperator: 'AND',
|
||||
},
|
||||
],
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
},
|
||||
{
|
||||
id: 'ffdd4271-75d4-4805-b1f8-2167a113c3b2',
|
||||
name: 'Attach person to existing company',
|
||||
type: 'UPDATE_RECORD',
|
||||
valid: false,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 910,
|
||||
},
|
||||
settings: {
|
||||
input: {
|
||||
objectName: 'person',
|
||||
objectRecord: {
|
||||
companyId:
|
||||
'{{9d0b6ef2-aad2-4853-92e1-95f2abf10d5b.companyId}}',
|
||||
},
|
||||
fieldsToUpdate: ['companyId'],
|
||||
objectRecordId: '{{trigger.properties.after.id}}',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
},
|
||||
{
|
||||
id: 'ddafb9db-a94f-40b9-a5c9-becce857edf7',
|
||||
name: 'Create a new company',
|
||||
type: 'CREATE_RECORD',
|
||||
valid: false,
|
||||
position: {
|
||||
x: 440,
|
||||
y: 910,
|
||||
},
|
||||
settings: {
|
||||
input: {
|
||||
objectName: 'company',
|
||||
objectRecord: {
|
||||
name: '{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
|
||||
domainName: {
|
||||
primaryLinkUrl:
|
||||
'{{1b01193b-8300-4d79-940b-44464bf45505.url}}',
|
||||
primaryLinkLabel:
|
||||
'{{1b01193b-8300-4d79-940b-44464bf45505.domain}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['d5d5d6e1-391f-4142-83c1-670f7087f079'],
|
||||
},
|
||||
{
|
||||
id: 'd5d5d6e1-391f-4142-83c1-670f7087f079',
|
||||
name: 'Attach person to this company',
|
||||
type: 'UPDATE_RECORD',
|
||||
valid: false,
|
||||
position: {
|
||||
x: 420.5,
|
||||
y: 1040,
|
||||
},
|
||||
settings: {
|
||||
input: {
|
||||
objectName: 'person',
|
||||
objectRecord: {
|
||||
companyId: '{{ddafb9db-a94f-40b9-a5c9-becce857edf7.id}}',
|
||||
},
|
||||
fieldsToUpdate: ['companyId'],
|
||||
objectRecordId: '{{trigger.properties.after.id}}',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
},
|
||||
]),
|
||||
status: 'ACTIVE',
|
||||
position: 2,
|
||||
workflowId: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.workflowAutomatedTrigger`, [
|
||||
'id',
|
||||
'workflowId',
|
||||
'type',
|
||||
'settings',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_AUTOMATED_TRIGGER_ID,
|
||||
workflowId: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
|
||||
type: 'DATABASE_EVENT',
|
||||
settings: {
|
||||
eventName: 'person.upserted',
|
||||
fields: ['emails'],
|
||||
},
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
+1678
-1549
File diff suppressed because it is too large
Load Diff
+84
-90
@@ -6,40 +6,37 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
|
||||
"id": "00000000-0000-0000-0000-000000000011",
|
||||
"tabs": {
|
||||
"calendar": {
|
||||
"id": "00000000-0000-0000-0000-000000000027",
|
||||
"id": "00000000-0000-0000-0000-000000000026",
|
||||
"widgets": {
|
||||
"calendar": {
|
||||
"id": "00000000-0000-0000-0000-000000000028",
|
||||
"id": "00000000-0000-0000-0000-000000000027",
|
||||
},
|
||||
},
|
||||
},
|
||||
"emails": {
|
||||
"id": "00000000-0000-0000-0000-000000000025",
|
||||
"id": "00000000-0000-0000-0000-000000000024",
|
||||
"widgets": {
|
||||
"emails": {
|
||||
"id": "00000000-0000-0000-0000-000000000026",
|
||||
"id": "00000000-0000-0000-0000-000000000025",
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000023",
|
||||
"id": "00000000-0000-0000-0000-000000000022",
|
||||
"widgets": {
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000024",
|
||||
"id": "00000000-0000-0000-0000-000000000023",
|
||||
},
|
||||
},
|
||||
},
|
||||
"home": {
|
||||
"id": "00000000-0000-0000-0000-000000000012",
|
||||
"widgets": {
|
||||
"accountOwner": {
|
||||
"id": "00000000-0000-0000-0000-000000000015",
|
||||
},
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000013",
|
||||
},
|
||||
"opportunities": {
|
||||
"id": "00000000-0000-0000-0000-000000000016",
|
||||
"id": "00000000-0000-0000-0000-000000000015",
|
||||
},
|
||||
"people": {
|
||||
"id": "00000000-0000-0000-0000-000000000014",
|
||||
@@ -47,26 +44,26 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
|
||||
},
|
||||
},
|
||||
"notes": {
|
||||
"id": "00000000-0000-0000-0000-000000000021",
|
||||
"id": "00000000-0000-0000-0000-000000000020",
|
||||
"widgets": {
|
||||
"notes": {
|
||||
"id": "00000000-0000-0000-0000-000000000022",
|
||||
"id": "00000000-0000-0000-0000-000000000021",
|
||||
},
|
||||
},
|
||||
},
|
||||
"tasks": {
|
||||
"id": "00000000-0000-0000-0000-000000000019",
|
||||
"id": "00000000-0000-0000-0000-000000000018",
|
||||
"widgets": {
|
||||
"tasks": {
|
||||
"id": "00000000-0000-0000-0000-000000000020",
|
||||
"id": "00000000-0000-0000-0000-000000000019",
|
||||
},
|
||||
},
|
||||
},
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000017",
|
||||
"id": "00000000-0000-0000-0000-000000000016",
|
||||
"widgets": {
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000018",
|
||||
"id": "00000000-0000-0000-0000-000000000017",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -107,281 +104,278 @@ exports[`getStandardPageLayoutMetadataRelatedEntityIds should return standard pa
|
||||
},
|
||||
},
|
||||
"noteRecordPage": {
|
||||
"id": "00000000-0000-0000-0000-000000000064",
|
||||
"id": "00000000-0000-0000-0000-000000000063",
|
||||
"tabs": {
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000072",
|
||||
"id": "00000000-0000-0000-0000-000000000071",
|
||||
"widgets": {
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000073",
|
||||
"id": "00000000-0000-0000-0000-000000000072",
|
||||
},
|
||||
},
|
||||
},
|
||||
"home": {
|
||||
"id": "00000000-0000-0000-0000-000000000065",
|
||||
"id": "00000000-0000-0000-0000-000000000064",
|
||||
"widgets": {
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000066",
|
||||
"id": "00000000-0000-0000-0000-000000000065",
|
||||
},
|
||||
"noteRichText": {
|
||||
"id": "00000000-0000-0000-0000-000000000067",
|
||||
"id": "00000000-0000-0000-0000-000000000066",
|
||||
},
|
||||
},
|
||||
},
|
||||
"note": {
|
||||
"id": "00000000-0000-0000-0000-000000000068",
|
||||
"id": "00000000-0000-0000-0000-000000000067",
|
||||
"widgets": {
|
||||
"noteRichText": {
|
||||
"id": "00000000-0000-0000-0000-000000000069",
|
||||
"id": "00000000-0000-0000-0000-000000000068",
|
||||
},
|
||||
},
|
||||
},
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000070",
|
||||
"id": "00000000-0000-0000-0000-000000000069",
|
||||
"widgets": {
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000071",
|
||||
"id": "00000000-0000-0000-0000-000000000070",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"opportunityRecordPage": {
|
||||
"id": "00000000-0000-0000-0000-000000000046",
|
||||
"id": "00000000-0000-0000-0000-000000000045",
|
||||
"tabs": {
|
||||
"calendar": {
|
||||
"id": "00000000-0000-0000-0000-000000000062",
|
||||
"id": "00000000-0000-0000-0000-000000000061",
|
||||
"widgets": {
|
||||
"calendar": {
|
||||
"id": "00000000-0000-0000-0000-000000000063",
|
||||
"id": "00000000-0000-0000-0000-000000000062",
|
||||
},
|
||||
},
|
||||
},
|
||||
"emails": {
|
||||
"id": "00000000-0000-0000-0000-000000000060",
|
||||
"id": "00000000-0000-0000-0000-000000000059",
|
||||
"widgets": {
|
||||
"emails": {
|
||||
"id": "00000000-0000-0000-0000-000000000061",
|
||||
"id": "00000000-0000-0000-0000-000000000060",
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000058",
|
||||
"id": "00000000-0000-0000-0000-000000000057",
|
||||
"widgets": {
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000059",
|
||||
"id": "00000000-0000-0000-0000-000000000058",
|
||||
},
|
||||
},
|
||||
},
|
||||
"home": {
|
||||
"id": "00000000-0000-0000-0000-000000000047",
|
||||
"id": "00000000-0000-0000-0000-000000000046",
|
||||
"widgets": {
|
||||
"company": {
|
||||
"id": "00000000-0000-0000-0000-000000000050",
|
||||
"id": "00000000-0000-0000-0000-000000000049",
|
||||
},
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000048",
|
||||
"id": "00000000-0000-0000-0000-000000000047",
|
||||
},
|
||||
"owner": {
|
||||
"id": "00000000-0000-0000-0000-000000000051",
|
||||
"id": "00000000-0000-0000-0000-000000000050",
|
||||
},
|
||||
"pointOfContact": {
|
||||
"id": "00000000-0000-0000-0000-000000000049",
|
||||
"id": "00000000-0000-0000-0000-000000000048",
|
||||
},
|
||||
},
|
||||
},
|
||||
"notes": {
|
||||
"id": "00000000-0000-0000-0000-000000000056",
|
||||
"id": "00000000-0000-0000-0000-000000000055",
|
||||
"widgets": {
|
||||
"notes": {
|
||||
"id": "00000000-0000-0000-0000-000000000057",
|
||||
"id": "00000000-0000-0000-0000-000000000056",
|
||||
},
|
||||
},
|
||||
},
|
||||
"tasks": {
|
||||
"id": "00000000-0000-0000-0000-000000000054",
|
||||
"id": "00000000-0000-0000-0000-000000000053",
|
||||
"widgets": {
|
||||
"tasks": {
|
||||
"id": "00000000-0000-0000-0000-000000000055",
|
||||
"id": "00000000-0000-0000-0000-000000000054",
|
||||
},
|
||||
},
|
||||
},
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000052",
|
||||
"id": "00000000-0000-0000-0000-000000000051",
|
||||
"widgets": {
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000053",
|
||||
"id": "00000000-0000-0000-0000-000000000052",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"personRecordPage": {
|
||||
"id": "00000000-0000-0000-0000-000000000029",
|
||||
"id": "00000000-0000-0000-0000-000000000028",
|
||||
"tabs": {
|
||||
"calendar": {
|
||||
"id": "00000000-0000-0000-0000-000000000044",
|
||||
"id": "00000000-0000-0000-0000-000000000043",
|
||||
"widgets": {
|
||||
"calendar": {
|
||||
"id": "00000000-0000-0000-0000-000000000045",
|
||||
"id": "00000000-0000-0000-0000-000000000044",
|
||||
},
|
||||
},
|
||||
},
|
||||
"emails": {
|
||||
"id": "00000000-0000-0000-0000-000000000042",
|
||||
"id": "00000000-0000-0000-0000-000000000041",
|
||||
"widgets": {
|
||||
"emails": {
|
||||
"id": "00000000-0000-0000-0000-000000000043",
|
||||
"id": "00000000-0000-0000-0000-000000000042",
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000040",
|
||||
"id": "00000000-0000-0000-0000-000000000039",
|
||||
"widgets": {
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000041",
|
||||
"id": "00000000-0000-0000-0000-000000000040",
|
||||
},
|
||||
},
|
||||
},
|
||||
"home": {
|
||||
"id": "00000000-0000-0000-0000-000000000030",
|
||||
"id": "00000000-0000-0000-0000-000000000029",
|
||||
"widgets": {
|
||||
"company": {
|
||||
"id": "00000000-0000-0000-0000-000000000032",
|
||||
},
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000031",
|
||||
},
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000030",
|
||||
},
|
||||
"pointOfContactForOpportunities": {
|
||||
"id": "00000000-0000-0000-0000-000000000033",
|
||||
"id": "00000000-0000-0000-0000-000000000032",
|
||||
},
|
||||
},
|
||||
},
|
||||
"notes": {
|
||||
"id": "00000000-0000-0000-0000-000000000038",
|
||||
"id": "00000000-0000-0000-0000-000000000037",
|
||||
"widgets": {
|
||||
"notes": {
|
||||
"id": "00000000-0000-0000-0000-000000000039",
|
||||
"id": "00000000-0000-0000-0000-000000000038",
|
||||
},
|
||||
},
|
||||
},
|
||||
"tasks": {
|
||||
"id": "00000000-0000-0000-0000-000000000036",
|
||||
"id": "00000000-0000-0000-0000-000000000035",
|
||||
"widgets": {
|
||||
"tasks": {
|
||||
"id": "00000000-0000-0000-0000-000000000037",
|
||||
"id": "00000000-0000-0000-0000-000000000036",
|
||||
},
|
||||
},
|
||||
},
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000034",
|
||||
"id": "00000000-0000-0000-0000-000000000033",
|
||||
"widgets": {
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000035",
|
||||
"id": "00000000-0000-0000-0000-000000000034",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"taskRecordPage": {
|
||||
"id": "00000000-0000-0000-0000-000000000074",
|
||||
"id": "00000000-0000-0000-0000-000000000073",
|
||||
"tabs": {
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000083",
|
||||
"id": "00000000-0000-0000-0000-000000000081",
|
||||
"widgets": {
|
||||
"files": {
|
||||
"id": "00000000-0000-0000-0000-000000000084",
|
||||
"id": "00000000-0000-0000-0000-000000000082",
|
||||
},
|
||||
},
|
||||
},
|
||||
"home": {
|
||||
"id": "00000000-0000-0000-0000-000000000075",
|
||||
"id": "00000000-0000-0000-0000-000000000074",
|
||||
"widgets": {
|
||||
"assignee": {
|
||||
"id": "00000000-0000-0000-0000-000000000078",
|
||||
},
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000076",
|
||||
"id": "00000000-0000-0000-0000-000000000075",
|
||||
},
|
||||
"taskRichText": {
|
||||
"id": "00000000-0000-0000-0000-000000000077",
|
||||
"id": "00000000-0000-0000-0000-000000000076",
|
||||
},
|
||||
},
|
||||
},
|
||||
"note": {
|
||||
"id": "00000000-0000-0000-0000-000000000079",
|
||||
"id": "00000000-0000-0000-0000-000000000077",
|
||||
"widgets": {
|
||||
"taskRichText": {
|
||||
"id": "00000000-0000-0000-0000-000000000080",
|
||||
"id": "00000000-0000-0000-0000-000000000078",
|
||||
},
|
||||
},
|
||||
},
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000081",
|
||||
"id": "00000000-0000-0000-0000-000000000079",
|
||||
"widgets": {
|
||||
"timeline": {
|
||||
"id": "00000000-0000-0000-0000-000000000082",
|
||||
"id": "00000000-0000-0000-0000-000000000080",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"workflowRecordPage": {
|
||||
"id": "00000000-0000-0000-0000-000000000085",
|
||||
"id": "00000000-0000-0000-0000-000000000083",
|
||||
"tabs": {
|
||||
"flow": {
|
||||
"id": "00000000-0000-0000-0000-000000000086",
|
||||
"id": "00000000-0000-0000-0000-000000000084",
|
||||
"widgets": {
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000087",
|
||||
"id": "00000000-0000-0000-0000-000000000085",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"workflowRunRecordPage": {
|
||||
"id": "00000000-0000-0000-0000-000000000094",
|
||||
"id": "00000000-0000-0000-0000-000000000092",
|
||||
"tabs": {
|
||||
"flow": {
|
||||
"id": "00000000-0000-0000-0000-000000000098",
|
||||
"id": "00000000-0000-0000-0000-000000000096",
|
||||
"widgets": {
|
||||
"workflowRun": {
|
||||
"id": "00000000-0000-0000-0000-000000000099",
|
||||
"id": "00000000-0000-0000-0000-000000000097",
|
||||
},
|
||||
},
|
||||
},
|
||||
"home": {
|
||||
"id": "00000000-0000-0000-0000-000000000095",
|
||||
"id": "00000000-0000-0000-0000-000000000093",
|
||||
"widgets": {
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000096",
|
||||
"id": "00000000-0000-0000-0000-000000000094",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000097",
|
||||
"id": "00000000-0000-0000-0000-000000000095",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"workflowVersionRecordPage": {
|
||||
"id": "00000000-0000-0000-0000-000000000088",
|
||||
"id": "00000000-0000-0000-0000-000000000086",
|
||||
"tabs": {
|
||||
"flow": {
|
||||
"id": "00000000-0000-0000-0000-000000000092",
|
||||
"id": "00000000-0000-0000-0000-000000000090",
|
||||
"widgets": {
|
||||
"workflowVersion": {
|
||||
"id": "00000000-0000-0000-0000-000000000093",
|
||||
"id": "00000000-0000-0000-0000-000000000091",
|
||||
},
|
||||
},
|
||||
},
|
||||
"home": {
|
||||
"id": "00000000-0000-0000-0000-000000000089",
|
||||
"id": "00000000-0000-0000-0000-000000000087",
|
||||
"widgets": {
|
||||
"fields": {
|
||||
"id": "00000000-0000-0000-0000-000000000090",
|
||||
"id": "00000000-0000-0000-0000-000000000088",
|
||||
},
|
||||
"workflow": {
|
||||
"id": "00000000-0000-0000-0000-000000000091",
|
||||
"id": "00000000-0000-0000-0000-000000000089",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+1
-10
@@ -31,21 +31,12 @@ const COMPANY_PAGE_TABS = {
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECTS.company.fields.people.universalIdentifier,
|
||||
},
|
||||
accountOwner: {
|
||||
universalIdentifier: '20202020-ac01-4001-8001-c0aba11c0113',
|
||||
title: 'Account Owner',
|
||||
type: WidgetType.FIELD,
|
||||
gridPosition: GRID_POSITIONS.FULL_WIDTH,
|
||||
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECTS.company.fields.accountOwner.universalIdentifier,
|
||||
},
|
||||
opportunities: {
|
||||
universalIdentifier: '20202020-ac01-4001-8001-c0aba11c0114',
|
||||
title: 'Opportunities',
|
||||
type: WidgetType.FIELD,
|
||||
gridPosition: GRID_POSITIONS.FULL_WIDTH,
|
||||
position: VERTICAL_LIST_LAYOUT_POSITIONS.FOURTH,
|
||||
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECTS.company.fields.opportunities.universalIdentifier,
|
||||
},
|
||||
|
||||
-12
@@ -1,13 +1,10 @@
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
|
||||
import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
|
||||
import {
|
||||
CONDITIONAL_DISPLAY_DEVICE_DESKTOP,
|
||||
CONDITIONAL_DISPLAY_DEVICE_MOBILE,
|
||||
GRID_POSITIONS,
|
||||
TAB_PROPS,
|
||||
VERTICAL_LIST_LAYOUT_POSITIONS,
|
||||
WIDGET_PROPS,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-page-layout-tabs.template';
|
||||
import {
|
||||
@@ -32,15 +29,6 @@ const TASK_PAGE_TABS = {
|
||||
position: { layoutMode: TAB_PROPS.home.layoutMode, index: 1 },
|
||||
conditionalDisplay: CONDITIONAL_DISPLAY_DEVICE_MOBILE,
|
||||
},
|
||||
assignee: {
|
||||
universalIdentifier: '20202020-ac05-4005-8005-ba5ca11a5513',
|
||||
title: 'Assignee',
|
||||
type: WidgetType.FIELD,
|
||||
gridPosition: GRID_POSITIONS.FULL_WIDTH,
|
||||
position: VERTICAL_LIST_LAYOUT_POSITIONS.THIRD,
|
||||
fieldUniversalIdentifier:
|
||||
STANDARD_OBJECTS.task.fields.assignee.universalIdentifier,
|
||||
},
|
||||
},
|
||||
},
|
||||
note: {
|
||||
|
||||
-13
@@ -45,19 +45,6 @@ export const computeStandardBlocklistViewFields = (
|
||||
},
|
||||
}),
|
||||
|
||||
blocklistRecordPageFieldsHandle: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'blocklist',
|
||||
context: {
|
||||
viewName: 'blocklistRecordPageFields',
|
||||
viewFieldName: 'handle',
|
||||
fieldName: 'handle',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
blocklistRecordPageFieldsWorkspaceMember:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
|
||||
-13
@@ -81,19 +81,6 @@ export const computeStandardCalendarChannelViewFields = (
|
||||
},
|
||||
}),
|
||||
|
||||
calendarChannelRecordPageFieldsHandle: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'calendarChannel',
|
||||
context: {
|
||||
viewName: 'calendarChannelRecordPageFields',
|
||||
viewFieldName: 'handle',
|
||||
fieldName: 'handle',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
calendarChannelRecordPageFieldsConnectedAccount:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
|
||||
+118
@@ -245,5 +245,123 @@ export const computeStandardCompanyViewFields = (
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
companyRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'company',
|
||||
context: {
|
||||
viewName: 'companyRecordPageFields',
|
||||
viewFieldName: 'updatedAt',
|
||||
fieldName: 'updatedAt',
|
||||
position: 6,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
companyRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'company',
|
||||
context: {
|
||||
viewName: 'companyRecordPageFields',
|
||||
viewFieldName: 'updatedBy',
|
||||
fieldName: 'updatedBy',
|
||||
position: 7,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
companyRecordPageFieldsPeople: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'company',
|
||||
context: {
|
||||
viewName: 'companyRecordPageFields',
|
||||
viewFieldName: 'people',
|
||||
fieldName: 'people',
|
||||
position: 4,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
companyRecordPageFieldsTaskTargets: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'company',
|
||||
context: {
|
||||
viewName: 'companyRecordPageFields',
|
||||
viewFieldName: 'taskTargets',
|
||||
fieldName: 'taskTargets',
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
companyRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'company',
|
||||
context: {
|
||||
viewName: 'companyRecordPageFields',
|
||||
viewFieldName: 'noteTargets',
|
||||
fieldName: 'noteTargets',
|
||||
position: 6,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
companyRecordPageFieldsOpportunities: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'company',
|
||||
context: {
|
||||
viewName: 'companyRecordPageFields',
|
||||
viewFieldName: 'opportunities',
|
||||
fieldName: 'opportunities',
|
||||
position: 7,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
companyRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'company',
|
||||
context: {
|
||||
viewName: 'companyRecordPageFields',
|
||||
viewFieldName: 'favorites',
|
||||
fieldName: 'favorites',
|
||||
position: 8,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
companyRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'company',
|
||||
context: {
|
||||
viewName: 'companyRecordPageFields',
|
||||
viewFieldName: 'attachments',
|
||||
fieldName: 'attachments',
|
||||
position: 9,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
companyRecordPageFieldsTimelineActivities:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'company',
|
||||
context: {
|
||||
viewName: 'companyRecordPageFields',
|
||||
viewFieldName: 'timelineActivities',
|
||||
fieldName: 'timelineActivities',
|
||||
position: 10,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
-15
@@ -69,21 +69,6 @@ export const computeStandardConnectedAccountViewFields = (
|
||||
},
|
||||
}),
|
||||
|
||||
connectedAccountRecordPageFieldsHandle: createStandardViewFieldFlatMetadata(
|
||||
{
|
||||
...args,
|
||||
objectName: 'connectedAccount',
|
||||
context: {
|
||||
viewName: 'connectedAccountRecordPageFields',
|
||||
viewFieldName: 'handle',
|
||||
fieldName: 'handle',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
},
|
||||
),
|
||||
connectedAccountRecordPageFieldsProvider:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
|
||||
-13
@@ -33,19 +33,6 @@ export const computeStandardFavoriteFolderViewFields = (
|
||||
},
|
||||
}),
|
||||
|
||||
favoriteFolderRecordPageFieldsName: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'favoriteFolder',
|
||||
context: {
|
||||
viewName: 'favoriteFolderRecordPageFields',
|
||||
viewFieldName: 'name',
|
||||
fieldName: 'name',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
favoriteFolderRecordPageFieldsCreatedAt:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
|
||||
-13
@@ -93,19 +93,6 @@ export const computeStandardMessageChannelViewFields = (
|
||||
},
|
||||
}),
|
||||
|
||||
messageChannelRecordPageFieldsHandle: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'messageChannel',
|
||||
context: {
|
||||
viewName: 'messageChannelRecordPageFields',
|
||||
viewFieldName: 'handle',
|
||||
fieldName: 'handle',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
messageChannelRecordPageFieldsConnectedAccount:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
|
||||
-13
@@ -69,19 +69,6 @@ export const computeStandardMessageFolderViewFields = (
|
||||
},
|
||||
}),
|
||||
|
||||
messageFolderRecordPageFieldsName: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'messageFolder',
|
||||
context: {
|
||||
viewName: 'messageFolderRecordPageFields',
|
||||
viewFieldName: 'name',
|
||||
fieldName: 'name',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
messageFolderRecordPageFieldsMessageChannel:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
|
||||
-14
@@ -93,20 +93,6 @@ export const computeStandardMessageParticipantViewFields = (
|
||||
},
|
||||
}),
|
||||
|
||||
messageParticipantRecordPageFieldsHandle:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'messageParticipant',
|
||||
context: {
|
||||
viewName: 'messageParticipantRecordPageFields',
|
||||
viewFieldName: 'handle',
|
||||
fieldName: 'handle',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
messageParticipantRecordPageFieldsMessage:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
|
||||
+80
-13
@@ -70,19 +70,6 @@ export const computeStandardNoteViewFields = (
|
||||
}),
|
||||
|
||||
// noteRecordPageFields view fields
|
||||
noteRecordPageFieldsTitle: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'note',
|
||||
context: {
|
||||
viewName: 'noteRecordPageFields',
|
||||
viewFieldName: 'title',
|
||||
fieldName: 'title',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
noteRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'note',
|
||||
@@ -122,5 +109,85 @@ export const computeStandardNoteViewFields = (
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
noteRecordPageFieldsBodyV2: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'note',
|
||||
context: {
|
||||
viewName: 'noteRecordPageFields',
|
||||
viewFieldName: 'bodyV2',
|
||||
fieldName: 'bodyV2',
|
||||
position: 0,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'additional',
|
||||
},
|
||||
}),
|
||||
noteRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'note',
|
||||
context: {
|
||||
viewName: 'noteRecordPageFields',
|
||||
viewFieldName: 'updatedAt',
|
||||
fieldName: 'updatedAt',
|
||||
position: 2,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
noteRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'note',
|
||||
context: {
|
||||
viewName: 'noteRecordPageFields',
|
||||
viewFieldName: 'updatedBy',
|
||||
fieldName: 'updatedBy',
|
||||
position: 3,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
noteRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'note',
|
||||
context: {
|
||||
viewName: 'noteRecordPageFields',
|
||||
viewFieldName: 'attachments',
|
||||
fieldName: 'attachments',
|
||||
position: 2,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
noteRecordPageFieldsTimelineActivities: createStandardViewFieldFlatMetadata(
|
||||
{
|
||||
...args,
|
||||
objectName: 'note',
|
||||
context: {
|
||||
viewName: 'noteRecordPageFields',
|
||||
viewFieldName: 'timelineActivities',
|
||||
fieldName: 'timelineActivities',
|
||||
position: 3,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
},
|
||||
),
|
||||
noteRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'note',
|
||||
context: {
|
||||
viewName: 'noteRecordPageFields',
|
||||
viewFieldName: 'favorites',
|
||||
fieldName: 'favorites',
|
||||
position: 4,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+98
@@ -266,5 +266,103 @@ export const computeStandardOpportunityViewFields = (
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
opportunityRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'opportunity',
|
||||
context: {
|
||||
viewName: 'opportunityRecordPageFields',
|
||||
viewFieldName: 'updatedAt',
|
||||
fieldName: 'updatedAt',
|
||||
position: 2,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
opportunityRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'opportunity',
|
||||
context: {
|
||||
viewName: 'opportunityRecordPageFields',
|
||||
viewFieldName: 'updatedBy',
|
||||
fieldName: 'updatedBy',
|
||||
position: 3,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
opportunityRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'opportunity',
|
||||
context: {
|
||||
viewName: 'opportunityRecordPageFields',
|
||||
viewFieldName: 'favorites',
|
||||
fieldName: 'favorites',
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
opportunityRecordPageFieldsTaskTargets: createStandardViewFieldFlatMetadata(
|
||||
{
|
||||
...args,
|
||||
objectName: 'opportunity',
|
||||
context: {
|
||||
viewName: 'opportunityRecordPageFields',
|
||||
viewFieldName: 'taskTargets',
|
||||
fieldName: 'taskTargets',
|
||||
position: 6,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
},
|
||||
),
|
||||
opportunityRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata(
|
||||
{
|
||||
...args,
|
||||
objectName: 'opportunity',
|
||||
context: {
|
||||
viewName: 'opportunityRecordPageFields',
|
||||
viewFieldName: 'noteTargets',
|
||||
fieldName: 'noteTargets',
|
||||
position: 7,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
},
|
||||
),
|
||||
opportunityRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata(
|
||||
{
|
||||
...args,
|
||||
objectName: 'opportunity',
|
||||
context: {
|
||||
viewName: 'opportunityRecordPageFields',
|
||||
viewFieldName: 'attachments',
|
||||
fieldName: 'attachments',
|
||||
position: 8,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
},
|
||||
),
|
||||
opportunityRecordPageFieldsTimelineActivities:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'opportunity',
|
||||
context: {
|
||||
viewName: 'opportunityRecordPageFields',
|
||||
viewFieldName: 'timelineActivities',
|
||||
fieldName: 'timelineActivities',
|
||||
position: 9,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+147
@@ -266,5 +266,152 @@ export const computeStandardPersonViewFields = (
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'updatedAt',
|
||||
fieldName: 'updatedAt',
|
||||
position: 4,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'updatedBy',
|
||||
fieldName: 'updatedBy',
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsAvatarFile: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'avatarFile',
|
||||
fieldName: 'avatarFile',
|
||||
position: 0,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'additional',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsPointOfContactForOpportunities:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'pointOfContactForOpportunities',
|
||||
fieldName: 'pointOfContactForOpportunities',
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsTaskTargets: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'taskTargets',
|
||||
fieldName: 'taskTargets',
|
||||
position: 6,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsNoteTargets: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'noteTargets',
|
||||
fieldName: 'noteTargets',
|
||||
position: 7,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'favorites',
|
||||
fieldName: 'favorites',
|
||||
position: 8,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'attachments',
|
||||
fieldName: 'attachments',
|
||||
position: 9,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsMessageParticipants:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'messageParticipants',
|
||||
fieldName: 'messageParticipants',
|
||||
position: 10,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsCalendarEventParticipants:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'calendarEventParticipants',
|
||||
fieldName: 'calendarEventParticipants',
|
||||
position: 11,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
personRecordPageFieldsTimelineActivities:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'person',
|
||||
context: {
|
||||
viewName: 'personRecordPageFields',
|
||||
viewFieldName: 'timelineActivities',
|
||||
fieldName: 'timelineActivities',
|
||||
position: 12,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+80
-13
@@ -255,19 +255,6 @@ export const computeStandardTaskViewFields = (
|
||||
}),
|
||||
|
||||
// taskRecordPageFields view fields
|
||||
taskRecordPageFieldsTitle: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'task',
|
||||
context: {
|
||||
viewName: 'taskRecordPageFields',
|
||||
viewFieldName: 'title',
|
||||
fieldName: 'title',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
taskRecordPageFieldsDueAt: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'task',
|
||||
@@ -346,5 +333,85 @@ export const computeStandardTaskViewFields = (
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
taskRecordPageFieldsBodyV2: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'task',
|
||||
context: {
|
||||
viewName: 'taskRecordPageFields',
|
||||
viewFieldName: 'bodyV2',
|
||||
fieldName: 'bodyV2',
|
||||
position: 0,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'additional',
|
||||
},
|
||||
}),
|
||||
taskRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'task',
|
||||
context: {
|
||||
viewName: 'taskRecordPageFields',
|
||||
viewFieldName: 'updatedAt',
|
||||
fieldName: 'updatedAt',
|
||||
position: 2,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
taskRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'task',
|
||||
context: {
|
||||
viewName: 'taskRecordPageFields',
|
||||
viewFieldName: 'updatedBy',
|
||||
fieldName: 'updatedBy',
|
||||
position: 3,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
taskRecordPageFieldsAttachments: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'task',
|
||||
context: {
|
||||
viewName: 'taskRecordPageFields',
|
||||
viewFieldName: 'attachments',
|
||||
fieldName: 'attachments',
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
taskRecordPageFieldsTimelineActivities: createStandardViewFieldFlatMetadata(
|
||||
{
|
||||
...args,
|
||||
objectName: 'task',
|
||||
context: {
|
||||
viewName: 'taskRecordPageFields',
|
||||
viewFieldName: 'timelineActivities',
|
||||
fieldName: 'timelineActivities',
|
||||
position: 6,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
},
|
||||
),
|
||||
taskRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'task',
|
||||
context: {
|
||||
viewName: 'taskRecordPageFields',
|
||||
viewFieldName: 'favorites',
|
||||
fieldName: 'favorites',
|
||||
position: 7,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+105
-13
@@ -46,19 +46,6 @@ export const computeStandardWorkflowRunViewFields = (
|
||||
}),
|
||||
|
||||
// workflowRunRecordPageFields view fields
|
||||
workflowRunRecordPageFieldsName: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
context: {
|
||||
viewName: 'workflowRunRecordPageFields',
|
||||
viewFieldName: 'name',
|
||||
fieldName: 'name',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
workflowRunRecordPageFieldsStatus: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
@@ -151,5 +138,110 @@ export const computeStandardWorkflowRunViewFields = (
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
workflowRunRecordPageFieldsEnqueuedAt: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
context: {
|
||||
viewName: 'workflowRunRecordPageFields',
|
||||
viewFieldName: 'enqueuedAt',
|
||||
fieldName: 'enqueuedAt',
|
||||
position: 0,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'additional',
|
||||
},
|
||||
}),
|
||||
workflowRunRecordPageFieldsOutput: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
context: {
|
||||
viewName: 'workflowRunRecordPageFields',
|
||||
viewFieldName: 'output',
|
||||
fieldName: 'output',
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
workflowRunRecordPageFieldsContext: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
context: {
|
||||
viewName: 'workflowRunRecordPageFields',
|
||||
viewFieldName: 'context',
|
||||
fieldName: 'context',
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
workflowRunRecordPageFieldsState: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
context: {
|
||||
viewName: 'workflowRunRecordPageFields',
|
||||
viewFieldName: 'state',
|
||||
fieldName: 'state',
|
||||
position: 8,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
workflowRunRecordPageFieldsUpdatedAt: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
context: {
|
||||
viewName: 'workflowRunRecordPageFields',
|
||||
viewFieldName: 'updatedAt',
|
||||
fieldName: 'updatedAt',
|
||||
position: 2,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
workflowRunRecordPageFieldsUpdatedBy: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
context: {
|
||||
viewName: 'workflowRunRecordPageFields',
|
||||
viewFieldName: 'updatedBy',
|
||||
fieldName: 'updatedBy',
|
||||
position: 3,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
workflowRunRecordPageFieldsFavorites: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
context: {
|
||||
viewName: 'workflowRunRecordPageFields',
|
||||
viewFieldName: 'favorites',
|
||||
fieldName: 'favorites',
|
||||
position: 9,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
workflowRunRecordPageFieldsTimelineActivities:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowRun',
|
||||
context: {
|
||||
viewName: 'workflowRunRecordPageFields',
|
||||
viewFieldName: 'timelineActivities',
|
||||
fieldName: 'timelineActivities',
|
||||
position: 10,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+96
-13
@@ -70,19 +70,6 @@ export const computeStandardWorkflowVersionViewFields = (
|
||||
}),
|
||||
|
||||
// workflowVersionRecordPageFields view fields
|
||||
workflowVersionRecordPageFieldsName: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowVersion',
|
||||
context: {
|
||||
viewName: 'workflowVersionRecordPageFields',
|
||||
viewFieldName: 'name',
|
||||
fieldName: 'name',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
workflowVersionRecordPageFieldsStatus: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowVersion',
|
||||
@@ -139,5 +126,101 @@ export const computeStandardWorkflowVersionViewFields = (
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
workflowVersionRecordPageFieldsSteps: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowVersion',
|
||||
context: {
|
||||
viewName: 'workflowVersionRecordPageFields',
|
||||
viewFieldName: 'steps',
|
||||
fieldName: 'steps',
|
||||
position: 0,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'additional',
|
||||
},
|
||||
}),
|
||||
workflowVersionRecordPageFieldsCreatedBy:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowVersion',
|
||||
context: {
|
||||
viewName: 'workflowVersionRecordPageFields',
|
||||
viewFieldName: 'createdBy',
|
||||
fieldName: 'createdBy',
|
||||
position: 1,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
workflowVersionRecordPageFieldsUpdatedAt:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowVersion',
|
||||
context: {
|
||||
viewName: 'workflowVersionRecordPageFields',
|
||||
viewFieldName: 'updatedAt',
|
||||
fieldName: 'updatedAt',
|
||||
position: 2,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
workflowVersionRecordPageFieldsUpdatedBy:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowVersion',
|
||||
context: {
|
||||
viewName: 'workflowVersionRecordPageFields',
|
||||
viewFieldName: 'updatedBy',
|
||||
fieldName: 'updatedBy',
|
||||
position: 3,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'other',
|
||||
},
|
||||
}),
|
||||
workflowVersionRecordPageFieldsRuns: createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowVersion',
|
||||
context: {
|
||||
viewName: 'workflowVersionRecordPageFields',
|
||||
viewFieldName: 'runs',
|
||||
fieldName: 'runs',
|
||||
position: 4,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
workflowVersionRecordPageFieldsFavorites:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowVersion',
|
||||
context: {
|
||||
viewName: 'workflowVersionRecordPageFields',
|
||||
viewFieldName: 'favorites',
|
||||
fieldName: 'favorites',
|
||||
position: 5,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
workflowVersionRecordPageFieldsTimelineActivities:
|
||||
createStandardViewFieldFlatMetadata({
|
||||
...args,
|
||||
objectName: 'workflowVersion',
|
||||
context: {
|
||||
viewName: 'workflowVersionRecordPageFields',
|
||||
viewFieldName: 'timelineActivities',
|
||||
fieldName: 'timelineActivities',
|
||||
position: 6,
|
||||
isVisible: false,
|
||||
size: 150,
|
||||
viewFieldGroupName: 'general',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
-1
@@ -4,5 +4,4 @@ export const DEFAULT_FEATURE_FLAGS = [
|
||||
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
] as const satisfies FeatureFlagKey[];
|
||||
|
||||
+18
-6
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { ViewType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
@@ -95,8 +96,9 @@ export class FlatViewFieldValidatorService {
|
||||
}
|
||||
|
||||
if (
|
||||
flatView.type !== ViewType.FIELDS_WIDGET &&
|
||||
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
|
||||
updatedFlatViewField.fieldMetadataUniversalIdentifier
|
||||
updatedFlatViewField.fieldMetadataUniversalIdentifier
|
||||
) {
|
||||
const otherFlatViewFields =
|
||||
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow(
|
||||
@@ -127,6 +129,7 @@ export class FlatViewFieldValidatorService {
|
||||
flatViewFieldMaps: optimisticFlatViewFieldMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatViewMaps,
|
||||
},
|
||||
}: UniversalFlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.viewField
|
||||
@@ -177,11 +180,18 @@ export class FlatViewFieldValidatorService {
|
||||
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
|
||||
existingFlatViewField.fieldMetadataUniversalIdentifier
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: ViewExceptionCode.INVALID_VIEW_DATA,
|
||||
message: t`Label identifier view field cannot be deleted`,
|
||||
userFriendlyMessage: msg`Label identifier view field cannot be deleted`,
|
||||
const flatView = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier: existingFlatViewField.viewUniversalIdentifier,
|
||||
flatEntityMaps: flatViewMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatView) || flatView.type !== ViewType.FIELDS_WIDGET) {
|
||||
validationResult.errors.push({
|
||||
code: ViewExceptionCode.INVALID_VIEW_DATA,
|
||||
message: t`Label identifier view field cannot be deleted`,
|
||||
userFriendlyMessage: msg`Label identifier view field cannot be deleted`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
@@ -289,8 +299,9 @@ export class FlatViewFieldValidatorService {
|
||||
}
|
||||
|
||||
if (
|
||||
flatView.type !== ViewType.FIELDS_WIDGET &&
|
||||
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
|
||||
flatViewFieldToValidate.fieldMetadataUniversalIdentifier
|
||||
flatViewFieldToValidate.fieldMetadataUniversalIdentifier
|
||||
) {
|
||||
validationResult.errors.push(
|
||||
...validateLabelIdentifierFieldMetadataIdFlatViewField({
|
||||
@@ -299,6 +310,7 @@ export class FlatViewFieldValidatorService {
|
||||
}),
|
||||
);
|
||||
} else if (
|
||||
flatView.type !== ViewType.FIELDS_WIDGET &&
|
||||
otherFlatViewFields.some(
|
||||
(flatViewField) =>
|
||||
flatViewField.fieldMetadataUniversalIdentifier ===
|
||||
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
import { createOneSelectFieldMetadataForIntegrationTests } from 'test/integration/metadata/suites/field-metadata/utils/create-one-select-field-metadata-for-integration-tests.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { createManyViewGroups } from 'test/integration/metadata/suites/view-group/utils/create-many-view-groups.util';
|
||||
import { deleteOneViewGroup } from 'test/integration/metadata/suites/view-group/utils/delete-one-view-group.util';
|
||||
import { destroyOneViewGroup } from 'test/integration/metadata/suites/view-group/utils/destroy-one-view-group.util';
|
||||
import { updateManyViewGroups } from 'test/integration/metadata/suites/view-group/utils/update-many-view-groups.util';
|
||||
import { createOneView } from 'test/integration/metadata/suites/view/utils/create-one-view.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CreateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/create-view-group.input';
|
||||
|
||||
describe('View Group Resolver - Successful Update Many Operations - v2', () => {
|
||||
let testSetup: {
|
||||
testViewId: string;
|
||||
testObjectMetadataId: string;
|
||||
};
|
||||
let createdViewGroupIds: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: 'myUpdateManyGroupTestObjectV2',
|
||||
namePlural: 'myUpdateManyGroupTestObjectsV2',
|
||||
labelSingular: 'My Update Many Group Test Object v2',
|
||||
labelPlural: 'My Update Many Group Test Objects v2',
|
||||
icon: 'Icon123',
|
||||
},
|
||||
});
|
||||
|
||||
const { selectFieldMetadataId } =
|
||||
await createOneSelectFieldMetadataForIntegrationTests({
|
||||
input: {
|
||||
objectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: {
|
||||
createView: { id: testViewId },
|
||||
},
|
||||
} = await createOneView({
|
||||
input: {
|
||||
icon: 'icon123',
|
||||
objectMetadataId,
|
||||
name: 'TestViewForUpdateManyGroups',
|
||||
mainGroupByFieldMetadataId: selectFieldMetadataId,
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
testSetup = {
|
||||
testViewId,
|
||||
testObjectMetadataId: objectMetadataId,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
input: {
|
||||
idToUpdate: testSetup.testObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: testSetup.testObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
for (const viewGroupId of createdViewGroupIds) {
|
||||
if (isDefined(viewGroupId)) {
|
||||
const {
|
||||
data: { deleteViewGroup },
|
||||
} = await deleteOneViewGroup({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: viewGroupId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(deleteViewGroup.deletedAt).not.toBeNull();
|
||||
await destroyOneViewGroup({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
id: viewGroupId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
createdViewGroupIds = [];
|
||||
});
|
||||
|
||||
it('should batch-update positions of multiple view groups at once', async () => {
|
||||
const createInputs: CreateViewGroupInput[] = [
|
||||
{
|
||||
viewId: testSetup.testViewId,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fieldValue: 'Group A',
|
||||
},
|
||||
{
|
||||
viewId: testSetup.testViewId,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
fieldValue: 'Group B',
|
||||
},
|
||||
{
|
||||
viewId: testSetup.testViewId,
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
fieldValue: 'Group C',
|
||||
},
|
||||
];
|
||||
|
||||
const {
|
||||
data: { createManyViewGroups: createdGroups },
|
||||
} = await createManyViewGroups({
|
||||
inputs: createInputs,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdViewGroupIds = createdGroups.map(
|
||||
(viewGroup: { id: string }) => viewGroup.id,
|
||||
);
|
||||
|
||||
const {
|
||||
data: { updateManyViewGroups: updatedGroups },
|
||||
errors,
|
||||
} = await updateManyViewGroups({
|
||||
inputs: [
|
||||
{ id: createdGroups[0].id, update: { position: 2 } },
|
||||
{ id: createdGroups[1].id, update: { position: 0 } },
|
||||
{ id: createdGroups[2].id, update: { position: 1 } },
|
||||
],
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(updatedGroups).toBeDefined();
|
||||
expect(updatedGroups).toHaveLength(3);
|
||||
|
||||
expect(updatedGroups[0]).toMatchObject({
|
||||
id: createdGroups[0].id,
|
||||
position: 2,
|
||||
});
|
||||
expect(updatedGroups[1]).toMatchObject({
|
||||
id: createdGroups[1].id,
|
||||
position: 0,
|
||||
});
|
||||
expect(updatedGroups[2]).toMatchObject({
|
||||
id: createdGroups[2].id,
|
||||
position: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should batch-update visibility of multiple view groups at once', async () => {
|
||||
const createInputs: CreateViewGroupInput[] = [
|
||||
{
|
||||
viewId: testSetup.testViewId,
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fieldValue: 'Visible Group',
|
||||
},
|
||||
{
|
||||
viewId: testSetup.testViewId,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
fieldValue: 'To Be Hidden Group',
|
||||
},
|
||||
];
|
||||
|
||||
const {
|
||||
data: { createManyViewGroups: createdGroups },
|
||||
} = await createManyViewGroups({
|
||||
inputs: createInputs,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdViewGroupIds = createdGroups.map(
|
||||
(viewGroup: { id: string }) => viewGroup.id,
|
||||
);
|
||||
|
||||
const {
|
||||
data: { updateManyViewGroups: updatedGroups },
|
||||
errors,
|
||||
} = await updateManyViewGroups({
|
||||
inputs: [
|
||||
{ id: createdGroups[0].id, update: { isVisible: false } },
|
||||
{
|
||||
id: createdGroups[1].id,
|
||||
update: { isVisible: false, position: 5 },
|
||||
},
|
||||
],
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(updatedGroups).toBeDefined();
|
||||
expect(updatedGroups).toHaveLength(2);
|
||||
|
||||
expect(updatedGroups[0]).toMatchObject({
|
||||
id: createdGroups[0].id,
|
||||
isVisible: false,
|
||||
});
|
||||
expect(updatedGroups[1]).toMatchObject({
|
||||
id: createdGroups[1].id,
|
||||
isVisible: false,
|
||||
position: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array for empty inputs', async () => {
|
||||
const {
|
||||
data: { updateManyViewGroups: updatedGroups },
|
||||
errors,
|
||||
} = await updateManyViewGroups({
|
||||
inputs: [],
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(errors).toBeUndefined();
|
||||
expect(updatedGroups).toBeDefined();
|
||||
expect(updatedGroups).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+45
-1695
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -93,6 +93,6 @@ describe('View side effect on object creation', () => {
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(createdViewFields.length).toBe(7);
|
||||
expect(createdViewFields.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user