Compare commits

..
543 changed files with 16227 additions and 22328 deletions
+4
View File
@@ -17,6 +17,10 @@
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
},
"twenty": {
"type": "streamable-http",
"url": "https://twentyfortwenty.twenty.com/mcp"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "1.22.0",
"version": "1.22.0-canary.6",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-client-sdk",
"version": "1.22.0",
"version": "1.22.0-canary.6",
"sideEffects": false,
"license": "AGPL-3.0",
"scripts": {
@@ -1266,37 +1266,6 @@ type Analytics {
success: Boolean!
}
type ApprovedAccessDomain {
id: UUID!
domain: String!
isValidated: Boolean!
createdAt: DateTime!
}
type FileWithSignedUrl {
id: UUID!
path: String!
size: Float!
createdAt: DateTime!
url: String!
}
type EnterpriseLicenseInfoDTO {
isValid: Boolean!
licensee: String
expiresAt: DateTime
subscriptionId: String
}
type EnterpriseSubscriptionStatusDTO {
status: String!
licensee: String
expiresAt: DateTime
cancelAt: DateTime
currentPeriodEnd: DateTime
isCancellationScheduled: Boolean!
}
type BillingSubscriptionSchedulePhaseItem {
price: String!
quantity: Float
@@ -1450,11 +1419,42 @@ type BillingUpdate {
billingSubscriptions: [BillingSubscription!]!
}
type EnterpriseLicenseInfoDTO {
isValid: Boolean!
licensee: String
expiresAt: DateTime
subscriptionId: String
}
type EnterpriseSubscriptionStatusDTO {
status: String!
licensee: String
expiresAt: DateTime
cancelAt: DateTime
currentPeriodEnd: DateTime
isCancellationScheduled: Boolean!
}
type OnboardingStepSuccess {
"""Boolean that confirms query was dispatched"""
success: Boolean!
}
type ApprovedAccessDomain {
id: UUID!
domain: String!
isValidated: Boolean!
createdAt: DateTime!
}
type FileWithSignedUrl {
id: UUID!
path: String!
size: Float!
createdAt: DateTime!
url: String!
}
type WorkspaceInvitation {
id: UUID!
email: String!
@@ -1732,12 +1732,6 @@ type PublicWorkspaceData {
workspaceUrls: WorkspaceUrls!
}
type PublicWorkspaceDataSummary {
id: UUID!
logo: String
displayName: String
}
type NativeModelCapabilities {
webSearch: Boolean
twitterSearch: Boolean
@@ -2651,7 +2645,6 @@ type CommandMenuItem {
hotKeys: [String!]
conditionalAvailabilityExpression: String
availabilityObjectMetadataId: UUID
pageLayoutId: UUID
applicationId: UUID
createdAt: DateTime!
updatedAt: DateTime!
@@ -3351,7 +3344,6 @@ type Query {
currentUser: User!
currentWorkspace: Workspace!
getPublicWorkspaceDataByDomain(origin: String): PublicWorkspaceData!
getPublicWorkspaceDataById(id: UUID!): PublicWorkspaceDataSummary!
getSSOIdentityProviders: [FindAvailableSSOIDP!]!
getConnectedImapSmtpCaldavAccount(id: UUID!): ConnectedImapSmtpCaldavAccount!
getAutoCompleteAddress(address: String!, token: String!, country: String, isFieldCity: Boolean): [AutocompleteResult!]!
@@ -4184,7 +4176,6 @@ input CreateCommandMenuItemInput {
conditionalAvailabilityExpression: String
availabilityObjectMetadataId: UUID
payload: JSON
pageLayoutId: UUID
}
input UpdateCommandMenuItemInput {
@@ -4198,7 +4189,6 @@ input UpdateCommandMenuItemInput {
availabilityObjectMetadataId: UUID
engineComponentKey: EngineComponentKey
hotKeys: [String!]
pageLayoutId: UUID
}
input CreateFrontComponentInput {
@@ -4609,7 +4599,6 @@ input UpdateApplicationRegistrationVariableInput {
input UpdateApplicationRegistrationVariablePayload {
value: String
resetValue: Boolean
description: String
}
@@ -989,41 +989,6 @@ export interface Analytics {
__typename: 'Analytics'
}
export interface ApprovedAccessDomain {
id: Scalars['UUID']
domain: Scalars['String']
isValidated: Scalars['Boolean']
createdAt: Scalars['DateTime']
__typename: 'ApprovedAccessDomain'
}
export interface FileWithSignedUrl {
id: Scalars['UUID']
path: Scalars['String']
size: Scalars['Float']
createdAt: Scalars['DateTime']
url: Scalars['String']
__typename: 'FileWithSignedUrl'
}
export interface EnterpriseLicenseInfoDTO {
isValid: Scalars['Boolean']
licensee?: Scalars['String']
expiresAt?: Scalars['DateTime']
subscriptionId?: Scalars['String']
__typename: 'EnterpriseLicenseInfoDTO'
}
export interface EnterpriseSubscriptionStatusDTO {
status: Scalars['String']
licensee?: Scalars['String']
expiresAt?: Scalars['DateTime']
cancelAt?: Scalars['DateTime']
currentPeriodEnd?: Scalars['DateTime']
isCancellationScheduled: Scalars['Boolean']
__typename: 'EnterpriseSubscriptionStatusDTO'
}
export interface BillingSubscriptionSchedulePhaseItem {
price: Scalars['String']
quantity?: Scalars['Float']
@@ -1169,12 +1134,47 @@ export interface BillingUpdate {
__typename: 'BillingUpdate'
}
export interface EnterpriseLicenseInfoDTO {
isValid: Scalars['Boolean']
licensee?: Scalars['String']
expiresAt?: Scalars['DateTime']
subscriptionId?: Scalars['String']
__typename: 'EnterpriseLicenseInfoDTO'
}
export interface EnterpriseSubscriptionStatusDTO {
status: Scalars['String']
licensee?: Scalars['String']
expiresAt?: Scalars['DateTime']
cancelAt?: Scalars['DateTime']
currentPeriodEnd?: Scalars['DateTime']
isCancellationScheduled: Scalars['Boolean']
__typename: 'EnterpriseSubscriptionStatusDTO'
}
export interface OnboardingStepSuccess {
/** Boolean that confirms query was dispatched */
success: Scalars['Boolean']
__typename: 'OnboardingStepSuccess'
}
export interface ApprovedAccessDomain {
id: Scalars['UUID']
domain: Scalars['String']
isValidated: Scalars['Boolean']
createdAt: Scalars['DateTime']
__typename: 'ApprovedAccessDomain'
}
export interface FileWithSignedUrl {
id: Scalars['UUID']
path: Scalars['String']
size: Scalars['Float']
createdAt: Scalars['DateTime']
url: Scalars['String']
__typename: 'FileWithSignedUrl'
}
export interface WorkspaceInvitation {
id: Scalars['UUID']
email: Scalars['String']
@@ -1430,13 +1430,6 @@ export interface PublicWorkspaceData {
__typename: 'PublicWorkspaceData'
}
export interface PublicWorkspaceDataSummary {
id: Scalars['UUID']
logo?: Scalars['String']
displayName?: Scalars['String']
__typename: 'PublicWorkspaceDataSummary'
}
export interface NativeModelCapabilities {
webSearch?: Scalars['Boolean']
twitterSearch?: Scalars['Boolean']
@@ -2368,7 +2361,6 @@ export interface CommandMenuItem {
hotKeys?: Scalars['String'][]
conditionalAvailabilityExpression?: Scalars['String']
availabilityObjectMetadataId?: Scalars['UUID']
pageLayoutId?: Scalars['UUID']
applicationId?: Scalars['UUID']
createdAt: Scalars['DateTime']
updatedAt: Scalars['DateTime']
@@ -2913,7 +2905,6 @@ export interface Query {
currentUser: User
currentWorkspace: Workspace
getPublicWorkspaceDataByDomain: PublicWorkspaceData
getPublicWorkspaceDataById: PublicWorkspaceDataSummary
getSSOIdentityProviders: FindAvailableSSOIDP[]
getConnectedImapSmtpCaldavAccount: ConnectedImapSmtpCaldavAccount
getAutoCompleteAddress: AutocompleteResult[]
@@ -4239,45 +4230,6 @@ export interface AnalyticsGenqlSelection{
__scalar?: boolean | number
}
export interface ApprovedAccessDomainGenqlSelection{
id?: boolean | number
domain?: boolean | number
isValidated?: boolean | number
createdAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface FileWithSignedUrlGenqlSelection{
id?: boolean | number
path?: boolean | number
size?: boolean | number
createdAt?: boolean | number
url?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface EnterpriseLicenseInfoDTOGenqlSelection{
isValid?: boolean | number
licensee?: boolean | number
expiresAt?: boolean | number
subscriptionId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface EnterpriseSubscriptionStatusDTOGenqlSelection{
status?: boolean | number
licensee?: boolean | number
expiresAt?: boolean | number
cancelAt?: boolean | number
currentPeriodEnd?: boolean | number
isCancellationScheduled?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface BillingSubscriptionSchedulePhaseItemGenqlSelection{
price?: boolean | number
quantity?: boolean | number
@@ -4425,6 +4377,26 @@ export interface BillingUpdateGenqlSelection{
__scalar?: boolean | number
}
export interface EnterpriseLicenseInfoDTOGenqlSelection{
isValid?: boolean | number
licensee?: boolean | number
expiresAt?: boolean | number
subscriptionId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface EnterpriseSubscriptionStatusDTOGenqlSelection{
status?: boolean | number
licensee?: boolean | number
expiresAt?: boolean | number
cancelAt?: boolean | number
currentPeriodEnd?: boolean | number
isCancellationScheduled?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface OnboardingStepSuccessGenqlSelection{
/** Boolean that confirms query was dispatched */
success?: boolean | number
@@ -4432,6 +4404,25 @@ export interface OnboardingStepSuccessGenqlSelection{
__scalar?: boolean | number
}
export interface ApprovedAccessDomainGenqlSelection{
id?: boolean | number
domain?: boolean | number
isValidated?: boolean | number
createdAt?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface FileWithSignedUrlGenqlSelection{
id?: boolean | number
path?: boolean | number
size?: boolean | number
createdAt?: boolean | number
url?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface WorkspaceInvitationGenqlSelection{
id?: boolean | number
email?: boolean | number
@@ -4689,14 +4680,6 @@ export interface PublicWorkspaceDataGenqlSelection{
__scalar?: boolean | number
}
export interface PublicWorkspaceDataSummaryGenqlSelection{
id?: boolean | number
logo?: boolean | number
displayName?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface NativeModelCapabilitiesGenqlSelection{
webSearch?: boolean | number
twitterSearch?: boolean | number
@@ -5707,7 +5690,6 @@ export interface CommandMenuItemGenqlSelection{
hotKeys?: boolean | number
conditionalAvailabilityExpression?: boolean | number
availabilityObjectMetadataId?: boolean | number
pageLayoutId?: boolean | number
applicationId?: boolean | number
createdAt?: boolean | number
updatedAt?: boolean | number
@@ -6292,7 +6274,6 @@ export interface QueryGenqlSelection{
currentUser?: UserGenqlSelection
currentWorkspace?: WorkspaceGenqlSelection
getPublicWorkspaceDataByDomain?: (PublicWorkspaceDataGenqlSelection & { __args?: {origin?: (Scalars['String'] | null)} })
getPublicWorkspaceDataById?: (PublicWorkspaceDataSummaryGenqlSelection & { __args: {id: Scalars['UUID']} })
getSSOIdentityProviders?: FindAvailableSSOIDPGenqlSelection
getConnectedImapSmtpCaldavAccount?: (ConnectedImapSmtpCaldavAccountGenqlSelection & { __args: {id: Scalars['UUID']} })
getAutoCompleteAddress?: (AutocompleteResultGenqlSelection & { __args: {address: Scalars['String'], token: Scalars['String'], country?: (Scalars['String'] | null), isFieldCity?: (Scalars['Boolean'] | null)} })
@@ -6744,9 +6725,9 @@ update: UpdateLogicFunctionFromSourceInputUpdates}
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),toolInputSchema?: (Scalars['JSON'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),isTool?: (Scalars['Boolean'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null),pageLayoutId?: (Scalars['UUID'] | null)}
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null)}
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null),pageLayoutId?: (Scalars['UUID'] | null)}
export interface UpdateCommandMenuItemInput {id: Scalars['UUID'],label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),engineComponentKey?: (EngineComponentKey | null),hotKeys?: (Scalars['String'][] | null)}
export interface CreateFrontComponentInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),sourceComponentPath: Scalars['String'],builtComponentPath: Scalars['String'],componentName: Scalars['String'],builtComponentChecksum: Scalars['String']}
@@ -6878,7 +6859,7 @@ export interface CreateApplicationRegistrationVariableInput {applicationRegistra
export interface UpdateApplicationRegistrationVariableInput {id: Scalars['String'],update: UpdateApplicationRegistrationVariablePayload}
export interface UpdateApplicationRegistrationVariablePayload {value?: (Scalars['String'] | null),resetValue?: (Scalars['Boolean'] | null),description?: (Scalars['String'] | null)}
export interface UpdateApplicationRegistrationVariablePayload {value?: (Scalars['String'] | null),description?: (Scalars['String'] | null)}
export interface UpdateWorkspaceMemberSettingsInput {workspaceMemberId: Scalars['UUID'],update: Scalars['JSON']}
@@ -7493,38 +7474,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const ApprovedAccessDomain_possibleTypes: string[] = ['ApprovedAccessDomain']
export const isApprovedAccessDomain = (obj?: { __typename?: any } | null): obj is ApprovedAccessDomain => {
if (!obj?.__typename) throw new Error('__typename is missing in "isApprovedAccessDomain"')
return ApprovedAccessDomain_possibleTypes.includes(obj.__typename)
}
const FileWithSignedUrl_possibleTypes: string[] = ['FileWithSignedUrl']
export const isFileWithSignedUrl = (obj?: { __typename?: any } | null): obj is FileWithSignedUrl => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFileWithSignedUrl"')
return FileWithSignedUrl_possibleTypes.includes(obj.__typename)
}
const EnterpriseLicenseInfoDTO_possibleTypes: string[] = ['EnterpriseLicenseInfoDTO']
export const isEnterpriseLicenseInfoDTO = (obj?: { __typename?: any } | null): obj is EnterpriseLicenseInfoDTO => {
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseLicenseInfoDTO"')
return EnterpriseLicenseInfoDTO_possibleTypes.includes(obj.__typename)
}
const EnterpriseSubscriptionStatusDTO_possibleTypes: string[] = ['EnterpriseSubscriptionStatusDTO']
export const isEnterpriseSubscriptionStatusDTO = (obj?: { __typename?: any } | null): obj is EnterpriseSubscriptionStatusDTO => {
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseSubscriptionStatusDTO"')
return EnterpriseSubscriptionStatusDTO_possibleTypes.includes(obj.__typename)
}
const BillingSubscriptionSchedulePhaseItem_possibleTypes: string[] = ['BillingSubscriptionSchedulePhaseItem']
export const isBillingSubscriptionSchedulePhaseItem = (obj?: { __typename?: any } | null): obj is BillingSubscriptionSchedulePhaseItem => {
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingSubscriptionSchedulePhaseItem"')
@@ -7653,6 +7602,22 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const EnterpriseLicenseInfoDTO_possibleTypes: string[] = ['EnterpriseLicenseInfoDTO']
export const isEnterpriseLicenseInfoDTO = (obj?: { __typename?: any } | null): obj is EnterpriseLicenseInfoDTO => {
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseLicenseInfoDTO"')
return EnterpriseLicenseInfoDTO_possibleTypes.includes(obj.__typename)
}
const EnterpriseSubscriptionStatusDTO_possibleTypes: string[] = ['EnterpriseSubscriptionStatusDTO']
export const isEnterpriseSubscriptionStatusDTO = (obj?: { __typename?: any } | null): obj is EnterpriseSubscriptionStatusDTO => {
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseSubscriptionStatusDTO"')
return EnterpriseSubscriptionStatusDTO_possibleTypes.includes(obj.__typename)
}
const OnboardingStepSuccess_possibleTypes: string[] = ['OnboardingStepSuccess']
export const isOnboardingStepSuccess = (obj?: { __typename?: any } | null): obj is OnboardingStepSuccess => {
if (!obj?.__typename) throw new Error('__typename is missing in "isOnboardingStepSuccess"')
@@ -7661,6 +7626,22 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const ApprovedAccessDomain_possibleTypes: string[] = ['ApprovedAccessDomain']
export const isApprovedAccessDomain = (obj?: { __typename?: any } | null): obj is ApprovedAccessDomain => {
if (!obj?.__typename) throw new Error('__typename is missing in "isApprovedAccessDomain"')
return ApprovedAccessDomain_possibleTypes.includes(obj.__typename)
}
const FileWithSignedUrl_possibleTypes: string[] = ['FileWithSignedUrl']
export const isFileWithSignedUrl = (obj?: { __typename?: any } | null): obj is FileWithSignedUrl => {
if (!obj?.__typename) throw new Error('__typename is missing in "isFileWithSignedUrl"')
return FileWithSignedUrl_possibleTypes.includes(obj.__typename)
}
const WorkspaceInvitation_possibleTypes: string[] = ['WorkspaceInvitation']
export const isWorkspaceInvitation = (obj?: { __typename?: any } | null): obj is WorkspaceInvitation => {
if (!obj?.__typename) throw new Error('__typename is missing in "isWorkspaceInvitation"')
@@ -7869,14 +7850,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const PublicWorkspaceDataSummary_possibleTypes: string[] = ['PublicWorkspaceDataSummary']
export const isPublicWorkspaceDataSummary = (obj?: { __typename?: any } | null): obj is PublicWorkspaceDataSummary => {
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicWorkspaceDataSummary"')
return PublicWorkspaceDataSummary_possibleTypes.includes(obj.__typename)
}
const NativeModelCapabilities_possibleTypes: string[] = ['NativeModelCapabilities']
export const isNativeModelCapabilities = (obj?: { __typename?: any } | null): obj is NativeModelCapabilities => {
if (!obj?.__typename) throw new Error('__typename is missing in "isNativeModelCapabilities"')
File diff suppressed because it is too large Load Diff
@@ -2,8 +2,3 @@ export const backendGraphQLUrl = new URL(
'/graphql',
process.env.BACKEND_BASE_URL,
).toString();
export const metadataGraphQLUrl = new URL(
'/metadata',
process.env.BACKEND_BASE_URL,
).toString();
@@ -1,51 +0,0 @@
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { backendGraphQLUrl } from './backend';
export const createCompany = async ({
page,
companyName,
}: {
page: Page;
companyName: string;
}) => {
const { authToken } = await getAccessAuthToken(page);
const response = await page.request.post(backendGraphQLUrl, {
headers: {
Authorization: `Bearer ${authToken}`,
},
data: {
operationName: 'CreateOneCompany',
query:
'mutation CreateOneCompany($input: CompanyCreateInput!) { createCompany(data: $input) { __typename id } }',
variables: {
input: {
name: companyName,
},
},
},
});
if (!response.ok()) {
throw new Error(
`createCompany request failed with status ${response.status()}`,
);
}
const body = await response.json();
if (body.errors?.length) {
throw new Error(
`createCompany GraphQL errors: ${body.errors.map((e: { message: string }) => e.message).join(', ')}`,
);
}
const id: string | undefined = body.data?.createCompany?.id;
if (!id) {
throw new Error('createCompany returned no id');
}
return id;
};
@@ -1,25 +0,0 @@
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { backendGraphQLUrl } from './backend';
export const deleteCompany = async ({
page,
companyId,
}: {
page: Page;
companyId: string;
}) => {
const { authToken } = await getAccessAuthToken(page);
return page.request.post(backendGraphQLUrl, {
headers: {
Authorization: `Bearer ${authToken}`,
},
data: {
operationName: 'DeleteOneCompany',
variables: { idToDelete: companyId },
query:
'mutation DeleteOneCompany($idToDelete: UUID!) {\n deleteCompany(id: $idToDelete) {\n __typename\n deletedAt\n id\n }\n}',
},
});
};
@@ -1,25 +0,0 @@
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { backendGraphQLUrl } from './backend';
export const destroyCompany = async ({
page,
companyId,
}: {
page: Page;
companyId: string;
}) => {
const { authToken } = await getAccessAuthToken(page);
return page.request.post(backendGraphQLUrl, {
headers: {
Authorization: `Bearer ${authToken}`,
},
data: {
operationName: 'DestroyOneCompany',
variables: { idToDestroy: companyId },
query:
'mutation DestroyOneCompany($idToDestroy: UUID!) {\n destroyCompany(id: $idToDestroy) {\n id\n __typename\n }\n}',
},
});
};
@@ -1,72 +0,0 @@
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { metadataGraphQLUrl } from './backend';
export const resetRecordPageLayout = async ({
page,
objectNameSingular,
}: {
page: Page;
objectNameSingular: string;
}) => {
const { authToken } = await getAccessAuthToken(page);
const headers = {
Authorization: `Bearer ${authToken}`,
};
// Step 1: Find the objectMetadataId for the given object name
const objectsResponse = await page.request.post(metadataGraphQLUrl, {
headers,
data: {
operationName: 'ObjectMetadataItems',
query:
'query ObjectMetadataItems { objects(paging: { first: 1000 }) { edges { node { id nameSingular } } } }',
},
});
const objectsBody = await objectsResponse.json();
const objectMetadataId = objectsBody.data.objects.edges.find(
(edge: { node: { nameSingular: string } }) =>
edge.node.nameSingular === objectNameSingular,
)?.node.id as string | undefined;
if (!objectMetadataId) {
throw new Error(
`Object metadata not found for nameSingular: ${objectNameSingular}`,
);
}
// Step 2: Find all record page layouts for this object
const layoutsResponse = await page.request.post(metadataGraphQLUrl, {
headers,
data: {
operationName: 'GetPageLayouts',
query:
'query GetPageLayouts($objectMetadataId: String, $pageLayoutType: PageLayoutType) { getPageLayouts(objectMetadataId: $objectMetadataId, pageLayoutType: $pageLayoutType) { id } }',
variables: {
objectMetadataId,
pageLayoutType: 'RECORD_PAGE',
},
},
});
const layoutsBody = await layoutsResponse.json();
const layouts = layoutsBody.data.getPageLayouts as { id: string }[];
// Step 3: Reset each layout to default
for (const layout of layouts) {
await page.request.post(metadataGraphQLUrl, {
headers,
data: {
operationName: 'ResetPageLayoutToDefault',
query:
'mutation ResetPageLayoutToDefault($id: String!) { resetPageLayoutToDefault(id: $id) { id } }',
variables: {
id: layout.id,
},
},
});
}
};
@@ -1,16 +0,0 @@
import { type Page } from '@playwright/test';
export const enterEditMode = async (page: Page) => {
await page.getByTestId('page-header-side-panel-button').click();
// Type to search for Edit Layout command
const searchInput = page.getByTestId('side-panel-focus');
await searchInput.fill('Edit Layout');
// Click the Edit Layout command
const editLayoutItem = page.getByText('Edit Layout', { exact: true });
await editLayoutItem.click();
// Wait for the layout customization bar to appear
await page.getByText('Layout customization').waitFor({ state: 'visible' });
};
@@ -64,7 +64,7 @@ test('Create and update record', async ({ page }) => {
await lastNameInput.press('Enter');
// Focus on recordFieldList
const recordFieldList = page.getByTestId('record-fields-widget');
const recordFieldList = page.getByTestId('person-widget-fields');
await expect(recordFieldList).toBeVisible();
await recordFieldList.getByText('Emails').first().click();
@@ -1,305 +0,0 @@
import { expect, test } from '../lib/fixtures/screenshot';
import { createCompany } from '../lib/requests/create-company';
import { deleteCompany } from '../lib/requests/delete-company';
import { destroyCompany } from '../lib/requests/destroy-company';
import { resetRecordPageLayout } from '../lib/requests/reset-record-page-layout';
import { enterEditMode } from '../lib/utils/enterEditMode';
const DEFAULT_TAB_NAMES = [
'Home',
'Timeline',
'Tasks',
'Notes',
'Files',
'Emails',
'Calendar',
];
test.describe('Record page layout', () => {
let companyId: string | undefined;
test.beforeEach(async ({ page }) => {
companyId = undefined;
await resetRecordPageLayout({ page, objectNameSingular: 'company' });
companyId = await createCompany({
page,
companyName: 'Layout E2E Test Company',
});
await page.goto(`/object/company/${companyId}`);
});
test.afterEach(async ({ page }) => {
if (!companyId) return;
await deleteCompany({ page, companyId });
await destroyCompany({ page, companyId });
});
test('Full layout workflow: view, edit tabs, widgets, save, cancel, reset', async ({
page,
}) => {
const sidePanel = page.locator('[data-side-panel]');
const tabsContainer = page.getByTestId('page-layout-tab-list');
// ===== PHASE 1: Default layout renders correctly =====
for (const tabName of DEFAULT_TAB_NAMES.slice(1)) {
const tab = tabsContainer.getByRole('link', {
name: tabName,
exact: true,
});
await expect(tab.last()).toBeVisible();
}
// ===== PHASE 2: Tab switching works in view mode =====
await tabsContainer
.getByRole('link', { name: 'Emails', exact: true })
.click();
await tabsContainer
.getByRole('link', { name: 'Tasks', exact: true })
.click();
await tabsContainer
.getByRole('link', { name: 'Timeline', exact: true })
.click();
// ===== PHASE 3: Enter edit mode =====
await enterEditMode(page);
// ===== PHASE 4: Create a new tab =====
const defaultMoreTabsCount = 5;
await expect(
page.getByText(`+${defaultMoreTabsCount} More`, { exact: true }),
).toBeVisible();
const newTabButton = tabsContainer.getByRole('button', { name: 'New Tab' });
await newTabButton.click();
const emptyTabInput = sidePanel.getByRole('textbox', { name: 'Tab' });
await expect(emptyTabInput).toHaveValue('Untitled');
await page.keyboard.press('Escape');
await expect(
page.getByText(`+${defaultMoreTabsCount + 1} More`, { exact: true }),
).toBeVisible();
// ===== PHASE 5: Delete the newly created tab =====
const deleteButton = sidePanel.getByText('Delete', { exact: true });
await deleteButton.click();
await expect(
page.getByText(`+${defaultMoreTabsCount} More`, { exact: true }),
).toBeVisible();
// ===== PHASE 6: Rename a tab =====
const timelineTab = tabsContainer.getByText('Timeline', { exact: true });
// Focus
await timelineTab.click();
// Edit
await timelineTab.click();
const titleLabel = sidePanel.getByText('Timeline');
await titleLabel.click();
const titleInput = sidePanel.getByRole('textbox');
await titleInput.clear();
await titleInput.fill('My Custom Timeline');
await page.keyboard.press('Enter');
const renamedTab = tabsContainer.getByText('My Custom Timeline', {
exact: true,
});
await expect(renamedTab).toBeVisible();
// ===== PHASE 7: Create a custom tab for duplication =====
await newTabButton.click();
const customTabInput = sidePanel.getByRole('textbox', { name: 'Tab' });
await customTabInput.clear();
await customTabInput.fill('Custom Tab');
await page.keyboard.press('Enter');
const moreButtonAfterCustomTabCreation = page.getByText(
`+${defaultMoreTabsCount + 1} More`,
{ exact: true },
);
await moreButtonAfterCustomTabCreation.click();
const customTab = page
.getByRole('listbox')
.getByRole('button', { name: 'Custom Tab', exact: true });
await expect(customTab).toBeVisible();
await page.keyboard.press('Escape');
// ===== PHASE 8: Duplicate the custom tab =====
const duplicateButton = sidePanel.getByText('Duplicate', { exact: true });
await duplicateButton.click();
const duplicatedTabInput = sidePanel.getByRole('textbox', { name: 'Tab' });
await expect(duplicatedTabInput).toHaveValue('Custom Tab (Copy)');
await page.keyboard.press('Escape');
const moreButtonAfterTabDuplication = page.getByText(
`+${defaultMoreTabsCount + 2} More`,
{ exact: true },
);
await expect(moreButtonAfterTabDuplication).toBeVisible();
// ===== PHASE 9: Reorder a tab (move left) =====
await moreButtonAfterTabDuplication.click();
const tasksTab = page.getByRole('listbox').getByRole('button', {
name: 'Tasks',
exact: true,
});
await tasksTab.click();
const moveLeftButton = sidePanel.getByText('Move left', { exact: true });
await moveLeftButton.click();
await expect(
tabsContainer.getByRole('button', { name: 'Tasks', exact: true }),
).toBeVisible();
await moreButtonAfterTabDuplication.click();
await expect(
page.getByRole('listbox').getByRole('button', {
name: 'My Custom Timeline',
exact: true,
}),
).toBeVisible();
// ===== PHASE 10: Cancel discards all changes =====
await page.getByRole('button', { name: 'Cancel' }).click();
// After canceling, all changes should be reverted
await expect(
tabsContainer.getByText('My Custom Timeline', { exact: true }),
).not.toBeVisible();
await expect(
tabsContainer.getByText('Custom Tab (Copy)', { exact: true }),
).not.toBeVisible();
await expect(
tabsContainer.getByText('Custom Tab', { exact: true }),
).not.toBeVisible();
await expect(
tabsContainer.getByText('Timeline', { exact: true }),
).toBeVisible();
// ===== PHASE 11: Create a tab, save, and verify persistence =====
await enterEditMode(page);
await newTabButton.click();
const newTabInput = sidePanel.getByRole('textbox', { name: 'Tab' });
await expect(newTabInput).toHaveValue('Untitled');
await newTabInput.clear();
await newTabInput.fill('Persisted Tab');
await page.keyboard.press('Enter');
// Add a widget so the tab is not filtered out as empty
const persistedTabFieldsGroup = page
.getByText('Fields group', {
exact: true,
})
.nth(1);
await persistedTabFieldsGroup.click();
await expect(sidePanel.getByText('Data and display')).toBeVisible();
const saveButton = page.getByRole('button', { name: 'Save' });
await saveButton.click();
await expect(saveButton).not.toBeVisible();
// Reload the page to verify persistence
await page.goto(`/object/company/${companyId}`);
const persistedTab = tabsContainer.getByText('Persisted Tab', {
exact: true,
});
await expect(persistedTab).toBeVisible();
// ===== PHASE 12: Clean up - delete the persisted tab =====
await enterEditMode(page);
const moreButtonAfterPersistedTabCreation = page.getByText(
`+${defaultMoreTabsCount + 1} More`,
{ exact: true },
);
await moreButtonAfterPersistedTabCreation.click();
const persistedTabInListBox = page
.getByRole('listbox')
.getByText('Persisted Tab', { exact: true });
await persistedTabInListBox.hover();
await page.getByTestId('tab-list-item-edit-button').click();
const deletePersistedButton = sidePanel.getByText('Delete', {
exact: true,
});
await deletePersistedButton.click();
await page.getByRole('button', { name: 'Save' }).click();
await expect(
tabsContainer.getByText('Persisted Tab', { exact: true }),
).not.toBeVisible();
await expect(
tabsContainer.getByText('Timeline', { exact: true }),
).toBeVisible();
// ===== PHASE 13: Widget management =====
await enterEditMode(page);
// Add a fields group widget on Home tab
const fieldsGroupOption = page.getByText('Field', { exact: true });
await fieldsGroupOption.click();
const createdFieldWidgetTitle = page.getByRole('button', {
name: 'Account Owner',
exact: true,
});
await expect(createdFieldWidgetTitle).toBeVisible();
await createdFieldWidgetTitle.click();
// Delete the widget
const deleteWidgetButton = sidePanel.getByText('Delete widget');
await deleteWidgetButton.click();
await expect(createdFieldWidgetTitle).not.toBeVisible();
// Cancel to discard widget changes
await page.getByRole('button', { name: 'Cancel' }).click();
});
});
@@ -1,5 +1,4 @@
node_modules
storybook-static
src/__stories__/example-sources-built/*
!src/__stories__/example-sources-built/bundle-sizes.json.d.ts
src/__stories__/example-sources-built
src/__stories__/example-sources-built-preact
@@ -60,7 +60,7 @@ const twentySharedAliases = Object.fromEntries(
const storyAlias = {
react: path.join(rootNodeModules, 'react'),
'react-dom': path.join(rootNodeModules, 'react-dom'),
'twenty-sdk': sdkIndividualIndex,
'@/sdk': sdkIndividualIndex,
'twenty-sdk/ui': twentyUiIndividualIndex,
...twentySharedAliases,
};
@@ -77,9 +77,6 @@ const STORY_COMPONENTS = [
'mui-example.front-component',
'twenty-ui-example.front-component',
'sdk-context-example.front-component',
'form-events.front-component',
'keyboard-events.front-component',
'host-api-calls.front-component',
];
const resolveEntryPoints = (): Record<string, string> => {
@@ -1,348 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
const errorHandler = fn();
const createHostApiMocks = () => ({
navigate: fn().mockResolvedValue(undefined),
enqueueSnackbar: fn().mockResolvedValue(undefined),
openSidePanelPage: fn().mockResolvedValue(undefined),
closeSidePanel: fn().mockResolvedValue(undefined),
unmountFrontComponent: fn().mockResolvedValue(undefined),
updateProgress: fn().mockResolvedValue(undefined),
requestAccessTokenRefresh: fn().mockResolvedValue('refreshed-token'),
openCommandConfirmationModal: fn().mockResolvedValue(undefined),
});
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/EventForwarding',
component: FrontComponentRenderer,
parameters: {
layout: 'centered',
},
args: {
onError: errorHandler,
applicationAccessToken: 'fake-token',
executionContext: {
frontComponentId: 'storybook-test',
userId: null,
recordId: null,
},
colorScheme: 'light',
frontComponentHostCommunicationApi: createHostApiMocks(),
},
beforeEach: () => {
errorHandler.mockClear();
},
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
const MOUNT_TIMEOUT = 30000;
const INTERACTION_TIMEOUT = 5000;
const HOST_API_TIMEOUT = 10000;
const createComponentStory = (
name: string,
options?: { play?: Story['play'] },
): Story => ({
args: {
componentUrl: getBuiltStoryComponentPathForRender(
`${name}.front-component`,
),
},
...(options?.play ? { play: options.play } : {}),
});
const createHostApiStory = (play: Story['play']): Story => ({
...createComponentStory('host-api-calls'),
args: {
...createComponentStory('host-api-calls').args,
frontComponentHostCommunicationApi: createHostApiMocks(),
},
play,
});
export const FormTextInput: Story = createComponentStory('form-events', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'form-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const textInput = await canvas.findByTestId('text-input');
await userEvent.type(textInput, 'hello');
expect(
await canvas.findByText('hello', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
},
});
export const FormCheckbox: Story = createComponentStory('form-events', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'form-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const checkbox = await canvas.findByTestId('checkbox-input');
await userEvent.click(checkbox);
expect(
await canvas.findByText('true', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
},
});
export const FormFocusAndBlur: Story = createComponentStory('form-events', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'form-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const textInput = await canvas.findByTestId('text-input');
await userEvent.click(textInput);
expect(
await canvas.findByText('focused', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
await userEvent.click(await canvas.findByTestId('form-events-component'));
expect(
await canvas.findByText('blurred', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
},
});
export const FormSubmission: Story = createComponentStory('form-events', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'form-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const textInput = await canvas.findByTestId('text-input');
await userEvent.type(textInput, 'hello');
const checkbox = await canvas.findByTestId('checkbox-input');
await userEvent.click(checkbox);
const submitButton = await canvas.findByTestId('submit-button');
await userEvent.click(submitButton);
expect(
await canvas.findByText(
'{"text":"hello","checkbox":true}',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});
export const KeyboardBasicInput: Story = createComponentStory(
'keyboard-events',
{
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'keyboard-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const input = await canvas.findByTestId('keyboard-input');
await userEvent.click(input);
await userEvent.keyboard('a');
expect(
await canvas.findByText('a', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
expect(
await canvas.findByText('KeyA', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
expect(
await canvas.findByText(
/^[1-9]\d*$/,
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
},
);
export const KeyboardModifiers: Story = createComponentStory(
'keyboard-events',
{
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'keyboard-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const input = await canvas.findByTestId('keyboard-input');
await userEvent.click(input);
await userEvent.keyboard('{Shift>}b{/Shift}');
expect(
await canvas.findByText('shift', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
},
},
);
export const HostApiNavigate: Story = createHostApiStory(
async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi!;
await canvas.findByTestId(
'host-api-calls-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const navigateBtn = await canvas.findByTestId('btn-navigate');
await userEvent.click(navigateBtn);
await waitFor(
() => {
expect(api.navigate).toHaveBeenCalled();
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'navigate:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
);
export const HostApiSnackbar: Story = createHostApiStory(
async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi!;
await canvas.findByTestId(
'host-api-calls-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const snackbarBtn = await canvas.findByTestId('btn-snackbar');
await userEvent.click(snackbarBtn);
await waitFor(
() => {
expect(api.enqueueSnackbar).toHaveBeenCalledWith({
message: 'Test notification',
variant: 'success',
});
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'snackbar:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
);
export const HostApiProgress: Story = createHostApiStory(
async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi!;
await canvas.findByTestId(
'host-api-calls-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const progressBtn = await canvas.findByTestId('btn-progress');
await userEvent.click(progressBtn);
await waitFor(
() => {
expect(api.updateProgress).toHaveBeenCalledWith(50);
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'progress:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
);
export const HostApiClosePanel: Story = createHostApiStory(
async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi!;
await canvas.findByTestId(
'host-api-calls-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const closePanelBtn = await canvas.findByTestId('btn-close-panel');
await userEvent.click(closePanelBtn);
await waitFor(
() => {
expect(api.closeSidePanel).toHaveBeenCalled();
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'closePanel:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
);
@@ -16,11 +16,7 @@ const meta: Meta<typeof FrontComponentRenderer> = {
args: {
onError: errorHandler,
applicationAccessToken: 'fake-token',
executionContext: {
frontComponentId: 'storybook-test',
userId: null,
recordId: null,
},
executionContext: { frontComponentId: 'storybook-test', userId: null },
},
beforeEach: () => {
errorHandler.mockClear();
@@ -96,9 +92,13 @@ export const Lifecycle: Story = {
expect(await canvas.findByText('Mounted')).toBeVisible();
expect(
await canvas.findByText(/Ticks: [1-9]\d*/, {}, { timeout: 10000 }),
).toBeVisible();
await waitFor(
() => {
const tickElement = canvas.getByTestId('tick-count');
expect(tickElement.textContent).toMatch(/Ticks: [1-9]\d*/);
},
{ timeout: 10000 },
);
},
};
@@ -121,7 +121,6 @@ export const SdkContext: Story = {
executionContext: {
frontComponentId: 'sdk-context-test',
userId: 'test-user-abc-123',
recordId: null,
},
},
play: async ({ canvasElement }) => {
@@ -16,11 +16,7 @@ const meta: Meta<typeof FrontComponentRenderer> = {
args: {
onError: errorHandler,
applicationAccessToken: 'fake-token',
executionContext: {
frontComponentId: 'storybook-test',
userId: null,
recordId: null,
},
executionContext: { frontComponentId: 'storybook-test', userId: null },
},
beforeEach: () => {
errorHandler.mockClear();
@@ -1,7 +0,0 @@
declare const bundleSizes: {
name: string;
reactBytes: number;
preactBytes: number;
}[];
export default bundleSizes;
@@ -1,6 +1,6 @@
import { defineFrontComponent } from 'twenty-sdk';
import styled from '@emotion/styled';
import { useState } from 'react';
import { defineFrontComponent } from '@/sdk';
const Card = styled.div`
padding: 24px;
@@ -1,141 +0,0 @@
import { defineFrontComponent } from 'twenty-sdk';
import { type ChangeEvent, useState } from 'react';
const CARD_STYLE = {
padding: 24,
backgroundColor: '#f0fdf4',
border: '2px solid #22c55e',
borderRadius: 12,
fontFamily: 'system-ui, sans-serif',
display: 'flex',
flexDirection: 'column' as const,
gap: 16,
maxWidth: 400,
};
const HEADING_STYLE = {
color: '#166534',
fontWeight: 700,
fontSize: 18,
margin: 0,
};
const LABEL_STYLE = {
fontSize: 13,
fontWeight: 600,
color: '#374151',
};
const HINT_STYLE = {
fontSize: 13,
color: '#6b7280',
};
const INPUT_STYLE = {
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: 6,
fontSize: 14,
};
const SUBMIT_BUTTON_STYLE = {
padding: '10px 20px',
backgroundColor: '#16a34a',
color: 'white',
border: 'none',
borderRadius: 6,
fontWeight: 600,
cursor: 'pointer',
};
const FormEventsComponent = () => {
const [textValue, setTextValue] = useState('');
const [checkboxValue, setCheckboxValue] = useState(false);
const [focusState, setFocusState] = useState('none');
const [submittedData, setSubmittedData] = useState<string | null>(null);
return (
<div data-testid="form-events-component" style={CARD_STYLE}>
<h2 style={HEADING_STYLE}>Form Events</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<label style={LABEL_STYLE}>Text Input</label>
<input
data-testid="text-input"
type="text"
placeholder="Type here..."
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const detail = (event as unknown as { detail: { value?: string } })
.detail;
setTextValue(detail?.value ?? '');
}}
onFocus={() => setFocusState('focused')}
onBlur={() => setFocusState('blurred')}
style={INPUT_STYLE}
/>
<span data-testid="text-value" style={HINT_STYLE}>
{textValue}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
data-testid="checkbox-input"
type="checkbox"
checked={checkboxValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const detail = (
event as unknown as { detail: { checked?: boolean } }
).detail;
setCheckboxValue(detail?.checked ?? false);
}}
/>
<label style={LABEL_STYLE}>Check me</label>
<span data-testid="checkbox-value" style={HINT_STYLE}>
{String(checkboxValue)}
</span>
</div>
<span data-testid="focus-state" style={HINT_STYLE}>
{focusState}
</span>
<button
data-testid="submit-button"
type="button"
onClick={() =>
setSubmittedData(
JSON.stringify({ text: textValue, checkbox: checkboxValue }),
)
}
style={SUBMIT_BUTTON_STYLE}
>
Submit
</button>
{submittedData !== null && (
<pre
data-testid="submitted-data"
style={{
fontSize: 13,
background: '#dcfce7',
padding: 12,
borderRadius: 8,
margin: 0,
overflow: 'auto',
}}
>
{submittedData}
</pre>
)}
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-form-00000000-0000-0000-0000-000000000020',
name: 'form-events-component',
description:
'Component testing form input events (onChange, onFocus, onBlur, submit)',
component: FormEventsComponent,
});
@@ -1,155 +0,0 @@
import {
AppPath,
closeSidePanel,
defineFrontComponent,
enqueueSnackbar,
navigate,
openSidePanelPage,
SidePanelPages,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk';
import { useState } from 'react';
const CARD_STYLE = {
padding: 24,
backgroundColor: '#faf5ff',
border: '2px solid #a78bfa',
borderRadius: 12,
fontFamily: 'system-ui, sans-serif',
display: 'flex',
flexDirection: 'column' as const,
gap: 10,
maxWidth: 400,
};
const HEADING_STYLE = {
color: '#5b21b6',
fontWeight: 700,
fontSize: 18,
margin: 0,
};
const BUTTON_STYLE = {
padding: '8px 16px',
backgroundColor: '#7c3aed',
color: 'white',
border: 'none',
borderRadius: 6,
fontWeight: 600,
cursor: 'pointer',
fontSize: 13,
};
const STATUS_STYLE = {
fontSize: 13,
color: '#6b7280',
fontFamily: 'monospace',
};
const HostApiCallsComponent = () => {
const [apiStatus, setApiStatus] = useState('idle');
const callApi = async (name: string, apiFunction: () => Promise<void>) => {
try {
await apiFunction();
setApiStatus(`${name}:success`);
} catch (error) {
setApiStatus(
`${name}:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<div data-testid="host-api-calls-component" style={CARD_STYLE}>
<h2 style={HEADING_STYLE}>Host API Calls</h2>
<button
data-testid="btn-navigate"
type="button"
onClick={() =>
callApi('navigate', () =>
navigate(AppPath.RecordIndexPage, {
objectNamePlural: 'companies',
}),
)
}
style={BUTTON_STYLE}
>
Navigate
</button>
<button
data-testid="btn-snackbar"
type="button"
onClick={() =>
callApi('snackbar', () =>
enqueueSnackbar({
message: 'Test notification',
variant: 'success',
}),
)
}
style={BUTTON_STYLE}
>
Snackbar
</button>
<button
data-testid="btn-side-panel"
type="button"
onClick={() =>
callApi('sidePanel', () =>
openSidePanelPage({
page: SidePanelPages.ViewRecord,
pageTitle: 'Test Record',
}),
)
}
style={BUTTON_STYLE}
>
Open Side Panel
</button>
<button
data-testid="btn-close-panel"
type="button"
onClick={() => callApi('closePanel', () => closeSidePanel())}
style={BUTTON_STYLE}
>
Close Side Panel
</button>
<button
data-testid="btn-unmount"
type="button"
onClick={() => callApi('unmount', () => unmountFrontComponent())}
style={BUTTON_STYLE}
>
Unmount
</button>
<button
data-testid="btn-progress"
type="button"
onClick={() => callApi('progress', () => updateProgress(50))}
style={BUTTON_STYLE}
>
Update Progress (50)
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{apiStatus}
</span>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-hapi-00000000-0000-0000-0000-000000000022',
name: 'host-api-calls-component',
description:
'Component testing host communication API calls (navigate, snackbar, side panel, etc.)',
component: HostApiCallsComponent,
});
@@ -1,4 +1,4 @@
import { defineFrontComponent } from 'twenty-sdk';
import { defineFrontComponent } from '@/sdk';
import { useState } from 'react';
const InteractiveComponent = () => {
@@ -1,125 +0,0 @@
import { defineFrontComponent } from 'twenty-sdk';
import { type KeyboardEvent, useState } from 'react';
type RemoteKeyboardEventDetail = {
key?: string;
code?: string;
shiftKey?: boolean;
ctrlKey?: boolean;
metaKey?: boolean;
altKey?: boolean;
};
const KeyboardEventsComponent = () => {
const [lastKey, setLastKey] = useState('');
const [lastCode, setLastCode] = useState('');
const [modifiers, setModifiers] = useState('');
const [keyCount, setKeyCount] = useState(0);
// remote-dom serializes keyboard events into CustomEvent.detail
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
const data = (event as unknown as { detail: RemoteKeyboardEventDetail })
.detail;
setLastKey(data.key ?? '');
setLastCode(data.code ?? '');
setKeyCount((previousCount) => previousCount + 1);
const activeModifiers: string[] = [];
if (data.shiftKey) activeModifiers.push('shift');
if (data.ctrlKey) activeModifiers.push('ctrl');
if (data.metaKey) activeModifiers.push('meta');
if (data.altKey) activeModifiers.push('alt');
setModifiers(activeModifiers.join(','));
};
return (
<div
data-testid="keyboard-events-component"
style={{
padding: 24,
backgroundColor: '#fefce8',
border: '2px solid #eab308',
borderRadius: 12,
fontFamily: 'system-ui, sans-serif',
display: 'flex',
flexDirection: 'column',
gap: 12,
maxWidth: 400,
}}
>
<h2
style={{
color: '#854d0e',
fontWeight: 700,
fontSize: 18,
margin: 0,
}}
>
Keyboard Events
</h2>
<input
data-testid="keyboard-input"
type="text"
placeholder="Press keys here..."
onKeyDown={handleKeyDown}
style={{
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: 6,
fontSize: 14,
}}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{ display: 'flex', gap: 16 }}>
<span style={{ fontSize: 13, color: '#6b7280' }}>
Key:{' '}
<span
data-testid="last-key"
style={{ fontWeight: 700, color: '#854d0e' }}
>
{lastKey}
</span>
</span>
<span style={{ fontSize: 13, color: '#6b7280' }}>
Code:{' '}
<span
data-testid="last-code"
style={{ fontWeight: 700, color: '#854d0e' }}
>
{lastCode}
</span>
</span>
</div>
<span style={{ fontSize: 13, color: '#6b7280' }}>
Modifiers:{' '}
<span
data-testid="modifiers"
style={{ fontWeight: 700, color: '#854d0e' }}
>
{modifiers}
</span>
</span>
<span style={{ fontSize: 13, color: '#6b7280' }}>
Key count:{' '}
<span
data-testid="key-count"
style={{ fontWeight: 700, color: '#854d0e' }}
>
{keyCount}
</span>
</span>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-kbd0-00000000-0000-0000-0000-000000000021',
name: 'keyboard-events-component',
description:
'Component testing keyboard event serialization (key, code, modifiers)',
component: KeyboardEventsComponent,
});
@@ -1,4 +1,4 @@
import { defineFrontComponent } from 'twenty-sdk';
import { defineFrontComponent } from '@/sdk';
import { useEffect, useState } from 'react';
const LifecycleComponent = () => {
@@ -1,7 +1,7 @@
import { defineFrontComponent } from 'twenty-sdk';
import Button from '@mui/material/Button';
import MuiChip from '@mui/material/Chip';
import { useState } from 'react';
import { defineFrontComponent } from '@/sdk';
const MuiComponent = () => {
const [count, setCount] = useState(0);
@@ -1,9 +1,9 @@
import { useState } from 'react';
import {
defineFrontComponent,
useFrontComponentExecutionContext,
useUserId,
} from 'twenty-sdk';
import { useState } from 'react';
} from '@/sdk';
const CARD_STYLE = {
padding: 24,
@@ -72,7 +72,7 @@ const SdkContextComponent = () => {
overflow: 'auto',
}}
>
{JSON.stringify(fullContext, null, 2)}
{JSON.stringify(fullContext, null, 2) ?? 'undefined'}
</pre>
</div>
@@ -1,5 +1,5 @@
import { defineFrontComponent } from 'twenty-sdk';
import { useState } from 'react';
import { defineFrontComponent } from '@/sdk';
const SHADCN_CSS = `
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
@@ -1,4 +1,4 @@
import { defineFrontComponent } from 'twenty-sdk';
import { defineFrontComponent } from '@/sdk';
const StaticComponent = () => (
<div
@@ -1,6 +1,6 @@
import { defineFrontComponent } from 'twenty-sdk';
import { useState } from 'react';
import styled from 'styled-components';
import { defineFrontComponent } from '@/sdk';
const Card = styled.div`
padding: 24px;
@@ -1,4 +1,4 @@
import { defineFrontComponent } from 'twenty-sdk';
import { defineFrontComponent } from '@/sdk';
import { useState } from 'react';
const TAILWIND_CSS = `
@@ -1,5 +1,5 @@
import { defineFrontComponent } from 'twenty-sdk';
import { useState } from 'react';
import { defineFrontComponent } from '@/sdk';
import {
Button,
Chip,
@@ -31,6 +31,7 @@
"src/remote/mock/**/*",
"src/host/generated/host-component-registry.ts",
"src/remote/generated/remote-components.ts",
"src/remote/generated/remote-elements.ts"
"src/remote/generated/remote-elements.ts",
"src/__stories__/**/*"
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -2,31 +2,45 @@ import { styled } from '@linaria/react';
import { Trans } from '@lingui/react/macro';
import { useContext } from 'react';
import { IconLock } from 'twenty-ui/display';
import { Card, CardContent } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
align-items: center;
background: ${themeCssVariables.background.transparent.lighter};
border: 1px solid ${themeCssVariables.border.color.light};
border-radius: ${themeCssVariables.border.radius.sm};
box-sizing: border-box;
const StyledVisibilityCardContainer = styled.div`
color: ${themeCssVariables.font.color.light};
flex: 1;
transition: color ${themeCssVariables.animation.duration.normal} ease;
width: 100%;
> * {
border-color: ${themeCssVariables.border.color.light};
}
`;
const StyledVisibilityCardContentContainer = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.transparent.lighter};
box-sizing: border-box;
display: flex;
font-size: ${themeCssVariables.font.size.sm};
font-weight: ${themeCssVariables.font.weight.medium};
gap: ${themeCssVariables.spacing[1]};
height: ${themeCssVariables.spacing[6]};
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[1]};
width: 100%;
`;
export const CalendarEventNotSharedContent = () => {
const { theme } = useContext(ThemeContext);
return (
<StyledContainer>
<IconLock size={theme.icon.size.sm} />
<Trans>Not shared</Trans>
</StyledContainer>
<StyledVisibilityCardContainer>
<Card>
<StyledVisibilityCardContentContainer>
<CardContent>
<IconLock size={theme.icon.size.sm} />
<Trans>Not shared</Trans>
</CardContent>
</StyledVisibilityCardContentContainer>
</Card>
</StyledVisibilityCardContainer>
);
};
@@ -42,7 +42,6 @@ export const AIChatTab = () => {
<StyledContainer
isDraggingFile={isDraggingFile}
onDragEnter={() => setIsDraggingFile(true)}
onDragLeave={() => setIsDraggingFile(false)}
>
{isDraggingFile && (
<DropZone
@@ -204,14 +204,6 @@ const SettingsApplicationRegistrationDetails = lazy(() =>
})),
);
const SettingsApplicationRegistrationConfigVariableDetail = lazy(() =>
import(
'~/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableDetail'
).then((module) => ({
default: module.SettingsApplicationRegistrationConfigVariableDetail,
})),
);
const SettingsAgentForm = lazy(() =>
import('~/pages/settings/ai/SettingsAgentForm').then((module) => ({
default: module.SettingsAgentForm,
@@ -752,10 +744,6 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
path={SettingsPath.ApplicationLogicFunctionDetail}
element={<SettingsLogicFunctionDetail />}
/>
<Route
path={SettingsPath.ApplicationRegistrationConfigVariableDetails}
element={<SettingsApplicationRegistrationConfigVariableDetail />}
/>
</Route>
<Route

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