Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d914039d45 | ||
|
|
c2b058a6a7 | ||
|
|
22c9693ce5 | ||
|
|
50ea560e57 | ||
|
|
56056b885d | ||
|
|
db9da3194f | ||
|
|
68f5e70ade | ||
|
|
08077476f3 | ||
|
|
6f0ac88e20 |
@@ -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,
|
||||
|
||||
@@ -31,8 +31,6 @@ import { useCallback, useState } from 'react';
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
|
||||
export const useAgentChat = (
|
||||
uiMessages: ExtendedUIMessage[],
|
||||
ensureThreadIdForSend: () => Promise<string | null>,
|
||||
@@ -94,7 +92,6 @@ export const useAgentChat = (
|
||||
return null;
|
||||
}
|
||||
|
||||
cookieStorage.setItem('tokenPair', JSON.stringify(renewedTokens));
|
||||
setTokenPair(renewedTokens);
|
||||
|
||||
const updatedHeaders = new Headers(init?.headers ?? {});
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { getGenericOperationName, isDefined } from 'twenty-shared/utils';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
const logger = loggerLink(() => 'Twenty');
|
||||
@@ -175,7 +174,6 @@ export class ApolloFactory implements ApolloManager {
|
||||
|
||||
if (isDefined(tokens)) {
|
||||
onTokenPairChange?.(tokens);
|
||||
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
|
||||
|
||||
export type NavigationMenuItemDropResult = {
|
||||
source: DropDestination;
|
||||
destination: DropDestination | null;
|
||||
draggableId: string;
|
||||
insertBeforeItemId?: string | null;
|
||||
};
|
||||
@@ -1,14 +1,14 @@
|
||||
import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig';
|
||||
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
|
||||
import type { DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
export const getDropTargetIdFromDestination = ({
|
||||
navigationMenuItemSection,
|
||||
destination,
|
||||
}: {
|
||||
navigationMenuItemSection: NavigationMenuItemSection;
|
||||
destination: DropResult['destination'];
|
||||
destination: DropDestination | null;
|
||||
}): string | null => {
|
||||
if (
|
||||
!destination ||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection';
|
||||
|
||||
type NavigationMenuItemDroppableProps = {
|
||||
droppableId: string;
|
||||
children: React.ReactNode;
|
||||
isDragIndicatorVisible?: boolean;
|
||||
showDropLine?: boolean;
|
||||
isWorkspaceSection?: boolean;
|
||||
};
|
||||
|
||||
const StyledDroppableWrapper = styled.div`
|
||||
position: relative;
|
||||
transition: all 150ms ease-in-out;
|
||||
width: 100%;
|
||||
|
||||
&[data-dragging-over='true'] {
|
||||
background-color: ${themeCssVariables.background.transparent.blue};
|
||||
}
|
||||
|
||||
&[data-dragging-over='true'][data-show-drop-line='true']::before {
|
||||
background-color: ${themeCssVariables.color.blue};
|
||||
border-radius: ${themeCssVariables.border.radius.sm}
|
||||
${themeCssVariables.border.radius.sm} 0 0;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 2px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const NavigationMenuItemDroppable = ({
|
||||
droppableId,
|
||||
children,
|
||||
isDragIndicatorVisible = true,
|
||||
showDropLine = true,
|
||||
isWorkspaceSection = false,
|
||||
}: NavigationMenuItemDroppableProps) => {
|
||||
const isDropDisabled = useIsDropDisabledForSection(isWorkspaceSection);
|
||||
|
||||
return (
|
||||
<Droppable droppableId={droppableId} isDropDisabled={isDropDisabled}>
|
||||
{(provided, snapshot) => (
|
||||
<StyledDroppableWrapper
|
||||
data-dragging-over={
|
||||
snapshot.isDraggingOver && isDragIndicatorVisible
|
||||
? 'true'
|
||||
: undefined
|
||||
}
|
||||
data-show-drop-line={showDropLine ? 'true' : undefined}
|
||||
>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{children}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
</StyledDroppableWrapper>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export const DROP_RESULT_OPTIONS = {
|
||||
reason: 'DROP' as const,
|
||||
combine: null,
|
||||
mode: 'FLUID' as const,
|
||||
type: 'DEFAULT' as const,
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { DropResult, ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -28,6 +27,8 @@ import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { useStore } from 'jotai';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import type { NavigationMenuItemDropResult } from '@/navigation-menu-item/common/types/navigationMenuItemDropResult';
|
||||
|
||||
export const useHandleAddToNavigationDrop = () => {
|
||||
const store = useStore();
|
||||
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
|
||||
@@ -50,7 +51,7 @@ export const useHandleAddToNavigationDrop = () => {
|
||||
);
|
||||
|
||||
const handleAddToNavigationDrop = useCallback(
|
||||
(result: DropResult, _provided: ResponderProvided) => {
|
||||
(result: NavigationMenuItemDropResult) => {
|
||||
const { source, destination, draggableId } = result;
|
||||
if (
|
||||
source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID ||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useStore } from 'jotai';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import type { NavigationMenuItemDropResult } from '@/navigation-menu-item/common/types/navigationMenuItemDropResult';
|
||||
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { useUpdateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems';
|
||||
import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig';
|
||||
@@ -115,10 +116,8 @@ export const useHandleNavigationMenuItemDragAndDrop = (
|
||||
});
|
||||
};
|
||||
|
||||
const handleNavigationMenuItemDragAndDrop: OnDragEndResponder = async (
|
||||
result: Parameters<OnDragEndResponder>[0] & {
|
||||
insertBeforeItemId?: string | null;
|
||||
},
|
||||
const handleNavigationMenuItemDragAndDrop = async (
|
||||
result: NavigationMenuItemDropResult,
|
||||
) => {
|
||||
const { destination, source, draggableId } = result;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { type DragDropProvider } from '@dnd-kit/react';
|
||||
import { isSortable } from '@dnd-kit/react/sortable';
|
||||
import type { ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { useStore } from 'jotai';
|
||||
import { type ComponentProps, useCallback, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -11,17 +10,16 @@ import { NavigationSections } from '@/navigation-menu-item/common/constants/Navi
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
|
||||
import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData';
|
||||
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
|
||||
import type { NavigationMenuItemDropResult } from '@/navigation-menu-item/common/types/navigationMenuItemDropResult';
|
||||
import type { SortableTargetDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
|
||||
import { extractFolderIdFromDroppableId } from '@/navigation-menu-item/common/utils/extractFolderIdFromDroppableId';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { DROP_RESULT_OPTIONS } from '@/navigation-menu-item/display/dnd/constants/navigationMenuItemDndKitDropResultOptions';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop';
|
||||
import { resolveDropTarget } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget';
|
||||
import { toDropResult } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitToDropResult';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
@@ -172,23 +170,13 @@ export const useNavigationMenuItemDndKit = (
|
||||
destination: DropDestination,
|
||||
insertBeforeItemId?: string | null,
|
||||
) => {
|
||||
const result = toDropResult(
|
||||
id,
|
||||
{
|
||||
sourceDroppableId: source.droppableId,
|
||||
sourceIndex: source.index,
|
||||
},
|
||||
const result: NavigationMenuItemDropResult = {
|
||||
draggableId: id,
|
||||
source,
|
||||
destination,
|
||||
);
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
handleNavigationMenuItemDragAndDrop(
|
||||
{
|
||||
...result,
|
||||
...DROP_RESULT_OPTIONS,
|
||||
...(insertBeforeItemId != null && { insertBeforeItemId }),
|
||||
},
|
||||
provided,
|
||||
);
|
||||
...(insertBeforeItemId != null && { insertBeforeItemId }),
|
||||
};
|
||||
handleNavigationMenuItemDragAndDrop(result);
|
||||
},
|
||||
[handleNavigationMenuItemDragAndDrop],
|
||||
);
|
||||
@@ -351,12 +339,18 @@ export const useNavigationMenuItemDndKit = (
|
||||
destination = fallback;
|
||||
}
|
||||
|
||||
const result = toDropResult(draggableId, data, destination);
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
const dropResult = { ...result, ...DROP_RESULT_OPTIONS };
|
||||
const dropResult: NavigationMenuItemDropResult = {
|
||||
draggableId,
|
||||
source: {
|
||||
droppableId: data?.sourceDroppableId ?? '',
|
||||
index: data?.sourceIndex ?? 0,
|
||||
},
|
||||
destination,
|
||||
...(insertBeforeItemId != null && { insertBeforeItemId }),
|
||||
};
|
||||
|
||||
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
handleAddToNavigationDrop(dropResult, provided);
|
||||
handleAddToNavigationDrop(dropResult);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -386,7 +380,7 @@ export const useNavigationMenuItemDndKit = (
|
||||
return;
|
||||
}
|
||||
|
||||
handleNavigationMenuItemDragAndDrop(dropResult, provided);
|
||||
handleNavigationMenuItemDragAndDrop(dropResult);
|
||||
};
|
||||
|
||||
const contextValues: NavigationMenuItemDndKitContextValues = {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
|
||||
import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData';
|
||||
|
||||
export const toDropResult = (
|
||||
draggableId: string,
|
||||
data: DraggableData | undefined,
|
||||
destination: DropDestination | null,
|
||||
): {
|
||||
source: DropDestination;
|
||||
destination: DropDestination | null;
|
||||
draggableId: string;
|
||||
} => {
|
||||
const sourceDroppableId = data?.sourceDroppableId ?? '';
|
||||
const sourceIndex = data?.sourceIndex ?? 0;
|
||||
return {
|
||||
source: { droppableId: sourceDroppableId, index: sourceIndex },
|
||||
destination,
|
||||
draggableId,
|
||||
};
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
import {
|
||||
type DraggableProvided,
|
||||
type DraggableRubric,
|
||||
type DraggableStateSnapshot,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { getObjectNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemSecondaryLabel';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
type WorkspaceNavigationMenuItemFolderDragCloneProps = {
|
||||
draggableProvided: DraggableProvided;
|
||||
draggableSnapshot: DraggableStateSnapshot;
|
||||
rubric: DraggableRubric;
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
navigationMenuItemFolderContentLength: number;
|
||||
selectedNavigationMenuItemIndex: number;
|
||||
};
|
||||
|
||||
export const WorkspaceNavigationMenuItemFolderDragClone = ({
|
||||
draggableProvided,
|
||||
draggableSnapshot,
|
||||
rubric,
|
||||
navigationMenuItems,
|
||||
navigationMenuItemFolderContentLength,
|
||||
selectedNavigationMenuItemIndex,
|
||||
}: WorkspaceNavigationMenuItemFolderDragCloneProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const navigationMenuItem = navigationMenuItems[rubric.source.index];
|
||||
|
||||
if (!isDefined(navigationMenuItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const view = isDefined(navigationMenuItem.viewId)
|
||||
? views.find((view) => view.id === navigationMenuItem.viewId)
|
||||
: undefined;
|
||||
const isIndexView = view?.key === ViewKey.INDEX;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={draggableProvided.innerRef}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided.draggableProps}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided.dragHandleProps}
|
||||
style={{
|
||||
...draggableProvided.draggableProps.style,
|
||||
background: draggableSnapshot.isDragging
|
||||
? theme.background.transparent.light
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
isIndexView
|
||||
? undefined
|
||||
: getObjectNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular: objectNameSingular ?? '',
|
||||
})
|
||||
}
|
||||
label={label}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
|
||||
)}
|
||||
to={undefined}
|
||||
active={false}
|
||||
isDragging={true}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index: rubric.source.index,
|
||||
arrayLength: navigationMenuItemFolderContentLength,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -252,26 +252,36 @@ describe('computeOptimisticRecordFromInput', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if recordInput contains fields unrelated to the current objectMetadata', () => {
|
||||
it('should warn and skip unknown fields if recordInput contains fields unrelated to the current objectMetadata', () => {
|
||||
const cache = new InMemoryCache();
|
||||
const personObjectMetadataItem = getMockObjectMetadataItemOrThrow('person');
|
||||
const consoleWarnSpy = jest
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
expect(() =>
|
||||
computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: getTestEnrichedObjectMetadataItemsMock(),
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
unknwon: 'unknown',
|
||||
foo: 'foo',
|
||||
bar: 'bar',
|
||||
city: 'Paris',
|
||||
},
|
||||
cache,
|
||||
objectPermissionsByObjectMetadataId: {},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Should never occur, encountered unknown fields unknwon, foo, bar in objectMetadataItem person"`,
|
||||
const result = computeOptimisticRecordFromInput({
|
||||
currentWorkspaceMember,
|
||||
objectMetadataItems: getTestEnrichedObjectMetadataItemsMock(),
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
recordInput: {
|
||||
unknwon: 'unknown',
|
||||
foo: 'foo',
|
||||
bar: 'bar',
|
||||
city: 'Paris',
|
||||
},
|
||||
cache,
|
||||
objectPermissionsByObjectMetadataId: {},
|
||||
});
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'encountered unknown fields unknwon, foo, bar in objectMetadataItem person',
|
||||
),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
city: 'Paris',
|
||||
});
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,8 +71,8 @@ export const computeOptimisticRecordFromInput = ({
|
||||
},
|
||||
);
|
||||
if (unknownRecordInputFields.length > 0) {
|
||||
throw new Error(
|
||||
`Should never occur, encountered unknown fields ${unknownRecordInputFields.join(', ')} in objectMetadataItem ${objectMetadataItem.nameSingular}`,
|
||||
console.warn(
|
||||
`computeOptimisticRecordFromInput: encountered unknown fields ${unknownRecordInputFields.join(', ')} in objectMetadataItem ${objectMetadataItem.nameSingular}. Skipping unknown fields.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_MANY_VIEW_GROUPS = gql`
|
||||
${VIEW_GROUP_FRAGMENT}
|
||||
mutation UpdateManyViewGroups($inputs: [UpdateViewGroupInput!]!) {
|
||||
updateManyViewGroups(inputs: $inputs) {
|
||||
...ViewGroupFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -8,43 +8,44 @@ import { t } from '@lingui/core/macro';
|
||||
import { CrudOperationType } from 'twenty-shared/types';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import {
|
||||
type UpdateViewGroupMutationVariables,
|
||||
UpdateViewGroupDocument,
|
||||
type UpdateManyViewGroupsMutationVariables,
|
||||
UpdateManyViewGroupsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const usePerformViewGroupAPIPersist = () => {
|
||||
const [updateViewGroupMutation] = useMutation(UpdateViewGroupDocument);
|
||||
const [updateManyViewGroupsMutation] = useMutation(
|
||||
UpdateManyViewGroupsDocument,
|
||||
);
|
||||
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const performViewGroupAPIUpdate = useCallback(
|
||||
async (
|
||||
updateViewGroupInputs: UpdateViewGroupMutationVariables[],
|
||||
updateViewGroupInputs: UpdateManyViewGroupsMutationVariables,
|
||||
): Promise<
|
||||
MetadataRequestResult<
|
||||
Awaited<ReturnType<typeof updateViewGroupMutation>>[]
|
||||
>
|
||||
MetadataRequestResult<Awaited<
|
||||
ReturnType<typeof updateManyViewGroupsMutation>
|
||||
> | null>
|
||||
> => {
|
||||
if (updateViewGroupInputs.length === 0) {
|
||||
if (
|
||||
!Array.isArray(updateViewGroupInputs.inputs) ||
|
||||
updateViewGroupInputs.inputs.length === 0
|
||||
) {
|
||||
return {
|
||||
status: 'successful',
|
||||
response: [],
|
||||
response: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
updateViewGroupInputs.map((variables) =>
|
||||
updateViewGroupMutation({
|
||||
variables,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const result = await updateManyViewGroupsMutation({
|
||||
variables: updateViewGroupInputs,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 'successful',
|
||||
response: results,
|
||||
response: result,
|
||||
};
|
||||
} catch (error) {
|
||||
if (CombinedGraphQLErrors.is(error)) {
|
||||
@@ -62,7 +63,7 @@ export const usePerformViewGroupAPIPersist = () => {
|
||||
};
|
||||
}
|
||||
},
|
||||
[updateViewGroupMutation, handleMetadataError, enqueueErrorSnackBar],
|
||||
[updateManyViewGroupsMutation, handleMetadataError, enqueueErrorSnackBar],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -67,9 +67,9 @@ export const useSaveCurrentViewGroups = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await performViewGroupAPIUpdate([
|
||||
{
|
||||
input: {
|
||||
await performViewGroupAPIUpdate({
|
||||
inputs: [
|
||||
{
|
||||
id: existingField.id,
|
||||
update: {
|
||||
isVisible: viewGroupToSave.isVisible,
|
||||
@@ -77,8 +77,8 @@ export const useSaveCurrentViewGroups = () => {
|
||||
fieldValue: viewGroupToSave.fieldValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
},
|
||||
[
|
||||
store,
|
||||
@@ -109,7 +109,7 @@ export const useSaveCurrentViewGroups = () => {
|
||||
|
||||
const currentViewGroups = view.viewGroups;
|
||||
|
||||
const viewGroupsToUpdate = viewGroupsToSave
|
||||
const viewGroupInputsToUpdate = viewGroupsToSave
|
||||
.map((viewGroupToSave) => {
|
||||
const existingField = currentViewGroups.find(
|
||||
(currentViewGroup) =>
|
||||
@@ -136,13 +136,11 @@ export const useSaveCurrentViewGroups = () => {
|
||||
}
|
||||
|
||||
return {
|
||||
input: {
|
||||
id: existingField.id,
|
||||
update: {
|
||||
isVisible: viewGroupToSave.isVisible,
|
||||
position: viewGroupToSave.position,
|
||||
fieldValue: viewGroupToSave.fieldValue,
|
||||
},
|
||||
id: existingField.id,
|
||||
update: {
|
||||
isVisible: viewGroupToSave.isVisible,
|
||||
position: viewGroupToSave.position,
|
||||
fieldValue: viewGroupToSave.fieldValue,
|
||||
},
|
||||
};
|
||||
})
|
||||
@@ -152,7 +150,7 @@ export const useSaveCurrentViewGroups = () => {
|
||||
throw new Error('mainGroupByFieldMetadataId is required');
|
||||
}
|
||||
|
||||
await performViewGroupAPIUpdate(viewGroupsToUpdate);
|
||||
await performViewGroupAPIUpdate({ inputs: viewGroupInputsToUpdate });
|
||||
},
|
||||
[
|
||||
store,
|
||||
|
||||
@@ -1,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)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upg
|
||||
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';
|
||||
@@ -17,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';
|
||||
@@ -40,7 +37,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
ApplicationEntity,
|
||||
ConnectedAccountEntity,
|
||||
MessageChannelEntity,
|
||||
CalendarChannelEntity,
|
||||
@@ -57,7 +53,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
ApplicationRegistrationModule,
|
||||
WorkspaceMigrationModule,
|
||||
FeatureFlagModule,
|
||||
SdkClientModule,
|
||||
WorkflowCommonModule,
|
||||
],
|
||||
providers: [
|
||||
@@ -70,7 +65,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
BackfillSelectFieldOptionIdsCommand,
|
||||
DeleteOrphanNavigationMenuItemsCommand,
|
||||
GenerateApplicationSdkClientsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
@@ -87,7 +81,6 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
BackfillSelectFieldOptionIdsCommand,
|
||||
DeleteOrphanNavigationMenuItemsCommand,
|
||||
GenerateApplicationSdkClientsCommand,
|
||||
SeedCliApplicationRegistrationCommand,
|
||||
MigrateRichTextToTextCommand,
|
||||
MigrateMessagingInfrastructureToMetadataCommand,
|
||||
|
||||
@@ -38,7 +38,6 @@ import { BackfillNavigationMenuItemTypeCommand } from 'src/database/commands/upg
|
||||
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';
|
||||
@@ -106,7 +105,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly backfillCommandMenuItemsCommand: BackfillCommandMenuItemsCommand,
|
||||
protected readonly deleteOrphanNavigationMenuItemsCommand: DeleteOrphanNavigationMenuItemsCommand,
|
||||
protected readonly backfillPageLayoutsAndFieldsWidgetViewFieldsCommand: BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand,
|
||||
protected readonly generateApplicationSdkClientsCommand: GenerateApplicationSdkClientsCommand,
|
||||
protected readonly seedCliApplicationRegistrationCommand: SeedCliApplicationRegistrationCommand,
|
||||
protected readonly migrateRichTextToTextCommand: MigrateRichTextToTextCommand,
|
||||
protected readonly migrateMessagingInfrastructureToMetadataCommand: MigrateMessagingInfrastructureToMetadataCommand,
|
||||
@@ -176,7 +174,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.backfillSelectFieldOptionIdsCommand,
|
||||
this.updateStandardIndexViewNamesCommand,
|
||||
this.makeWorkflowSearchableCommand,
|
||||
this.generateApplicationSdkClientsCommand,
|
||||
];
|
||||
|
||||
this.allCommands = {
|
||||
|
||||
@@ -721,6 +721,7 @@ export class LambdaDriver implements LogicFunctionDriver {
|
||||
const sdkArchiveBuffer =
|
||||
await this.sdkClientArchiveService.downloadArchiveBuffer({
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ export class LocalDriver implements LogicFunctionDriver {
|
||||
|
||||
await this.sdkClientArchiveService.downloadAndExtractToPackage({
|
||||
workspaceId: flatApplication.workspaceId,
|
||||
applicationId: flatApplication.id,
|
||||
applicationUniversalIdentifier,
|
||||
targetPackagePath: sdkPackagePath,
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import crypto from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -40,6 +41,7 @@ type UpdateSourceFilesParams = Omit<
|
||||
'builtHandlerPath'
|
||||
> & {
|
||||
sourceHandlerCode: string;
|
||||
queryRunner?: QueryRunner;
|
||||
};
|
||||
|
||||
type GetSourceCodeParams = Identifier & {
|
||||
@@ -128,6 +130,7 @@ export class LogicFunctionResourceService {
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
sourceHandlerCode,
|
||||
queryRunner,
|
||||
}: UpdateSourceFilesParams): Promise<void> {
|
||||
await this.fileStorageService.writeFile({
|
||||
workspaceId,
|
||||
@@ -137,6 +140,20 @@ export class LogicFunctionResourceService {
|
||||
sourceFile: sourceHandlerCode,
|
||||
settings: { isTemporaryFile: false, toDelete: false },
|
||||
mimeType: 'application/typescript',
|
||||
queryRunner,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSourceFile({
|
||||
sourceHandlerPath,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
}: GetSourceCodeParams): Promise<void> {
|
||||
await this.fileStorageService.delete({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Source,
|
||||
resourcePath: sourceHandlerPath,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ export class SdkClientController {
|
||||
const fileBuffer =
|
||||
await this.sdkClientArchiveService.getClientModuleFromArchive({
|
||||
workspaceId: workspace.id,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
moduleName,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { createWriteStream } from 'fs';
|
||||
import * as fs from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { type Readable } from 'stream';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -16,12 +12,12 @@ import {
|
||||
FileStorageException,
|
||||
FileStorageExceptionCode,
|
||||
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { TemporaryDirManager } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/temporary-dir-manager';
|
||||
import { type SdkModuleName } from 'src/engine/core-modules/sdk-client/constants/allowed-sdk-modules';
|
||||
import {
|
||||
SdkClientException,
|
||||
SdkClientExceptionCode,
|
||||
} from 'src/engine/core-modules/sdk-client/exceptions/sdk-client.exception';
|
||||
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
@@ -29,75 +25,74 @@ const SDK_CLIENT_ARCHIVE_NAME = 'twenty-client-sdk.zip';
|
||||
|
||||
@Injectable()
|
||||
export class SdkClientArchiveService {
|
||||
private readonly logger = new Logger(SdkClientArchiveService.name);
|
||||
|
||||
constructor(
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly sdkClientGenerationService: SdkClientGenerationService,
|
||||
) {}
|
||||
|
||||
async downloadAndExtractToPackage({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
targetPackagePath,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
targetPackagePath: string;
|
||||
}): Promise<void> {
|
||||
const temporaryDirManager = new TemporaryDirManager();
|
||||
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
try {
|
||||
const { sourceTemporaryDir } = await temporaryDirManager.init();
|
||||
const archivePath = join(sourceTemporaryDir, SDK_CLIENT_ARCHIVE_NAME);
|
||||
await fs.rm(targetPackagePath, { recursive: true, force: true });
|
||||
await fs.mkdir(targetPackagePath, { recursive: true });
|
||||
|
||||
const archiveStream = await this.readArchiveStream({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
const { default: unzipper } = await import('unzipper');
|
||||
const directory = await unzipper.Open.buffer(archiveBuffer);
|
||||
|
||||
await pipeline(archiveStream, createWriteStream(archivePath));
|
||||
|
||||
await fs.rm(targetPackagePath, { recursive: true, force: true });
|
||||
await fs.mkdir(targetPackagePath, { recursive: true });
|
||||
|
||||
const { default: unzipper } = await import('unzipper');
|
||||
const directory = await unzipper.Open.file(archivePath);
|
||||
|
||||
await directory.extract({ path: targetPackagePath });
|
||||
} finally {
|
||||
await temporaryDirManager.clean();
|
||||
}
|
||||
await directory.extract({ path: targetPackagePath });
|
||||
}
|
||||
|
||||
async downloadArchiveBuffer({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<Buffer> {
|
||||
const archiveStream = await this.readArchiveStream({
|
||||
return this.downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
return streamToBuffer(archiveStream);
|
||||
}
|
||||
|
||||
async getClientModuleFromArchive({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
moduleName,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
moduleName: SdkModuleName;
|
||||
}): Promise<Buffer> {
|
||||
const filePath = `dist/${moduleName}.mjs`;
|
||||
|
||||
const archiveBuffer = await this.downloadArchiveBuffer({
|
||||
const archiveBuffer = await this.downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
@@ -135,32 +130,41 @@ export class SdkClientArchiveService {
|
||||
]);
|
||||
}
|
||||
|
||||
private async readArchiveStream({
|
||||
private async downloadArchiveBufferOrGenerate({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<Readable> {
|
||||
}): Promise<Buffer> {
|
||||
try {
|
||||
return await this.fileStorageService.readFile({
|
||||
const stream = await this.fileStorageService.readFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.GeneratedSdkClient,
|
||||
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
|
||||
});
|
||||
|
||||
return await streamToBuffer(stream);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FileStorageException &&
|
||||
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
!(error instanceof FileStorageException) ||
|
||||
error.code !== FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
) {
|
||||
throw new SdkClientException(
|
||||
`SDK client archive "${SDK_CLIENT_ARCHIVE_NAME}" not found for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}".`,
|
||||
SdkClientExceptionCode.ARCHIVE_NOT_FOUND,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
`SDK client archive missing for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}", generating on-the-fly`,
|
||||
);
|
||||
|
||||
return this.sdkClientGenerationService.generateSdkClientForApplication({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,13 +44,13 @@ export class SdkClientGenerationService {
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): Promise<void> {
|
||||
}): Promise<Buffer> {
|
||||
const graphqlSchema = await this.workspaceSchemaFactory.createGraphQLSchema(
|
||||
{ id: workspaceId } as WorkspaceEntity,
|
||||
applicationId,
|
||||
);
|
||||
|
||||
await this.generateAndStore({
|
||||
const archiveBuffer = await this.generateAndStore({
|
||||
workspaceId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
@@ -60,6 +60,8 @@ export class SdkClientGenerationService {
|
||||
this.logger.log(
|
||||
`Generated SDK client for application ${applicationUniversalIdentifier}`,
|
||||
);
|
||||
|
||||
return archiveBuffer;
|
||||
}
|
||||
|
||||
private async generateAndStore({
|
||||
@@ -72,7 +74,7 @@ export class SdkClientGenerationService {
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
schema: string;
|
||||
}): Promise<void> {
|
||||
}): Promise<Buffer> {
|
||||
const temporaryDirManager = new TemporaryDirManager();
|
||||
|
||||
try {
|
||||
@@ -101,12 +103,14 @@ export class SdkClientGenerationService {
|
||||
|
||||
await createZipFile(tempPackageRoot, archivePath);
|
||||
|
||||
const archiveBuffer = await fs.readFile(archivePath);
|
||||
|
||||
await this.fileStorageService.writeFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.GeneratedSdkClient,
|
||||
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
|
||||
sourceFile: await fs.readFile(archivePath),
|
||||
sourceFile: archiveBuffer,
|
||||
mimeType: 'application/zip',
|
||||
settings: { isTemporaryFile: false, toDelete: false },
|
||||
});
|
||||
@@ -119,6 +123,8 @@ export class SdkClientGenerationService {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
return archiveBuffer;
|
||||
} catch (error) {
|
||||
throw new SdkClientException(
|
||||
`Failed to generate SDK client for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}": ${error instanceof Error ? error.message : String(error)}`,
|
||||
|
||||
@@ -33,6 +33,7 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
||||
|
||||
describe('WorkspaceService', () => {
|
||||
@@ -119,6 +120,7 @@ describe('WorkspaceService', () => {
|
||||
PermissionsService,
|
||||
FileCorePictureService,
|
||||
AiModelRegistryService,
|
||||
PrefillLogicFunctionService,
|
||||
].map((service) => ({
|
||||
provide: service,
|
||||
useValue: {},
|
||||
|
||||
@@ -53,12 +53,14 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
|
||||
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-dashboards';
|
||||
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-opportunities';
|
||||
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
|
||||
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
|
||||
import { prefillDashboards } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-dashboards.util';
|
||||
import { prefillOpportunities } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-opportunities.util';
|
||||
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-people.util';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util';
|
||||
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
||||
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-migration/constant/default-feature-flags';
|
||||
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
|
||||
@@ -111,6 +113,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly dnsManagerService: DnsManagerService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly subdomainManagerService: SubdomainManagerService,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
@@ -681,6 +684,14 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
},
|
||||
);
|
||||
|
||||
await this.prefillLogicFunctionService.ensureSeeded({
|
||||
workspaceId,
|
||||
definitions:
|
||||
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
|
||||
workspaceId,
|
||||
),
|
||||
});
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
@@ -694,6 +705,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
|
||||
await prefillWorkflows(
|
||||
queryRunner.manager,
|
||||
workspaceId,
|
||||
schemaName,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
@@ -720,6 +732,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
|
||||
@@ -41,12 +41,14 @@ import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||
import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeORMModule,
|
||||
TypeOrmModule.forFeature([BillingSubscriptionEntity, WorkspaceEntity]),
|
||||
MetricsModule,
|
||||
StandardObjectsPrefillModule,
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
AuditModule,
|
||||
@@ -79,6 +81,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
ApplicationModule,
|
||||
EnterpriseModule,
|
||||
StandardObjectsPrefillModule,
|
||||
],
|
||||
services: [WorkspaceService],
|
||||
resolvers: workspaceAutoResolverOpts,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -84,6 +84,19 @@ export class ViewGroupResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => [ViewGroupDTO])
|
||||
@UseGuards(UpdateViewGroupPermissionGuard)
|
||||
async updateManyViewGroups(
|
||||
@Args('inputs', { type: () => [UpdateViewGroupInput] })
|
||||
updateViewGroupInputs: UpdateViewGroupInput[],
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ViewGroupDTO[]> {
|
||||
return await this.viewGroupService.updateMany({
|
||||
updateViewGroupInputs,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => ViewGroupDTO)
|
||||
@UseGuards(DeleteViewGroupPermissionGuard)
|
||||
async deleteViewGroup(
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IsNull, Repository } from 'typeorm';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { fromCreateViewGroupInputToFlatViewGroupToCreate } from 'src/engine/metadata-modules/flat-view-group/utils/from-create-view-group-input-to-flat-view-group-to-create.util';
|
||||
@@ -152,6 +153,32 @@ export class ViewGroupService {
|
||||
workspaceId: string;
|
||||
updateViewGroupInput: UpdateViewGroupInput;
|
||||
}): Promise<ViewGroupDTO> {
|
||||
const [updatedViewGroup] = await this.updateMany({
|
||||
updateViewGroupInputs: [updateViewGroupInput],
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!isDefined(updatedViewGroup)) {
|
||||
throw new ViewGroupException(
|
||||
'Failed to update view group',
|
||||
ViewGroupExceptionCode.INVALID_VIEW_GROUP_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
return updatedViewGroup;
|
||||
}
|
||||
|
||||
async updateMany({
|
||||
updateViewGroupInputs,
|
||||
workspaceId,
|
||||
}: {
|
||||
updateViewGroupInputs: UpdateViewGroupInput[];
|
||||
workspaceId: string;
|
||||
}): Promise<ViewGroupDTO[]> {
|
||||
if (updateViewGroupInputs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
@@ -167,11 +194,13 @@ export class ViewGroupService {
|
||||
},
|
||||
);
|
||||
|
||||
const optimisticallyUpdatedFlatViewGroup =
|
||||
fromUpdateViewGroupInputToFlatViewGroupToUpdateOrThrow({
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
updateViewGroupInput,
|
||||
});
|
||||
const flatViewGroupsToUpdate = updateViewGroupInputs.map(
|
||||
(updateViewGroupInput) =>
|
||||
fromUpdateViewGroupInputToFlatViewGroupToUpdateOrThrow({
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
updateViewGroupInput,
|
||||
}),
|
||||
);
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
@@ -180,7 +209,7 @@ export class ViewGroupService {
|
||||
viewGroup: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [optimisticallyUpdatedFlatViewGroup],
|
||||
flatEntityToUpdate: flatViewGroupsToUpdate,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
@@ -193,7 +222,7 @@ export class ViewGroupService {
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while updating view group',
|
||||
'Multiple validation errors occurred while updating view groups',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -205,12 +234,13 @@ export class ViewGroupService {
|
||||
},
|
||||
);
|
||||
|
||||
return fromFlatViewGroupToViewGroupDto(
|
||||
findFlatEntityByUniversalIdentifierOrThrow({
|
||||
universalIdentifier:
|
||||
optimisticallyUpdatedFlatViewGroup.universalIdentifier,
|
||||
flatEntityMaps: recomputedExistingFlatViewGroupMaps,
|
||||
}),
|
||||
return updateViewGroupInputs.map(({ id }) =>
|
||||
fromFlatViewGroupToViewGroupDto(
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedExistingFlatViewGroupMaps,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,8 +121,10 @@ import {
|
||||
WORKSPACE_MEMBER_DATA_SEED_COLUMNS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
|
||||
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util';
|
||||
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
type RecordSeedConfig = {
|
||||
@@ -304,6 +306,7 @@ export class DevSeederDataService {
|
||||
private readonly timelineActivitySeederService: TimelineActivitySeederService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly prefillLogicFunctionService: PrefillLogicFunctionService,
|
||||
) {}
|
||||
|
||||
public async seed({
|
||||
@@ -331,6 +334,14 @@ export class DevSeederDataService {
|
||||
const { seeds: attachmentSeeds, fileSeedMetadata: attachmentFileMeta } =
|
||||
generateAttachmentSeedsForWorkspace(workspaceId);
|
||||
|
||||
await this.prefillLogicFunctionService.ensureSeeded({
|
||||
workspaceId,
|
||||
definitions:
|
||||
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions(
|
||||
workspaceId,
|
||||
),
|
||||
});
|
||||
|
||||
await this.coreDataSource.transaction(
|
||||
async (entityManager: WorkspaceEntityManager) => {
|
||||
await this.seedRecordsInBatches({
|
||||
@@ -359,6 +370,7 @@ export class DevSeederDataService {
|
||||
|
||||
await prefillWorkflows(
|
||||
entityManager,
|
||||
workspaceId,
|
||||
schemaName,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
|
||||
@@ -26,6 +26,7 @@ import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/da
|
||||
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
|
||||
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
|
||||
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
|
||||
import { StandardObjectsPrefillModule } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill.module';
|
||||
import { TwentyStandardApplicationModule } from 'src/engine/workspace-manager/twenty-standard-application/twenty-standard-application.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@@ -48,6 +49,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
TypeOrmModule.forFeature([WorkspaceEntity, ObjectMetadataEntity]),
|
||||
ObjectPermissionModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
StandardObjectsPrefillModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceMigrationModule,
|
||||
TwentyStandardApplicationModule,
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
import { FieldActorSource } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type EntityManager } from 'typeorm';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
|
||||
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
|
||||
|
||||
export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
|
||||
export const QUICK_LEAD_WORKFLOW_VERSION_ID =
|
||||
'ac67974f-c524-4288-9d88-af8515400b68';
|
||||
|
||||
export const prefillWorkflows = async (
|
||||
entityManager: EntityManager,
|
||||
schemaName: string,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
) => {
|
||||
const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
|
||||
const companyObjectMetadataId = objectIdByNameSingular['company'];
|
||||
const personObjectMetadataId = objectIdByNameSingular['person'];
|
||||
|
||||
if (
|
||||
!isDefined(companyObjectMetadataId) ||
|
||||
!isDefined(personObjectMetadataId)
|
||||
) {
|
||||
throw new Error('Company or person object metadata not found');
|
||||
}
|
||||
|
||||
const companyObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: companyObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const personObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: personObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) {
|
||||
throw new Error('Company or person object metadata not found');
|
||||
}
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.workflow`, [
|
||||
'id',
|
||||
'name',
|
||||
'lastPublishedVersionId',
|
||||
'statuses',
|
||||
'position',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
'createdByContext',
|
||||
'updatedBySource',
|
||||
'updatedByWorkspaceMemberId',
|
||||
'updatedByName',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: QUICK_LEAD_WORKFLOW_ID,
|
||||
name: 'Quick Lead',
|
||||
lastPublishedVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
|
||||
statuses: ['ACTIVE'],
|
||||
position: 1,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
createdByContext: {},
|
||||
updatedBySource: FieldActorSource.SYSTEM,
|
||||
updatedByWorkspaceMemberId: null,
|
||||
updatedByName: 'System',
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.workflowVersion`, [
|
||||
'id',
|
||||
'name',
|
||||
'trigger',
|
||||
'steps',
|
||||
'status',
|
||||
'position',
|
||||
'workflowId',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: QUICK_LEAD_WORKFLOW_VERSION_ID,
|
||||
name: 'v1',
|
||||
trigger: JSON.stringify({
|
||||
name: 'Launch manually',
|
||||
type: 'MANUAL',
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
icon: 'IconUserPlus',
|
||||
availability: { type: 'GLOBAL', locations: undefined },
|
||||
},
|
||||
nextStepIds: ['6e089bc9-aabd-435f-865f-f31c01c8f4a7'],
|
||||
}),
|
||||
steps: JSON.stringify([
|
||||
{
|
||||
id: '6e089bc9-aabd-435f-865f-f31c01c8f4a7',
|
||||
name: 'Quick Lead Form',
|
||||
type: 'FORM',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: [
|
||||
{
|
||||
id: '14d669f0-5249-4fa4-b0bb-f8bd408328d5',
|
||||
name: 'firstName',
|
||||
type: 'TEXT',
|
||||
label: 'First name',
|
||||
placeholder: 'Tim',
|
||||
},
|
||||
{
|
||||
id: '4eb6ce85-d231-4aef-9837-744490c026d0',
|
||||
name: 'lastName',
|
||||
type: 'TEXT',
|
||||
label: 'Last Name',
|
||||
placeholder: 'Apple',
|
||||
},
|
||||
{
|
||||
id: 'adbf0e9f-1427-49be-b4fb-092b34d97350',
|
||||
name: 'email',
|
||||
type: 'TEXT',
|
||||
label: 'Email',
|
||||
placeholder: 'timapple@apple.com',
|
||||
},
|
||||
{
|
||||
id: '4ffc7992-9e65-4a4d-9baf-b52e62f2c273',
|
||||
name: 'jobTitle',
|
||||
type: 'TEXT',
|
||||
label: 'Job title',
|
||||
placeholder: 'CEO',
|
||||
},
|
||||
{
|
||||
id: '42f11926-04ea-4924-94a4-2293cc748362',
|
||||
name: 'companyName',
|
||||
type: 'TEXT',
|
||||
label: 'Company name',
|
||||
placeholder: 'Apple',
|
||||
},
|
||||
{
|
||||
id: 'd6ca80ee-26cd-466d-91bf-984d7205451c',
|
||||
name: 'companyDomain',
|
||||
type: 'TEXT',
|
||||
label: 'Company domain',
|
||||
placeholder: 'https://www.apple.com',
|
||||
},
|
||||
],
|
||||
outputSchema: {
|
||||
email: {
|
||||
type: 'TEXT',
|
||||
label: 'Email',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
jobTitle: {
|
||||
type: 'TEXT',
|
||||
label: 'Job title',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
lastName: {
|
||||
type: 'TEXT',
|
||||
label: 'Last Name',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
firstName: {
|
||||
type: 'TEXT',
|
||||
label: 'First name',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
companyName: {
|
||||
type: 'TEXT',
|
||||
label: 'Company name',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
companyDomain: {
|
||||
type: 'TEXT',
|
||||
label: 'Company domain',
|
||||
value: 'My text',
|
||||
isLeaf: true,
|
||||
},
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['0715b6cd-7cc1-4b98-971b-00f54dfe643b'],
|
||||
},
|
||||
{
|
||||
id: '0715b6cd-7cc1-4b98-971b-00f54dfe643b',
|
||||
name: 'Create Company',
|
||||
type: 'CREATE_RECORD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
objectName: 'company',
|
||||
objectRecord: {
|
||||
name: '{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyName}}',
|
||||
domainName: {
|
||||
primaryLinkUrl:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.companyDomain}}',
|
||||
primaryLinkLabel: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
outputSchema: {
|
||||
object: {
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
label: 'Company',
|
||||
value: 'A company',
|
||||
isLeaf: true,
|
||||
fieldIdName: 'id',
|
||||
nameSingular: 'company',
|
||||
},
|
||||
_outputSchemaType: 'RECORD',
|
||||
fields: generateObjectRecordFields({
|
||||
objectMetadataInfo: {
|
||||
flatObjectMetadata: companyObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: ['6f553ea7-b00e-4371-9d88-d8298568a246'],
|
||||
},
|
||||
{
|
||||
id: '6f553ea7-b00e-4371-9d88-d8298568a246',
|
||||
name: 'Create Person',
|
||||
type: 'CREATE_RECORD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
objectName: 'person',
|
||||
objectRecord: {
|
||||
name: {
|
||||
lastName:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.lastName}}',
|
||||
firstName:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.firstName}}',
|
||||
},
|
||||
emails: {
|
||||
primaryEmail:
|
||||
'{{6e089bc9-aabd-435f-865f-f31c01c8f4a7.email}}',
|
||||
additionalEmails: [],
|
||||
},
|
||||
companyId: '{{0715b6cd-7cc1-4b98-971b-00f54dfe643b.id}}',
|
||||
},
|
||||
},
|
||||
outputSchema: {
|
||||
fields: generateObjectRecordFields({
|
||||
objectMetadataInfo: {
|
||||
flatObjectMetadata: personObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
},
|
||||
__typename: 'WorkflowAction',
|
||||
nextStepIds: null,
|
||||
},
|
||||
]),
|
||||
status: 'ACTIVE',
|
||||
position: 1,
|
||||
workflowId: QUICK_LEAD_WORKFLOW_ID,
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
|
||||
import { type PrefilledWorkflowCodeStepLogicFunctionDefinition } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
|
||||
|
||||
@Injectable()
|
||||
export class PrefillLogicFunctionService {
|
||||
constructor(
|
||||
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
async ensureSeeded({
|
||||
workspaceId,
|
||||
definitions,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
definitions: PrefilledWorkflowCodeStepLogicFunctionDefinition[];
|
||||
}) {
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatLogicFunctionMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
for (const definition of definitions) {
|
||||
const existingLogicFunction = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: definition.id,
|
||||
flatEntityMaps: flatLogicFunctionMaps,
|
||||
});
|
||||
|
||||
if (isDefined(existingLogicFunction)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.logicFunctionFromSourceService.createOneFromSource({
|
||||
workspaceId,
|
||||
input: {
|
||||
id: definition.id,
|
||||
name: definition.name,
|
||||
description: definition.description,
|
||||
toolInputSchema: definition.toolInputSchema,
|
||||
source: {
|
||||
sourceHandlerCode: definition.sourceHandlerCode,
|
||||
toolInputSchema: definition.toolInputSchema,
|
||||
handlerName: 'main',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { type DataSource, type EntityManager } from 'typeorm';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
|
||||
import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
|
||||
import { prefillWorkflowCommandMenuItems } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflow-command-menu-items';
|
||||
import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
|
||||
|
||||
export const standardObjectsPrefillData = async (
|
||||
dataSource: DataSource,
|
||||
schemaName: string,
|
||||
workspaceId: string,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
) => {
|
||||
dataSource.transaction(async (entityManager: EntityManager) => {
|
||||
await prefillCompanies(entityManager, schemaName);
|
||||
|
||||
await prefillPeople(entityManager, schemaName);
|
||||
|
||||
await prefillWorkflows(
|
||||
entityManager,
|
||||
schemaName,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
);
|
||||
|
||||
await prefillWorkflowCommandMenuItems(entityManager, workspaceId);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { PrefillLogicFunctionService } from 'src/engine/workspace-manager/standard-objects-prefill-data/services/prefill-logic-function.service';
|
||||
|
||||
@Module({
|
||||
imports: [LogicFunctionModule, WorkspaceManyOrAllFlatEntityMapsCacheModule],
|
||||
providers: [PrefillLogicFunctionService],
|
||||
exports: [PrefillLogicFunctionService],
|
||||
})
|
||||
export class StandardObjectsPrefillModule {}
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
FIGMA_ID,
|
||||
NOTION_ID,
|
||||
STRIPE_ID,
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
|
||||
import {
|
||||
BRIAN_CHESKY_ID,
|
||||
DARIO_AMODEI_ID,
|
||||
DYLAN_FIELD_ID,
|
||||
IVAN_ZHAO_ID,
|
||||
PATRICK_COLLISON_ID,
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people';
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-people.util';
|
||||
|
||||
export const OPPORTUNITY_STRIPE_PLATFORM_MIGRATION_ID =
|
||||
'822639e5-9bf7-40f1-8882-a11140362339';
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
FIGMA_ID,
|
||||
NOTION_ID,
|
||||
STRIPE_ID,
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies';
|
||||
} from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-companies.util';
|
||||
|
||||
export const BRIAN_CHESKY_ID = 'a2e78a5e-338b-46df-8811-fa08c7d19d35'; // Airbnb
|
||||
export const DARIO_AMODEI_ID = '93c72d2e-e65c-44c4-99ad-f87f50349dcf'; // Anthropic
|
||||
@@ -0,0 +1,336 @@
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
|
||||
const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE =
|
||||
'd41b0a2d-aa97-44dc-ad5d-89774164af27';
|
||||
|
||||
export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds = (
|
||||
workspaceId: string,
|
||||
) => ({
|
||||
extractDomainLogicFunctionId: uuidv5(
|
||||
`${workspaceId}:create-company-when-adding-new-person:extract-domain`,
|
||||
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
|
||||
),
|
||||
findMatchingCompanyByDomainLogicFunctionId: uuidv5(
|
||||
`${workspaceId}:create-company-when-adding-new-person:find-matching-company-by-domain`,
|
||||
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
|
||||
),
|
||||
isPersonalEmailLogicFunctionId: uuidv5(
|
||||
`${workspaceId}:create-company-when-adding-new-person:is-personal-email`,
|
||||
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_LOGIC_FUNCTION_ID_NAMESPACE,
|
||||
),
|
||||
});
|
||||
|
||||
const EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['email'],
|
||||
};
|
||||
|
||||
const IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
primaryEmail: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['primaryEmail'],
|
||||
};
|
||||
|
||||
const FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companies: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['companies', 'domain'],
|
||||
};
|
||||
|
||||
const EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
|
||||
'ac.uk',
|
||||
'co.in',
|
||||
'co.jp',
|
||||
'co.kr',
|
||||
'co.nz',
|
||||
'co.uk',
|
||||
'co.za',
|
||||
'com.ar',
|
||||
'com.au',
|
||||
'com.br',
|
||||
'com.cn',
|
||||
'com.hk',
|
||||
'com.mx',
|
||||
'com.sg',
|
||||
'com.tr',
|
||||
'com.tw',
|
||||
'com.ua',
|
||||
'gov.uk',
|
||||
'ne.jp',
|
||||
'net.au',
|
||||
'org.au',
|
||||
'org.uk',
|
||||
]);
|
||||
|
||||
const getRegistrableDomain = (host) => {
|
||||
const normalizedHost = host.toLowerCase().replace(/^www\\./, '');
|
||||
const parts = normalizedHost.split('.').filter(Boolean);
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return normalizedHost;
|
||||
}
|
||||
|
||||
const lastTwo = parts.slice(-2).join('.');
|
||||
|
||||
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
|
||||
return lastTwo;
|
||||
};
|
||||
|
||||
export const main = async (params) => {
|
||||
const email =
|
||||
typeof params?.email === 'string' ? params.email.trim().toLowerCase() : '';
|
||||
const domainFromEmail = email.split('@')[1] ?? '';
|
||||
const domain = domainFromEmail ? getRegistrableDomain(domainFromEmail) : '';
|
||||
|
||||
return {
|
||||
url: domain ? \`https://\${domain}\` : '',
|
||||
domain,
|
||||
};
|
||||
};`;
|
||||
|
||||
const IS_PERSONAL_EMAIL_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
|
||||
'ac.uk',
|
||||
'co.in',
|
||||
'co.jp',
|
||||
'co.kr',
|
||||
'co.nz',
|
||||
'co.uk',
|
||||
'co.za',
|
||||
'com.ar',
|
||||
'com.au',
|
||||
'com.br',
|
||||
'com.cn',
|
||||
'com.hk',
|
||||
'com.mx',
|
||||
'com.sg',
|
||||
'com.tr',
|
||||
'com.tw',
|
||||
'com.ua',
|
||||
'gov.uk',
|
||||
'ne.jp',
|
||||
'net.au',
|
||||
'org.au',
|
||||
'org.uk',
|
||||
]);
|
||||
|
||||
const PERSONAL_EMAIL_DOMAINS = new Set([
|
||||
'aol.com',
|
||||
'att.net',
|
||||
'btinternet.com',
|
||||
'comcast.net',
|
||||
'fastmail.com',
|
||||
'free.fr',
|
||||
'gmx.com',
|
||||
'gmx.de',
|
||||
'googlemail.com',
|
||||
'hotmail.com',
|
||||
'hotmail.fr',
|
||||
'icloud.com',
|
||||
'laposte.net',
|
||||
'live.com',
|
||||
'mac.com',
|
||||
'mail.com',
|
||||
'me.com',
|
||||
'msn.com',
|
||||
'orange.fr',
|
||||
'outlook.com',
|
||||
'outlook.fr',
|
||||
'pm.me',
|
||||
'proton.me',
|
||||
'protonmail.com',
|
||||
'sfr.fr',
|
||||
'verizon.net',
|
||||
'wanadoo.fr',
|
||||
'yahoo.co.jp',
|
||||
'yahoo.co.uk',
|
||||
'yahoo.com',
|
||||
'yandex.com',
|
||||
'yandex.ru',
|
||||
'zoho.com',
|
||||
'gmail.com',
|
||||
]);
|
||||
|
||||
const getRegistrableDomain = (host) => {
|
||||
const normalizedHost = host.toLowerCase().replace(/^www\\./, '');
|
||||
const parts = normalizedHost.split('.').filter(Boolean);
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return normalizedHost;
|
||||
}
|
||||
|
||||
const lastTwo = parts.slice(-2).join('.');
|
||||
|
||||
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
|
||||
return lastTwo;
|
||||
};
|
||||
|
||||
export const main = async (params) => {
|
||||
const email =
|
||||
typeof params?.primaryEmail === 'string'
|
||||
? params.primaryEmail.trim().toLowerCase()
|
||||
: '';
|
||||
|
||||
if (!email || !email.includes('@')) {
|
||||
return { isPersonal: true };
|
||||
}
|
||||
|
||||
const host = email.split('@')[1] ?? '';
|
||||
const registrableDomain = host ? getRegistrableDomain(host) : '';
|
||||
|
||||
return {
|
||||
isPersonal:
|
||||
!registrableDomain ||
|
||||
PERSONAL_EMAIL_DOMAINS.has(host) ||
|
||||
PERSONAL_EMAIL_DOMAINS.has(registrableDomain),
|
||||
};
|
||||
};`;
|
||||
|
||||
const FIND_MATCHING_COMPANY_BY_DOMAIN_LOGIC_FUNCTION_SOURCE = `const MULTI_PART_SUFFIXES = new Set([
|
||||
'ac.uk',
|
||||
'co.in',
|
||||
'co.jp',
|
||||
'co.kr',
|
||||
'co.nz',
|
||||
'co.uk',
|
||||
'co.za',
|
||||
'com.ar',
|
||||
'com.au',
|
||||
'com.br',
|
||||
'com.cn',
|
||||
'com.hk',
|
||||
'com.mx',
|
||||
'com.sg',
|
||||
'com.tr',
|
||||
'com.tw',
|
||||
'com.ua',
|
||||
'gov.uk',
|
||||
'ne.jp',
|
||||
'net.au',
|
||||
'org.au',
|
||||
'org.uk',
|
||||
]);
|
||||
|
||||
const normalizeHost = (value) => {
|
||||
if (typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^https?:\\/\\//, '')
|
||||
.replace(/^www\\./, '')
|
||||
.split('/')[0]
|
||||
.split('?')[0]
|
||||
.split('#')[0]
|
||||
.split(':')[0];
|
||||
};
|
||||
|
||||
const getRegistrableDomain = (value) => {
|
||||
const normalizedHost = normalizeHost(value);
|
||||
const parts = normalizedHost.split('.').filter(Boolean);
|
||||
|
||||
if (parts.length <= 2) {
|
||||
return normalizedHost;
|
||||
}
|
||||
|
||||
const lastTwo = parts.slice(-2).join('.');
|
||||
|
||||
if (MULTI_PART_SUFFIXES.has(lastTwo) && parts.length >= 3) {
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
|
||||
return lastTwo;
|
||||
};
|
||||
|
||||
export const main = async (params) => {
|
||||
const domain = getRegistrableDomain(params?.domain);
|
||||
const companies = Array.isArray(params?.companies) ? params.companies : [];
|
||||
|
||||
const matchingCompany = companies.find((company) => {
|
||||
const companyDomain = getRegistrableDomain(
|
||||
company?.domainName?.primaryLinkUrl,
|
||||
);
|
||||
|
||||
return companyDomain !== '' && companyDomain === domain;
|
||||
});
|
||||
|
||||
const companyId =
|
||||
typeof matchingCompany?.id === 'string' ? matchingCompany.id : '';
|
||||
|
||||
return {
|
||||
companyId,
|
||||
hasMatch: companyId !== '',
|
||||
};
|
||||
};`;
|
||||
|
||||
export type PrefilledWorkflowCodeStepLogicFunctionDefinition = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
sourceHandlerCode: string;
|
||||
toolInputSchema: object;
|
||||
};
|
||||
|
||||
export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions =
|
||||
(workspaceId: string): PrefilledWorkflowCodeStepLogicFunctionDefinition[] => {
|
||||
const {
|
||||
extractDomainLogicFunctionId,
|
||||
findMatchingCompanyByDomainLogicFunctionId,
|
||||
isPersonalEmailLogicFunctionId,
|
||||
} =
|
||||
getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds(workspaceId);
|
||||
|
||||
return [
|
||||
{
|
||||
id: extractDomainLogicFunctionId,
|
||||
name: 'Extract domain from email',
|
||||
description:
|
||||
'Extracts a normalized company domain and URL from a person email address.',
|
||||
sourceHandlerCode: EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE,
|
||||
toolInputSchema: EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
{
|
||||
id: findMatchingCompanyByDomainLogicFunctionId,
|
||||
name: 'Find matching company by domain',
|
||||
description:
|
||||
'Finds an existing company whose website matches a normalized registrable domain.',
|
||||
sourceHandlerCode:
|
||||
FIND_MATCHING_COMPANY_BY_DOMAIN_LOGIC_FUNCTION_SOURCE,
|
||||
toolInputSchema: FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
{
|
||||
id: isPersonalEmailLogicFunctionId,
|
||||
name: 'Is this a personal email?',
|
||||
description:
|
||||
'Detects whether an email address belongs to a common personal email provider.',
|
||||
sourceHandlerCode: IS_PERSONAL_EMAIL_LOGIC_FUNCTION_SOURCE,
|
||||
toolInputSchema: IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { type EntityManager } from 'typeorm';
|
||||
|
||||
import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/command-menu-item/enums/command-menu-item-availability-type.enum';
|
||||
import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum';
|
||||
import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows';
|
||||
import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
|
||||
const QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER =
|
||||
@@ -0,0 +1,762 @@
|
||||
import { FieldActorSource } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type EntityManager } from 'typeorm';
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
|
||||
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event';
|
||||
import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields';
|
||||
import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util';
|
||||
|
||||
export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d';
|
||||
export const QUICK_LEAD_WORKFLOW_VERSION_ID =
|
||||
'ac67974f-c524-4288-9d88-af8515400b68';
|
||||
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID =
|
||||
'887c6c06-fbc5-4b45-8d6b-f7b6b0f40b12';
|
||||
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID =
|
||||
'0f276d7e-a950-41ab-ad98-35e80753dc58';
|
||||
export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_AUTOMATED_TRIGGER_ID =
|
||||
'c54f5990-13a3-4c3b-b75d-df09e7843036';
|
||||
|
||||
export const prefillWorkflows = async (
|
||||
entityManager: EntityManager,
|
||||
workspaceId: string,
|
||||
schemaName: string,
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>,
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
|
||||
) => {
|
||||
const {
|
||||
extractDomainLogicFunctionId,
|
||||
findMatchingCompanyByDomainLogicFunctionId,
|
||||
isPersonalEmailLogicFunctionId,
|
||||
} = getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds(workspaceId);
|
||||
|
||||
const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(
|
||||
flatObjectMetadataMaps,
|
||||
);
|
||||
|
||||
const companyObjectMetadataId = objectIdByNameSingular['company'];
|
||||
const personObjectMetadataId = objectIdByNameSingular['person'];
|
||||
|
||||
if (
|
||||
!isDefined(companyObjectMetadataId) ||
|
||||
!isDefined(personObjectMetadataId)
|
||||
) {
|
||||
throw new Error('Company or person object metadata not found');
|
||||
}
|
||||
|
||||
const companyObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: companyObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const personObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: personObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) {
|
||||
throw new Error('Company or person object metadata not found');
|
||||
}
|
||||
|
||||
const companyDomainNameFieldMetadata = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
).find(
|
||||
(fieldMetadata) =>
|
||||
fieldMetadata?.objectMetadataId === companyObjectMetadataId &&
|
||||
fieldMetadata?.name === 'domainName',
|
||||
);
|
||||
|
||||
if (!isDefined(companyDomainNameFieldMetadata)) {
|
||||
throw new Error('Company domainName field metadata not found');
|
||||
}
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.workflow`, [
|
||||
'id',
|
||||
'name',
|
||||
'lastPublishedVersionId',
|
||||
'statuses',
|
||||
'position',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
'createdByContext',
|
||||
'updatedBySource',
|
||||
'updatedByWorkspaceMemberId',
|
||||
'updatedByName',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: QUICK_LEAD_WORKFLOW_ID,
|
||||
name: 'Quick Lead',
|
||||
lastPublishedVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID,
|
||||
statuses: ['ACTIVE'],
|
||||
position: 1,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
createdByContext: {},
|
||||
updatedBySource: FieldActorSource.SYSTEM,
|
||||
updatedByWorkspaceMemberId: null,
|
||||
updatedByName: 'System',
|
||||
},
|
||||
{
|
||||
id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID,
|
||||
name: 'Create company when adding a new person',
|
||||
lastPublishedVersionId:
|
||||
CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID,
|
||||
statuses: ['ACTIVE'],
|
||||
position: 2,
|
||||
createdBySource: FieldActorSource.SYSTEM,
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
createdByContext: {},
|
||||
updatedBySource: FieldActorSource.SYSTEM,
|
||||
updatedByWorkspaceMemberId: null,
|
||||
updatedByName: 'System',
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.workflowVersion`, [
|
||||
'id',
|
||||
'name',
|
||||
'trigger',
|
||||
'steps',
|
||||
'status',
|
||||
'position',
|
||||
'workflowId',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: QUICK_LEAD_WORKFLOW_VERSION_ID,
|
||||
name: 'v1',
|
||||
trigger: JSON.stringify({
|
||||
name: 'Launch manually',
|
||||
type: 'MANUAL',
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
icon: 'IconUserPlus',
|
||||
availability: { type: 'GLOBAL', locations: undefined },
|
||||
},
|
||||
nextStepIds: ['6e089bc9-aabd-435f-865f-f31c01c8f4a7'],
|
||||
}),
|
||||
steps: JSON.stringify([
|
||||
{
|
||||
id: '6e089bc9-aabd-435f-865f-f31c01c8f4a7',
|
||||
name: 'Quick Lead Form',
|
||||
type: 'FORM',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: [
|
||||
{
|
||||
id: '14d669f0-5249-4fa4-b0bb-f8bd408328d5',
|
||||
name: 'firstName',
|
||||
type: 'TEXT',
|
||||
label: 'First name',
|
||||
placeholder: 'Tim',
|
||||
},
|
||||
{
|
||||
id: '4eb6ce85-d231-4aef-9837-744490c026d0',
|
||||
name: 'lastName',
|
||||
type: 'TEXT',
|
||||
label: 'Last Name',
|
||||
placeholder: 'Apple',
|
||||
},
|
||||
{
|
||||
id: 'adbf0e9f-1427-49be-b4fb-092b34d97350',
|
||||
name: 'email',
|
||||
type: 'TEXT',
|
||||
label: 'Email',
|
||||
placeholder: 'timapple@apple.com',
|
||||
},
|
||||
{
|
||||
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();
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { VIEW_GROUP_GQL_FIELDS } from 'test/integration/constants/view-gql-fields.constants';
|
||||
|
||||
import { type UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input';
|
||||
|
||||
export const updateManyViewGroupsQueryFactory = ({
|
||||
gqlFields = VIEW_GROUP_GQL_FIELDS,
|
||||
inputs,
|
||||
}: {
|
||||
gqlFields?: string;
|
||||
inputs: UpdateViewGroupInput[];
|
||||
}) => ({
|
||||
query: gql`
|
||||
mutation UpdateManyViewGroups($inputs: [UpdateViewGroupInput!]!) {
|
||||
updateManyViewGroups(inputs: $inputs) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
inputs,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { updateManyViewGroupsQueryFactory } from 'test/integration/metadata/suites/view-group/utils/update-many-view-groups-query-factory.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
|
||||
import { type UpdateViewGroupInput } from 'src/engine/metadata-modules/view-group/dtos/inputs/update-view-group.input';
|
||||
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
|
||||
export const updateManyViewGroups = async ({
|
||||
inputs,
|
||||
gqlFields,
|
||||
expectToFail,
|
||||
}: {
|
||||
inputs: UpdateViewGroupInput[];
|
||||
gqlFields?: string;
|
||||
expectToFail?: boolean;
|
||||
}): CommonResponseBody<{
|
||||
updateManyViewGroups: ViewGroupEntity[];
|
||||
}> => {
|
||||
const graphqlOperation = updateManyViewGroupsQueryFactory({
|
||||
inputs,
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage:
|
||||
'View Groups batch update should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage:
|
||||
'View Groups batch update has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# DB
|
||||
*.sqlite
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["react", "typescript", "import", "unicorn"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules"],
|
||||
"rules": {
|
||||
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
|
||||
"no-console": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-debugger": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-undef": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-redeclare": "off",
|
||||
"import/no-duplicates": "error",
|
||||
"typescript/no-redeclare": "error",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/consistent-type-imports": ["error", {
|
||||
"prefer": "type-imports",
|
||||
"fixStyle": "inline-type-imports"
|
||||
}],
|
||||
"typescript/explicit-function-return-type": "off",
|
||||
"typescript/explicit-module-boundary-types": "off",
|
||||
"typescript/no-empty-object-type": ["error", {
|
||||
"allowInterfaces": "with-single-extends"
|
||||
}],
|
||||
"typescript/no-empty-function": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/no-unused-vars": ["warn", {
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}],
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-key": "off",
|
||||
"react/display-name": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/jsx-no-useless-fragment": "off",
|
||||
"react/jsx-props-no-spreading": ["error", { "explicitSpread": "ignore" }],
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Twenty-Website
|
||||
|
||||
This is used for the marketing website (twenty.com).
|
||||
This is not related in any way to the main app, which you can find in twenty-front and twenty-server.
|
||||
@@ -0,0 +1,5 @@
|
||||
import withLinaria, { type LinariaConfig } from 'next-with-linaria';
|
||||
|
||||
const nextConfig: LinariaConfig = { reactCompiler: true };
|
||||
|
||||
module.exports = withLinaria(nextConfig);
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "twenty-website-new",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"nx": "NX_DEFAULT_PROJECT=twenty-website-new node ../../node_modules/nx/bin/nx.js",
|
||||
"dev": "npx next dev",
|
||||
"build": "npx next build",
|
||||
"start": "npx next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@base-ui/utils": "^0.2.6",
|
||||
"@linaria/core": "^7.0.0",
|
||||
"@linaria/react": "^7.0.1",
|
||||
"@wyw-in-js/babel-preset": "^0.8.1",
|
||||
"next": "16.1.7",
|
||||
"next-with-linaria": "^1.3.0",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"babel-plugin-react-compiler": "1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "twenty-website-new",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/twenty-website-new/src",
|
||||
"projectType": "application",
|
||||
"tags": ["scope:website"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"inputs": ["production", "^production"],
|
||||
"outputs": ["{projectRoot}/.next"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx next build"
|
||||
},
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx next dev"
|
||||
}
|
||||
},
|
||||
"start": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "npx next start"
|
||||
}
|
||||
},
|
||||
"lint": {},
|
||||
"lint:diff-with-main": {},
|
||||
"typecheck": {},
|
||||
"fmt": {
|
||||
"options": {
|
||||
"files": "."
|
||||
},
|
||||
"configurations": {
|
||||
"fix": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 473 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 954 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,12 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'Build', fontFamily: 'sans' },
|
||||
{ text: ' your Enterprise CRM ', fontFamily: 'serif' },
|
||||
{ text: 'at AI Speed', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: 'Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves.',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ProblemDataType } from '@/sections/Problem/types';
|
||||
|
||||
export const PROBLEM_DATA: ProblemDataType = {
|
||||
eyebrow: { heading: { text: 'The Problem:', fontFamily: 'sans' } },
|
||||
heading: [
|
||||
{ text: 'A custom CRM gives your org an edge, ', fontFamily: 'serif' },
|
||||
{ text: 'but building one comes with tradeoffs', fontFamily: 'sans' },
|
||||
],
|
||||
points: [
|
||||
{
|
||||
heading: { text: 'The Giant Monolith', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Proprietary languages, slow deployment cycles, and "black box" logic.',
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'The In-house Burden', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "It's fragile. V1 ships quickly, but maintaining and making changes is a long term burden.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ThreeCardsFeatureCardsDataType } from '@/sections/ThreeCards/types';
|
||||
|
||||
export const THREE_CARDS_FEATURE_DATA: ThreeCardsFeatureCardsDataType = {
|
||||
eyebrow: {
|
||||
heading: {
|
||||
text: 'Skip the clunky UX that always comes with custom.',
|
||||
fontFamily: 'sans',
|
||||
},
|
||||
},
|
||||
heading: [
|
||||
{ text: 'Make your GTM team happy with ', fontFamily: 'serif' },
|
||||
{ text: "a CRM they'll love", fontFamily: 'sans' },
|
||||
],
|
||||
featureCards: [
|
||||
{
|
||||
heading: { text: 'Familiar, modern interface', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "Twenty makes it simple. It's clean, intuitive, and built to feel like Notion.",
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'Fast path to action', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Smart patterns, shortcuts, and layouts make everyday tasks faster and easier to execute.',
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'Live data and AI Built', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Everything updates in real time, with AI chat always ready to help you move faster.',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ThreeCardsIllustrationDataType } from '@/sections/ThreeCards/types';
|
||||
|
||||
export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
|
||||
eyebrow: {
|
||||
heading: { text: 'Stop settling for trade-offs.', fontFamily: 'sans' },
|
||||
},
|
||||
heading: [
|
||||
{ text: 'Assemble, iterate and adapt a robust CRM,', fontFamily: 'serif' },
|
||||
{ text: " that's quick to flex", fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: 'Compose your CRM and interal apps with a single exesibility toolkit.',
|
||||
},
|
||||
illustrationCards: [
|
||||
{
|
||||
heading: { text: 'Production grade quality', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "What stood out with Twenty wasn't just flexibility it was the quality. The system feels stable, polished, and ready for real teams with real stakes.",
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: {
|
||||
role: { text: 'Head of Engineering' },
|
||||
company: { text: 'Mid-Market Fintech' },
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'AI for rapid iterations', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Twenty removes friction from iteration and lets us adapt our CRM as the business changes.',
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: {
|
||||
role: { text: 'Head of Engineering' },
|
||||
company: { text: 'Mid-Market Fintech' },
|
||||
},
|
||||
},
|
||||
{
|
||||
heading: { text: 'Control without drag', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "It's a modern, open-source alternative to Salesforce/Hubspot that lets you manage your customer relationships in a secure + privacy-first way.",
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: {
|
||||
role: { text: 'Head of Engineering' },
|
||||
company: { text: 'Mid-Market Fintech' },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { TrustedByDataType } from '@/sections/TrustedBy/types';
|
||||
|
||||
export const TRUSTED_BY_DATA: TrustedByDataType = {
|
||||
separator: { text: 'trusted by' },
|
||||
clientCountLabel: { text: '+ 10 000 other clients' },
|
||||
logos: [{ icon: 'flexport' }, { icon: 'zapier' }],
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { HERO_DATA } from '@/app/(home)/constants/hero';
|
||||
import { PROBLEM_DATA } from '@/app/(home)/constants/problem';
|
||||
import { THREE_CARDS_FEATURE_DATA } from '@/app/(home)/constants/three-cards-feature';
|
||||
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/(home)/constants/three-cards-illustration';
|
||||
import { TRUSTED_BY_DATA } from '@/app/(home)/constants/trusted-by';
|
||||
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { Problem } from '@/sections/Problem/components';
|
||||
import { ThreeCards } from '@/sections/ThreeCards/components';
|
||||
import { TrustedBy } from '@/sections/TrustedBy/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<Hero.Heading page={Pages.Home} segments={HERO_DATA.heading} />
|
||||
<Hero.Body page={Pages.Home} body={HERO_DATA.body} size="sm" />
|
||||
<Hero.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Get started"
|
||||
type="anchor"
|
||||
variant="contained"
|
||||
/>
|
||||
</Hero.Cta>
|
||||
<Hero.HomeVisual />
|
||||
</Hero.Root>
|
||||
|
||||
<TrustedBy.Root>
|
||||
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
|
||||
<TrustedBy.Logos
|
||||
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel}
|
||||
logos={TRUSTED_BY_DATA.logos}
|
||||
/>
|
||||
</TrustedBy.Root>
|
||||
|
||||
<Problem.Root>
|
||||
<Problem.Visual />
|
||||
<Problem.Content>
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={PROBLEM_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Problem.Heading segments={PROBLEM_DATA.heading} />
|
||||
<Problem.Points points={PROBLEM_DATA.points} />
|
||||
</Problem.Content>
|
||||
</Problem.Root>
|
||||
|
||||
<ThreeCards.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<ThreeCards.Intro page={Pages.Home} align="left">
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Heading
|
||||
segments={THREE_CARDS_ILLUSTRATION_DATA.heading}
|
||||
size="lg"
|
||||
weight="light"
|
||||
/>
|
||||
<Body body={THREE_CARDS_ILLUSTRATION_DATA.body} size="sm" />
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.IllustrationCards
|
||||
illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards}
|
||||
/>
|
||||
</ThreeCards.Root>
|
||||
|
||||
<ThreeCards.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<ThreeCards.Intro page={Pages.Home} align="center">
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={THREE_CARDS_FEATURE_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Heading
|
||||
segments={THREE_CARDS_FEATURE_DATA.heading}
|
||||
size="lg"
|
||||
weight="light"
|
||||
/>
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.FeatureCards
|
||||
featureCards={THREE_CARDS_FEATURE_DATA.featureCards}
|
||||
/>
|
||||
</ThreeCards.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 170 KiB |
@@ -0,0 +1,89 @@
|
||||
import { Footer } from '@/sections/Footer/components';
|
||||
import { Menu } from '@/sections/Menu/components';
|
||||
import { theme } from '@/theme';
|
||||
import { cssVariables } from '@/theme/css-variables';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { Metadata } from 'next';
|
||||
import { Aleo, Azeret_Mono, Host_Grotesk } from 'next/font/google';
|
||||
|
||||
const hostGrotesk = Host_Grotesk({
|
||||
subsets: ['latin'],
|
||||
weight: ['300', '400', '500', '600'],
|
||||
variable: '--font-sans',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const aleo = Aleo({
|
||||
subsets: ['latin'],
|
||||
weight: ['300'],
|
||||
variable: '--font-serif',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const azeretMono = Azeret_Mono({
|
||||
subsets: ['latin'],
|
||||
weight: ['300', '500'],
|
||||
variable: '--font-mono',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
css`
|
||||
:global(*),
|
||||
:global(*::before),
|
||||
:global(*::after) {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(html) {
|
||||
background-color: ${theme.colors.primary.background[100]};
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
display: flex;
|
||||
font-family: ${theme.font.family.sans};
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMain = styled.main`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Twenty — Open Source CRM',
|
||||
description: 'Modular, scalable open source CRM for modern teams.',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${cssVariables} ${hostGrotesk.variable} ${aleo.variable} ${azeretMono.variable}`}
|
||||
>
|
||||
<Menu.Root>
|
||||
<Menu.Logo />
|
||||
<Menu.Nav />
|
||||
<Menu.Social />
|
||||
<Menu.Cta />
|
||||
</Menu.Root>
|
||||
<StyledMain>{children}</StyledMain>
|
||||
<Footer.Root>
|
||||
<Footer.Logo />
|
||||
<Footer.Nav />
|
||||
<Footer.Bottom>
|
||||
<Footer.Social />
|
||||
</Footer.Bottom>
|
||||
</Footer.Root>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'Become a ', fontFamily: 'serif' },
|
||||
{ text: 'Twenty Partner', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: "We're building the #1 open-source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ThreeCardsIllustrationDataType } from '@/sections/ThreeCards/types';
|
||||
|
||||
export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
|
||||
eyebrow: {
|
||||
heading: {
|
||||
text: 'Which partner program is right for you?',
|
||||
fontFamily: 'sans',
|
||||
},
|
||||
},
|
||||
heading: [
|
||||
{ text: 'Find the program that fits your business ', fontFamily: 'serif' },
|
||||
{ text: 'and unlock new opportunities with Twenty', fontFamily: 'sans' },
|
||||
],
|
||||
body: { text: '' },
|
||||
illustrationCards: [
|
||||
{
|
||||
heading: { text: 'Technology Partners', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Build integrations that connect Twenty with the tools your customers already use. Help us expand the Twenty ecosystem.',
|
||||
},
|
||||
benefits: [
|
||||
{ text: 'Co-marketing opportunities' },
|
||||
{ text: 'Listing on Twenty integrations page' },
|
||||
{ text: 'Soon: earn revenue' },
|
||||
],
|
||||
attribution: undefined,
|
||||
},
|
||||
{
|
||||
heading: { text: 'Content & Community Partners', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: "Share Twenty with your audience and help shape the future of open-source CRM. We're looking for creators, educators, and community builders who want to showcase great software.",
|
||||
},
|
||||
benefits: [
|
||||
{ text: 'Revenue share for referred customers' },
|
||||
{ text: 'Exclusive content collaboration opportunities' },
|
||||
{ text: 'Marketing assets & brand resources' },
|
||||
],
|
||||
attribution: undefined,
|
||||
},
|
||||
{
|
||||
heading: { text: 'Solutions Partners', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Help customers implement, customize, and succeed with Twenty. Combine sales and services to grow your business.',
|
||||
},
|
||||
benefits: [
|
||||
{ text: 'Resale discounts & revenue share' },
|
||||
{ text: 'Marketplace listing' },
|
||||
{ text: 'Dedicated partner support' },
|
||||
],
|
||||
attribution: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { TRUSTED_BY_DATA } from '@/app/(home)/constants/trusted-by';
|
||||
import { HERO_DATA } from '@/app/partner/constants/hero';
|
||||
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/partner/constants/three-cards-illustration';
|
||||
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { ThreeCards } from '@/sections/ThreeCards/components';
|
||||
import { TrustedBy } from '@/sections/TrustedBy/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function PartnerPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<Hero.Heading page={Pages.Partner} segments={HERO_DATA.heading} />
|
||||
<Hero.Body page={Pages.Partner} body={HERO_DATA.body} />
|
||||
<Hero.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Become a partner"
|
||||
type="anchor"
|
||||
variant="outlined"
|
||||
/>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Find a partner"
|
||||
type="anchor"
|
||||
variant="contained"
|
||||
/>
|
||||
</Hero.Cta>
|
||||
<Hero.Illustration
|
||||
src="https://app.endlesstools.io/embed/1c6c8259-3276-4cf2-84d8-6b7e87e7ec95"
|
||||
title="Endless Tools Editor"
|
||||
backgroundColor={theme.colors.secondary.background[100]}
|
||||
/>
|
||||
</Hero.Root>
|
||||
|
||||
<TrustedBy.Root>
|
||||
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
|
||||
<TrustedBy.Logos
|
||||
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel}
|
||||
logos={TRUSTED_BY_DATA.logos}
|
||||
/>
|
||||
</TrustedBy.Root>
|
||||
|
||||
<ThreeCards.Root backgroundColor={theme.colors.secondary.background[5]}>
|
||||
<ThreeCards.Intro align="left">
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Heading
|
||||
segments={THREE_CARDS_ILLUSTRATION_DATA.heading}
|
||||
size="lg"
|
||||
weight="light"
|
||||
/>
|
||||
<Body body={THREE_CARDS_ILLUSTRATION_DATA.body} size="sm" />
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.IllustrationCards
|
||||
illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards}
|
||||
variant="simple"
|
||||
/>
|
||||
</ThreeCards.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'A pricing that scales ', fontFamily: 'serif' },
|
||||
{ text: 'with your need', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: "We're building the #1 open-source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { HERO_DATA } from '@/app/pricing/constants/hero';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero.Root backgroundColor={theme.colors.secondary.background[5]}>
|
||||
<Hero.Heading page={Pages.Pricing} segments={HERO_DATA.heading} />
|
||||
<Hero.Body page={Pages.Pricing} body={HERO_DATA.body} />
|
||||
</Hero.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'The CRM that moves ', fontFamily: 'serif' },
|
||||
{ text: 'as fast as you do', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: 'Modern interface. AI assistance. All the features you need, ready from day one.',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ThreeCardsIllustrationDataType } from '@/sections/ThreeCards/types';
|
||||
|
||||
export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
|
||||
eyebrow: {
|
||||
heading: { text: 'Stop settling for trade-offs.', fontFamily: 'sans' },
|
||||
},
|
||||
heading: [
|
||||
{ text: 'A modern CRM with ', fontFamily: 'serif' },
|
||||
{ text: 'an intuitive interface', fontFamily: 'sans' },
|
||||
],
|
||||
body: { text: '' },
|
||||
illustrationCards: [
|
||||
{
|
||||
heading: { text: 'Built for speed', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'Fly through your workspace with shortcuts and short load times.',
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: undefined,
|
||||
},
|
||||
{
|
||||
heading: { text: 'Real-time data', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'See updates as they happen. Work with your team and agents seamlessly.',
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: undefined,
|
||||
},
|
||||
{
|
||||
heading: { text: 'Stay in Flow', fontFamily: 'sans' },
|
||||
body: {
|
||||
text: 'AI chat, settings, and records in a side panels for fast, single-screen access.',
|
||||
},
|
||||
benefits: undefined,
|
||||
attribution: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { TRUSTED_BY_DATA } from '@/app/(home)/constants/trusted-by';
|
||||
import { HERO_DATA } from '@/app/product/constants/hero';
|
||||
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/product/constants/three-cards';
|
||||
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { ThreeCards } from '@/sections/ThreeCards/components';
|
||||
import { TrustedBy } from '@/sections/TrustedBy/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function ProductPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<Hero.Heading page={Pages.Product} segments={HERO_DATA.heading} />
|
||||
<Hero.Body page={Pages.Product} body={HERO_DATA.body} />
|
||||
<Hero.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label="Get started"
|
||||
type="anchor"
|
||||
variant="contained"
|
||||
/>
|
||||
</Hero.Cta>
|
||||
<Hero.Illustration
|
||||
src="https://app.endlesstools.io/embed/0bcf3ac2-58cf-4cd5-90bd-e8fada9816a9"
|
||||
title="Endless Tools Editor"
|
||||
backgroundColor={theme.colors.secondary.background[5]}
|
||||
/>
|
||||
|
||||
<TrustedBy.Root>
|
||||
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
|
||||
<TrustedBy.Logos
|
||||
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel}
|
||||
logos={TRUSTED_BY_DATA.logos}
|
||||
/>
|
||||
</TrustedBy.Root>
|
||||
|
||||
<ThreeCards.Root backgroundColor={theme.colors.primary.background[100]}>
|
||||
<ThreeCards.Intro page={Pages.Product} align="left">
|
||||
<Eyebrow
|
||||
colorScheme="primary"
|
||||
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
|
||||
/>
|
||||
<Heading
|
||||
segments={THREE_CARDS_ILLUSTRATION_DATA.heading}
|
||||
size="lg"
|
||||
weight="light"
|
||||
/>
|
||||
<Body body={THREE_CARDS_ILLUSTRATION_DATA.body} size="sm" />
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.IllustrationCards
|
||||
illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards}
|
||||
/>
|
||||
</ThreeCards.Root>
|
||||
</Hero.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { HeroDataType } from '@/sections/Hero/types';
|
||||
|
||||
export const HERO_DATA: HeroDataType = {
|
||||
heading: [
|
||||
{ text: 'The future of CRM is built, ', fontFamily: 'serif' },
|
||||
{ text: 'not bought.', fontFamily: 'sans' },
|
||||
],
|
||||
body: {
|
||||
text: 'Most packaged software makes companies more similar. CRM is no exception. AI is turning software into a commodity. That does not make your go-to-market a commodity. It makes it the battleground.',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { HERO_DATA } from '@/app/why-twenty/constants/hero';
|
||||
import { Pages } from '@/enums/pages';
|
||||
import { Hero } from '@/sections/Hero/components';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export default function WhyTwentyPage() {
|
||||
return (
|
||||
<Hero.Root backgroundColor={theme.colors.secondary.background[100]}>
|
||||
<Hero.Heading page={Pages.WhyTwenty} segments={HERO_DATA.heading} size="xl" />
|
||||
<Hero.Body page={Pages.WhyTwenty} body={HERO_DATA.body} />
|
||||
<Hero.WhyTwentyVisual />
|
||||
</Hero.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { theme } from '@/theme';
|
||||
import { css } from '@linaria/core';
|
||||
import { BodyType } from './types/Body';
|
||||
|
||||
const bodyClassName = css`
|
||||
margin: 0;
|
||||
|
||||
&[data-family='sans'] {
|
||||
font-family: ${theme.font.family.sans};
|
||||
}
|
||||
|
||||
&[data-family='serif'] {
|
||||
font-family: ${theme.font.family.serif};
|
||||
}
|
||||
|
||||
&[data-family='mono'] {
|
||||
font-family: ${theme.font.family.mono};
|
||||
}
|
||||
|
||||
&[data-weight='light'] {
|
||||
font-weight: ${theme.font.weight.light};
|
||||
}
|
||||
|
||||
&[data-weight='regular'] {
|
||||
font-weight: ${theme.font.weight.regular};
|
||||
}
|
||||
|
||||
&[data-weight='medium'] {
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
}
|
||||
|
||||
&[data-size='md'] {
|
||||
font-size: ${theme.font.size(4)};
|
||||
line-height: ${theme.lineHeight(5.5)};
|
||||
}
|
||||
|
||||
&[data-size='sm'] {
|
||||
font-size: ${theme.font.size(4)};
|
||||
line-height: ${theme.lineHeight(5.5)};
|
||||
}
|
||||
|
||||
&[data-size='xs'] {
|
||||
font-size: ${theme.font.size(3)};
|
||||
line-height: ${theme.lineHeight(3.5)};
|
||||
}
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
&[data-size='md'] {
|
||||
font-size: ${theme.font.size(4.5)};
|
||||
line-height: ${theme.lineHeight(6)};
|
||||
}
|
||||
|
||||
&[data-size='sm'] {
|
||||
font-size: ${theme.font.size(4)};
|
||||
line-height: ${theme.lineHeight(5.5)};
|
||||
}
|
||||
|
||||
&[data-size='xs'] {
|
||||
font-size: ${theme.font.size(3)};
|
||||
line-height: ${theme.lineHeight(3.5)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type BodyAs = 'p' | 'span' | 'div';
|
||||
export type BodyFamily = 'sans' | 'serif' | 'mono';
|
||||
export type BodyWeight = 'light' | 'regular' | 'medium';
|
||||
export type BodySize = 'md' | 'sm' | 'xs';
|
||||
|
||||
export type BodyProps = {
|
||||
as?: BodyAs;
|
||||
body: BodyType;
|
||||
family?: BodyFamily;
|
||||
weight?: BodyWeight;
|
||||
size?: BodySize;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Body({
|
||||
as: Tag = 'p',
|
||||
body,
|
||||
family = 'sans',
|
||||
weight = 'regular',
|
||||
size = 'md',
|
||||
className,
|
||||
}: BodyProps) {
|
||||
const rootClassName = [bodyClassName, className].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<Tag
|
||||
className={rootClassName}
|
||||
data-family={family}
|
||||
data-weight={weight}
|
||||
data-size={size}
|
||||
>
|
||||
{body.text}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type BodyType = {
|
||||
text: string;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import { ButtonShape } from './ButtonShape';
|
||||
|
||||
export const buttonBaseStyles = `
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: ${theme.radius(2)};
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-family: ${theme.font.family.mono};
|
||||
font-size: ${theme.font.size(3)};
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
height: ${theme.spacing(10)};
|
||||
justify-content: center;
|
||||
letter-spacing: 0;
|
||||
padding: 0 ${theme.spacing(5)};
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Label = styled.span`
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&[data-color='primary'] {
|
||||
color: ${theme.colors.primary.text[100]};
|
||||
}
|
||||
|
||||
&[data-color='secondary'] {
|
||||
color: ${theme.colors.secondary.text[100]};
|
||||
}
|
||||
`;
|
||||
|
||||
export type BaseButtonProps = {
|
||||
color: 'primary' | 'secondary';
|
||||
label: string;
|
||||
variant: 'contained' | 'outlined';
|
||||
};
|
||||
|
||||
export function BaseButton({ color, label, variant }: BaseButtonProps) {
|
||||
let fillColor: string;
|
||||
let strokeColor: string;
|
||||
let labelColor: 'primary' | 'secondary';
|
||||
|
||||
switch (`${variant}.${color}`) {
|
||||
case 'contained.primary':
|
||||
fillColor = theme.colors.primary.background[100];
|
||||
strokeColor = 'none';
|
||||
labelColor = 'primary';
|
||||
break;
|
||||
case 'contained.secondary':
|
||||
fillColor = theme.colors.secondary.background[100];
|
||||
strokeColor = 'none';
|
||||
labelColor = 'secondary';
|
||||
break;
|
||||
case 'outlined.primary':
|
||||
fillColor = 'none';
|
||||
strokeColor = theme.colors.primary.background[100];
|
||||
labelColor = 'secondary';
|
||||
break;
|
||||
case 'outlined.secondary':
|
||||
fillColor = 'none';
|
||||
strokeColor = theme.colors.secondary.background[100];
|
||||
labelColor = 'primary';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled button appearance: ${variant} ${color}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonShape fillColor={fillColor} strokeColor={strokeColor} />
|
||||
<Label data-color={labelColor}>{label}</Label>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { styled } from "@linaria/react";
|
||||
|
||||
interface ButtonShapeProps {
|
||||
fillColor: string;
|
||||
strokeColor: string;
|
||||
}
|
||||
|
||||
const LEFT_FILL = "M4 0 A4 4 0 0 0 0 4 V36 A4 4 0 0 0 4 40 Z";
|
||||
const LEFT_OUTLINE = "M4 0.5 A3.5 3.5 0 0 0 0.5 4 V36 A3.5 3.5 0 0 0 4 39.5";
|
||||
|
||||
const RIGHT_FILL = "M0 0 h11 a4 4 0 0 1 4 4 v20.523 a6 6 0 0 1 -1.544 4.019 l-8.548 9.477 A6 6 0 0 1 0.453 40 H0 Z";
|
||||
const RIGHT_OUTLINE = "M0 0.5 h11 a3.5 3.5 0 0 1 3.5 3.5 v20.523 a5.5 5.5 0 0 1 -1.416 3.684 l-8.547 9.477 a5.5 5.5 0 0 1 -4.084 1.816 H0";
|
||||
|
||||
const ShapeContainer = styled.div`
|
||||
display: flex;
|
||||
height: 40px;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
`;
|
||||
|
||||
const LeftCap = styled.svg`
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const MiddleSegment = styled.svg`
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
margin: 0 -1px;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const RightCap = styled.svg`
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export function ButtonShape({ fillColor, strokeColor }: ButtonShapeProps) {
|
||||
const isOutline = fillColor === "none";
|
||||
|
||||
return (
|
||||
<ShapeContainer>
|
||||
<LeftCap width="4" height="40" viewBox="0 0 4 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{isOutline ? (
|
||||
<path d={LEFT_OUTLINE} fill={fillColor} stroke={strokeColor} strokeWidth="1" />
|
||||
) : (
|
||||
<path d={LEFT_FILL} fill={fillColor} stroke={strokeColor} />
|
||||
)}
|
||||
</LeftCap>
|
||||
|
||||
<MiddleSegment width="100%" height="40" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{isOutline ? (
|
||||
<>
|
||||
<line x1="0" y1="0.5" x2="100%" y2="0.5" stroke={strokeColor} strokeWidth="1" />
|
||||
<line x1="0" y1="39.5" x2="100%" y2="39.5" stroke={strokeColor} strokeWidth="1" />
|
||||
</>
|
||||
) : (
|
||||
<rect width="100%" height="40" fill={fillColor} stroke={strokeColor} />
|
||||
)}
|
||||
</MiddleSegment>
|
||||
|
||||
<RightCap width="15" height="40" viewBox="0 0 15 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{isOutline ? (
|
||||
<path d={RIGHT_OUTLINE} fill={fillColor} stroke={strokeColor} strokeWidth="1" strokeLinejoin="round" strokeLinecap="round" />
|
||||
) : (
|
||||
<path d={RIGHT_FILL} fill={fillColor} stroke={strokeColor} />
|
||||
)}
|
||||
</RightCap>
|
||||
</ShapeContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { LinkButtonType } from '@/design-system/components/Button/types/LinkButtonType';
|
||||
import { styled } from '@linaria/react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
BaseButton,
|
||||
type BaseButtonProps,
|
||||
buttonBaseStyles,
|
||||
} from './BaseButton';
|
||||
|
||||
const StyledButtonAnchor = styled.a`
|
||||
${buttonBaseStyles}
|
||||
`;
|
||||
|
||||
const StyledButtonLink = styled(Link)`
|
||||
${buttonBaseStyles}
|
||||
`;
|
||||
|
||||
export type LinkButtonPresentation = 'anchor' | 'link';
|
||||
|
||||
export type LinkButtonProps = Omit<BaseButtonProps, 'label'> &
|
||||
LinkButtonType & { type: LinkButtonPresentation };
|
||||
|
||||
export function LinkButton({
|
||||
color,
|
||||
href,
|
||||
label,
|
||||
type,
|
||||
variant,
|
||||
}: LinkButtonProps) {
|
||||
const inner = <BaseButton color={color} label={label} variant={variant} />;
|
||||
|
||||
if (type === 'anchor') {
|
||||
return (
|
||||
<StyledButtonAnchor
|
||||
href={href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{inner}
|
||||
</StyledButtonAnchor>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButtonLink href={href}>
|
||||
{inner}
|
||||
</StyledButtonLink>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
BaseButton,
|
||||
type BaseButtonProps,
|
||||
buttonBaseStyles,
|
||||
} from './BaseButton';
|
||||
import type { SubmitButtonType } from './types/SubmitButtonType';
|
||||
|
||||
const StyledSubmitButton = styled.button`
|
||||
${buttonBaseStyles}
|
||||
`;
|
||||
|
||||
export type SubmitButtonProps = Omit<BaseButtonProps, 'label'> &
|
||||
SubmitButtonType & { onClick?: () => void };
|
||||
|
||||
export function SubmitButton({
|
||||
color,
|
||||
label,
|
||||
onClick,
|
||||
variant,
|
||||
}: SubmitButtonProps) {
|
||||
return (
|
||||
<StyledSubmitButton type="submit" onClick={onClick}>
|
||||
<BaseButton color={color} label={label} variant={variant} />
|
||||
</StyledSubmitButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type LinkButtonType = { href: string; label: string };
|
||||
@@ -0,0 +1 @@
|
||||
export type SubmitButtonType = { label: string };
|
||||
@@ -0,0 +1,15 @@
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const ContainerRoot = styled.div`
|
||||
margin: 0 auto;
|
||||
max-width: ${theme.breakpoints.maxContent}px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type ContainerProps = { children: ReactNode; className?: string };
|
||||
|
||||
export function Container({ children, className }: ContainerProps) {
|
||||
return <ContainerRoot className={className}>{children}</ContainerRoot>;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Heading } from '@/design-system/components/Heading/Heading';
|
||||
import { HeadingType } from '@/design-system/components/Heading/types/Heading';
|
||||
import { RectangleFillIcon } from '@/icons';
|
||||
import { theme } from '@/theme';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const EyebrowRow = styled.div`
|
||||
align-items: start;
|
||||
display: grid;
|
||||
gap: ${theme.spacing(2)};
|
||||
grid-template-columns: auto 1fr;
|
||||
text-align: start;
|
||||
`;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${theme.lineHeight(6)};
|
||||
`;
|
||||
|
||||
const eyebrowColorPrimary = css`
|
||||
color: ${theme.colors.primary.text[60]};
|
||||
`;
|
||||
|
||||
const eyebrowColorSecondary = css`
|
||||
color: ${theme.colors.secondary.text[60]};
|
||||
`;
|
||||
|
||||
type EyebrowProps = {
|
||||
heading: HeadingType;
|
||||
colorScheme: 'primary' | 'secondary';
|
||||
};
|
||||
|
||||
export function Eyebrow({ heading, colorScheme }: EyebrowProps) {
|
||||
const colorClassName =
|
||||
colorScheme === 'primary' ? eyebrowColorPrimary : eyebrowColorSecondary;
|
||||
|
||||
return (
|
||||
<EyebrowRow>
|
||||
<IconWrapper>
|
||||
<RectangleFillIcon size={14} fillColor={theme.colors.highlight[100]} />
|
||||
</IconWrapper>
|
||||
<Heading
|
||||
as="h3"
|
||||
className={colorClassName}
|
||||
segments={{ fontFamily: heading.fontFamily, text: heading.text }}
|
||||
size="xs"
|
||||
weight="medium"
|
||||
/>
|
||||
</EyebrowRow>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { HeadingType } from '@/design-system/components/Heading/types/Heading';
|
||||
|
||||
export type EyebrowType = { heading: HeadingType };
|
||||
@@ -0,0 +1,130 @@
|
||||
import { theme } from '@/theme';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
import { HeadingType } from './types/Heading';
|
||||
|
||||
const headingRootClassName = css`
|
||||
margin: 0;
|
||||
|
||||
&[data-weight='light'] {
|
||||
font-weight: ${theme.font.weight.light};
|
||||
}
|
||||
|
||||
&[data-weight='regular'] {
|
||||
font-weight: ${theme.font.weight.regular};
|
||||
}
|
||||
|
||||
&[data-weight='medium'] {
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
}
|
||||
|
||||
&[data-size='xl'] {
|
||||
font-size: ${theme.font.size(15)};
|
||||
line-height: ${theme.lineHeight(16.5)};
|
||||
}
|
||||
|
||||
&[data-size='lg'] {
|
||||
font-size: ${theme.font.size(10)};
|
||||
line-height: ${theme.lineHeight(11.5)};
|
||||
}
|
||||
|
||||
&[data-size='md'] {
|
||||
font-size: ${theme.font.size(10)};
|
||||
line-height: ${theme.lineHeight(11.5)};
|
||||
}
|
||||
|
||||
&[data-size='sm'] {
|
||||
font-size: ${theme.font.size(8)};
|
||||
line-height: ${theme.lineHeight(10)};
|
||||
}
|
||||
|
||||
&[data-size='xs'] {
|
||||
font-size: ${theme.font.size(4.5)};
|
||||
line-height: ${theme.lineHeight(6)};
|
||||
}
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
&[data-size='xl'] {
|
||||
font-size: ${theme.font.size(20)};
|
||||
line-height: ${theme.lineHeight(21.5)};
|
||||
}
|
||||
|
||||
&[data-size='lg'] {
|
||||
font-size: ${theme.font.size(15)};
|
||||
line-height: ${theme.lineHeight(16.5)};
|
||||
}
|
||||
|
||||
&[data-size='md'] {
|
||||
font-size: ${theme.font.size(12)};
|
||||
line-height: ${theme.lineHeight(14)};
|
||||
}
|
||||
|
||||
&[data-size='sm'] {
|
||||
font-size: ${theme.font.size(8)};
|
||||
line-height: ${theme.lineHeight(10)};
|
||||
}
|
||||
|
||||
&[data-size='xs'] {
|
||||
font-size: ${theme.font.size(5.5)};
|
||||
line-height: ${theme.lineHeight(7)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSpan = styled.span`
|
||||
&[data-family='sans'] {
|
||||
font-family: ${theme.font.family.sans};
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
&[data-family='serif'] {
|
||||
font-family: ${theme.font.family.serif};
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
&[data-family='mono'] {
|
||||
font-family: ${theme.font.family.mono};
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
`;
|
||||
|
||||
export type HeadingAs = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
export type HeadingFamily = 'sans' | 'serif' | 'mono';
|
||||
export type HeadingWeight = 'light' | 'regular' | 'medium';
|
||||
export type HeadingSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';
|
||||
|
||||
export type HeadingProps = {
|
||||
as?: HeadingAs;
|
||||
segments: HeadingType | HeadingType[];
|
||||
weight?: HeadingWeight;
|
||||
size?: HeadingSize;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Heading({
|
||||
as: Tag = 'h1',
|
||||
segments,
|
||||
weight = 'regular',
|
||||
size = 'md',
|
||||
className,
|
||||
}: HeadingProps) {
|
||||
const rootClassName = [headingRootClassName, className]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<Tag className={rootClassName} data-weight={weight} data-size={size}>
|
||||
{Array.isArray(segments) ? (
|
||||
segments.map((segment, index) => (
|
||||
<StyledSpan key={index} data-family={segment.fontFamily}>
|
||||
{segment.text}
|
||||
</StyledSpan>
|
||||
))
|
||||
) : (
|
||||
<StyledSpan data-family={segments.fontFamily}>
|
||||
{segments.text}
|
||||
</StyledSpan>
|
||||
)}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type HeadingType = {
|
||||
fontFamily: 'sans' | 'serif' | 'mono';
|
||||
text: string;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ComponentType } from "react";
|
||||
import { theme } from "@/theme";
|
||||
import { styled } from "@linaria/react";
|
||||
|
||||
type IconComponent =
|
||||
| ComponentType<{ size: number; fillColor: string }>
|
||||
| ComponentType<{ size: number; strokeColor: string }>;
|
||||
|
||||
interface IconButtonProps {
|
||||
icon: IconComponent;
|
||||
ariaLabel: string;
|
||||
borderColor: string;
|
||||
iconFillColor: string;
|
||||
iconSize: number;
|
||||
iconStrokeColor: string;
|
||||
size: number;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
ariaExpanded?: boolean;
|
||||
}
|
||||
|
||||
const StyledButton = styled.button<{ size: number; borderColor: string }>`
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 1px solid ${(p) => p.borderColor};
|
||||
border-radius: ${theme.radius(2)};
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
height: ${(p) => p.size}px;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: ${(p) => p.size}px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid ${theme.colors.highlight[100]};
|
||||
outline-offset: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function IconButton({
|
||||
icon: Icon,
|
||||
ariaLabel,
|
||||
borderColor,
|
||||
iconFillColor,
|
||||
iconSize,
|
||||
iconStrokeColor,
|
||||
size,
|
||||
onClick,
|
||||
ariaExpanded,
|
||||
}: IconButtonProps) {
|
||||
return (
|
||||
<StyledButton
|
||||
type="button"
|
||||
size={size}
|
||||
borderColor={borderColor}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={ariaExpanded}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon
|
||||
size={iconSize}
|
||||
strokeColor={iconStrokeColor}
|
||||
fillColor={iconFillColor}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type IllustrationType = {
|
||||
src: string;
|
||||
title: string;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import NextImage from 'next/image';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ImageType } from './types/Image';
|
||||
|
||||
const ImageRoot = styled.div`
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type ImageComponentProps = ImageType & { className?: string };
|
||||
|
||||
export function Image({ alt, className, src }: ImageComponentProps) {
|
||||
return (
|
||||
<ImageRoot className={className}>
|
||||
<NextImage
|
||||
alt={alt}
|
||||
fill
|
||||
sizes="100vw"
|
||||
src={src}
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
</ImageRoot>
|
||||
);
|
||||
}
|
||||