diff --git a/package.json b/package.json index aa05f761c0f0..0b146a6bb6a8 100644 --- a/package.json +++ b/package.json @@ -320,7 +320,7 @@ "msw": "^2.0.11", "msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0", "nx": "18.3.3", - "playwright": "^1.40.1", + "playwright": "^1.46.0", "prettier": "^3.1.1", "raw-loader": "^4.0.2", "rimraf": "^5.0.5", @@ -369,6 +369,7 @@ "packages/twenty-utils", "packages/twenty-zapier", "packages/twenty-website", + "packages/twenty-e2e-testing", "tools/eslint-rules" ] } diff --git a/packages/twenty-e2e-testing/.env.example b/packages/twenty-e2e-testing/.env.example new file mode 100644 index 000000000000..9ff92d0193a7 --- /dev/null +++ b/packages/twenty-e2e-testing/.env.example @@ -0,0 +1,2 @@ +# Note that provide always without trailing forward slash to have expected behaviour +FRONTEND_BASE_URL="http://localhost:3001" diff --git a/packages/twenty-e2e-testing/.gitignore b/packages/twenty-e2e-testing/.gitignore new file mode 100644 index 000000000000..68c5d18f00dc --- /dev/null +++ b/packages/twenty-e2e-testing/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/packages/twenty-e2e-testing/README.md b/packages/twenty-e2e-testing/README.md new file mode 100644 index 000000000000..222f1d8070db --- /dev/null +++ b/packages/twenty-e2e-testing/README.md @@ -0,0 +1,37 @@ +# Twenty e2e Testing + +## Install + +Don't forget to install the browsers before launching the tests : + +``` +yarn playwright install +``` + +### Run end-to-end tests + +``` +yarn run test:e2e +``` + +### Start the interactive UI mode + +``` +yarn run test:e2e:ui +``` + +### Run test only on Desktop Chrome + +``` +yarn run test:e2e:chrome +``` + +### Run test in specific file +``` +yarn run test:e2e +``` + +### Runs the tests in debug mode. +``` +yarn run test:e2e:debug +``` diff --git a/packages/twenty-e2e-testing/e2e/companies.spec.ts b/packages/twenty-e2e-testing/e2e/companies.spec.ts new file mode 100644 index 000000000000..48485da04df6 --- /dev/null +++ b/packages/twenty-e2e-testing/e2e/companies.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from '@playwright/test'; + +test.describe('visible table', () => { + test('table should be visible on navigation to /objects/companies', async ({ + page, + }) => { + // Navigate to the page + await page.goto('/objects/companies'); + + // Check if the table is visible + const table = page.locator('table'); + await expect(table).toBeVisible(); + }); +}); diff --git a/packages/twenty-e2e-testing/package.json b/packages/twenty-e2e-testing/package.json new file mode 100644 index 000000000000..6cef81f80c74 --- /dev/null +++ b/packages/twenty-e2e-testing/package.json @@ -0,0 +1,14 @@ +{ + "name": "twenty-e2e-testing", + "devDependencies": { + "@playwright/test": "^1.46.0" + }, + "scripts": { + "test:e2e:setup": "yarn playwright install", + "test:e2e": "yarn playwright test", + "test:e2e:ui": "yarn playwright test --ui", + "test:e2e:chrome": "yarn playwright test --project=chromium", + "test:e2e:debug": "yarn playwright test --debug", + "test:e2e:report": "yarn playwright show-report" + } +} diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts new file mode 100644 index 000000000000..4b4f081de794 --- /dev/null +++ b/packages/twenty-e2e-testing/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from '@playwright/test'; + +import { config } from 'dotenv'; +config(); + +/** + * See https://playwright.dev/docs/test-configuration. + * See https://playwright.dev/docs/trace-viewer to Collect trace when retrying the failed test + */ +export default defineConfig({ + testDir: 'e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.FRONTEND_BASE_URL ?? 'http://localhost:3001', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + { + name: 'Google Chrome', + use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + }, + ], +}); diff --git a/packages/twenty-front/.storybook/main.ts b/packages/twenty-front/.storybook/main.ts index 633df49b3dae..3bd4d16b2a57 100644 --- a/packages/twenty-front/.storybook/main.ts +++ b/packages/twenty-front/.storybook/main.ts @@ -45,5 +45,16 @@ const config: StorybookConfig = { name: '@storybook/react-vite', options: {}, }, + viteFinal: async (config) => { + // Merge custom configuration into the default config + const { mergeConfig } = await import('vite'); + + return mergeConfig(config, { + // Add dependencies to pre-optimization + optimizeDeps: { + exclude: ['@tabler/icons-react'], + }, + }); + }, }; export default config; diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index a8eff68866fd..f3668ff0b2da 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -59,7 +59,7 @@ import { SettingsAccountsEmails } from '~/pages/settings/accounts/SettingsAccoun import { SettingsNewAccount } from '~/pages/settings/accounts/SettingsNewAccount'; import { SettingsCRMMigration } from '~/pages/settings/crm-migration/SettingsCRMMigration'; import { SettingsNewObject } from '~/pages/settings/data-model/SettingsNewObject'; -import { SettingsObjectDetail } from '~/pages/settings/data-model/SettingsObjectDetail'; +import { SettingsObjectDetailPage } from '~/pages/settings/data-model/SettingsObjectDetailPage'; import { SettingsObjectEdit } from '~/pages/settings/data-model/SettingsObjectEdit'; import { SettingsObjectFieldEdit } from '~/pages/settings/data-model/SettingsObjectFieldEdit'; import { SettingsObjectNewFieldStep1 } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1'; @@ -218,7 +218,7 @@ const createRouter = ( /> } + element={} /> ; }; @@ -639,11 +644,6 @@ export type MutationTrackArgs = { }; -export type MutationTriggerWorkflowArgs = { - workflowVersionId: Scalars['String']['input']; -}; - - export type MutationUnsyncRemoteTableArgs = { input: RemoteTableInput; }; @@ -1001,6 +1001,13 @@ export enum RemoteTableStatus { Synced = 'SYNCED' } +export type RunWorkflowVersionInput = { + /** Execution result in JSON format */ + payload?: InputMaybe; + /** Workflow version ID */ + workflowVersionId: Scalars['String']['input']; +}; + export type SendInviteLink = { __typename?: 'SendInviteLink'; /** Boolean that confirms query was dispatched */ @@ -1400,6 +1407,7 @@ export type WorkspaceFeatureFlagsArgs = { export enum WorkspaceActivationStatus { Active = 'ACTIVE', Inactive = 'INACTIVE', + OngoingCreation = 'ONGOING_CREATION', PendingCreation = 'PENDING_CREATION' } diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index ce20dd81f9d7..cb973a45cf52 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import * as Apollo from '@apollo/client'; import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -249,9 +249,9 @@ export type FieldConnection = { /** Type of the field */ export enum FieldMetadataType { + Actor = 'ACTOR', Address = 'ADDRESS', Boolean = 'BOOLEAN', - Actor = 'ACTOR', Currency = 'CURRENCY', Date = 'DATE', DateTime = 'DATE_TIME', @@ -344,11 +344,11 @@ export type Mutation = { generateTransientToken: TransientToken; impersonate: Verify; renewToken: AuthTokens; + runWorkflowVersion: WorkflowTriggerResult; sendInviteLink: SendInviteLink; signUp: LoginToken; skipSyncEmailOnboardingStep: OnboardingStepSuccess; track: Analytics; - triggerWorkflow: WorkflowTriggerResult; updateBillingSubscription: UpdateBillingEntity; updateOneObject: Object; updateOneServerlessFunction: ServerlessFunction; @@ -457,6 +457,11 @@ export type MutationRenewTokenArgs = { }; +export type MutationRunWorkflowVersionArgs = { + input: RunWorkflowVersionInput; +}; + + export type MutationSendInviteLinkArgs = { emails: Array; }; @@ -476,11 +481,6 @@ export type MutationTrackArgs = { }; -export type MutationTriggerWorkflowArgs = { - workflowVersionId: Scalars['String']; -}; - - export type MutationUpdateOneObjectArgs = { input: UpdateOneObjectInput; }; @@ -743,6 +743,13 @@ export enum RemoteTableStatus { Synced = 'SYNCED' } +export type RunWorkflowVersionInput = { + /** Execution result in JSON format */ + payload?: InputMaybe; + /** Workflow version ID */ + workflowVersionId: Scalars['String']; +}; + export type SendInviteLink = { __typename?: 'SendInviteLink'; /** Boolean that confirms query was dispatched */ @@ -1087,6 +1094,7 @@ export type WorkspaceFeatureFlagsArgs = { export enum WorkspaceActivationStatus { Active = 'ACTIVE', Inactive = 'INACTIVE', + OngoingCreation = 'ONGOING_CREATION', PendingCreation = 'PENDING_CREATION' } diff --git a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx index 9a94373cef34..38803c9efecf 100644 --- a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx +++ b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx @@ -17,7 +17,7 @@ const StyledItemsContainer = styled.div` align-items: center; display: flex; flex-direction: column; - gap: 12px; + gap: 14px; height: calc(100dvh - 32px); margin-bottom: auto; max-width: 204px; diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index e8fb4f36669e..74781c52cecd 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -42,6 +42,7 @@ export const useOpenCreateActivityDrawer = ({ activityObjectNameSingular === CoreObjectNameSingular.Task ? CoreObjectNameSingular.TaskTarget : CoreObjectNameSingular.NoteTarget, + shouldMatchRootQueryFilter: true, }); const setActivityTargetableEntityArray = useSetRecoilState( diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx index d1b35d7f2293..ecb7bc90d51f 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx @@ -1,4 +1,4 @@ -import { IconCirclePlus, IconEditCircle, useIcons } from 'twenty-ui'; +import { IconCirclePlus, IconEditCircle, IconTrash, useIcons } from 'twenty-ui'; import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -19,6 +19,9 @@ export const EventIconDynamicComponent = ({ if (eventAction === 'updated') { return ; } + if (eventAction === 'deleted') { + return ; + } const IconComponent = getIcon(linkedObjectMetadataItem?.icon); diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx index f2dcc69055fa..053e9217bb66 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx @@ -45,6 +45,17 @@ export const EventRowMainObject = ({ /> ); } + case 'deleted': { + return ( + + + {labelIdentifierValue} + + was deleted by + {authorFullName} + + ); + } default: return null; } diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/sortCachedObjectEdges.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/sortCachedObjectEdges.ts index 6f76dd66a5a8..6b762f22158a 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/sortCachedObjectEdges.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/sortCachedObjectEdges.ts @@ -1,9 +1,9 @@ import { Reference, StoreObject } from '@apollo/client'; import { ReadFieldFunction } from '@apollo/client/cache/core/types/common'; -import { OrderBy } from '@/object-metadata/types/OrderBy'; import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge'; import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy'; +import { OrderBy } from '@/types/OrderBy'; import { isDefined } from '~/utils/isDefined'; import { sortAsc, sortDesc, sortNullsFirst, sortNullsLast } from '~/utils/sort'; diff --git a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx index 2581dd48934c..39d2437bf6f7 100644 --- a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx @@ -1,6 +1,6 @@ import { Button } from '@/ui/input/button/components/Button'; import styled from '@emotion/styled'; -import { Banner, IconComponent } from 'twenty-ui'; +import { Banner, BannerVariant, IconComponent } from 'twenty-ui'; const StyledBanner = styled(Banner)` position: absolute; @@ -14,26 +14,30 @@ const StyledText = styled.div` export const InformationBanner = ({ message, + variant = 'default', buttonTitle, buttonIcon, buttonOnClick, }: { message: string; - buttonTitle: string; + variant?: BannerVariant; + buttonTitle?: string; buttonIcon?: IconComponent; - buttonOnClick: () => void; + buttonOnClick?: () => void; }) => { return ( - + {message} -