diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 278981d323..7cecd8b9fd 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -3151,7 +3151,7 @@ type Query { getViewGroup(id: String!): ViewGroup findManyApplications: [Application!]! findOneApplication(id: UUID, universalIdentifier: UUID): Application! - findManyMarketplaceApps: [MarketplaceApp!]! + findManyMarketplaceApps(universalIdentifiers: [String!]): [MarketplaceApp!]! findMarketplaceAppDetail(universalIdentifier: String!): MarketplaceAppDetail! publicMarketplaceApps(isVetted: Boolean! = true): [MarketplaceApp!]! publicMarketplaceAppDetail(universalIdentifier: String!): MarketplaceAppDetail! diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index aa6f2ef676..86081d4fbb 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -5990,7 +5990,7 @@ export interface QueryGenqlSelection{ getViewGroup?: (ViewGroupGenqlSelection & { __args: {id: Scalars['String']} }) findManyApplications?: ApplicationGenqlSelection findOneApplication?: (ApplicationGenqlSelection & { __args?: {id?: (Scalars['UUID'] | null), universalIdentifier?: (Scalars['UUID'] | null)} }) - findManyMarketplaceApps?: MarketplaceAppGenqlSelection + findManyMarketplaceApps?: (MarketplaceAppGenqlSelection & { __args?: {universalIdentifiers?: (Scalars['String'][] | null)} }) findMarketplaceAppDetail?: (MarketplaceAppDetailGenqlSelection & { __args: {universalIdentifier: Scalars['String']} }) publicMarketplaceApps?: (MarketplaceAppGenqlSelection & { __args: {isVetted: Scalars['Boolean']} }) publicMarketplaceAppDetail?: (MarketplaceAppDetailGenqlSelection & { __args: {universalIdentifier: Scalars['String']} }) diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts index 03ab1f0e53..b1688e9490 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/types.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts @@ -6529,7 +6529,13 @@ export default { } ], "findManyMarketplaceApps": [ - 228 + 228, + { + "universalIdentifiers": [ + 1, + "[String!]" + ] + } ], "findMarketplaceAppDetail": [ 232, diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index a986475d53..549a1a4ddf 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -4593,6 +4593,11 @@ export type QueryFindApplicationRegistrationVariablesArgs = { }; +export type QueryFindManyMarketplaceAppsArgs = { + universalIdentifiers?: InputMaybe>; +}; + + export type QueryFindMarketplaceAppDetailArgs = { universalIdentifier: Scalars['String']['input']; }; @@ -7259,7 +7264,9 @@ export type UpgradeApplicationMutationVariables = Exact<{ export type UpgradeApplicationMutation = { __typename?: 'Mutation', upgradeApplication: boolean }; -export type FindManyMarketplaceAppsQueryVariables = Exact<{ [key: string]: never; }>; +export type FindManyMarketplaceAppsQueryVariables = Exact<{ + universalIdentifiers?: InputMaybe | Scalars['String']['input']>; +}>; export type FindManyMarketplaceAppsQuery = { __typename?: 'Query', findManyMarketplaceApps: Array<{ __typename?: 'MarketplaceApp', id: string, name: string, description: string, author: string, category: string, logo?: string | null, sourcePackage?: string | null, isVetted: boolean }> }; @@ -8951,7 +8958,7 @@ export const FindOneLogicFunctionDocument = {"kind":"Document","definitions":[{" export const GetLogicFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLogicFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getLogicFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; export const InstallApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const UpgradeApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpgradeApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upgradeApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appRegistrationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"targetVersion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"targetVersion"}}}]}]}}]} as unknown as DocumentNode; -export const FindManyMarketplaceAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyMarketplaceApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyMarketplaceApps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceApp"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"isVetted"}}]}}]} as unknown as DocumentNode; +export const FindManyMarketplaceAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyMarketplaceApps"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifiers"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyMarketplaceApps"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifiers"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifiers"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceApp"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"isVetted"}}]}}]} as unknown as DocumentNode; export const FindMarketplaceAppDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindMarketplaceAppDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findMarketplaceAppDetail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MarketplaceAppDetailFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MarketplaceAppDetailFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MarketplaceAppDetail"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isVetted"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}},{"kind":"Field","name":{"kind":"Name","value":"aboutDescription"}},{"kind":"Field","name":{"kind":"Name","value":"termsUrl"}},{"kind":"Field","name":{"kind":"Name","value":"emailSupport"}},{"kind":"Field","name":{"kind":"Name","value":"issueReportUrl"}},{"kind":"Field","name":{"kind":"Name","value":"screenshots"}},{"kind":"Field","name":{"kind":"Name","value":"defaultRoleUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"roles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllSettings"}},{"kind":"Field","name":{"kind":"Name","value":"canAccessAllTools"}},{"kind":"Field","name":{"kind":"Name","value":"canReadAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyAllObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"permissionFlagUniversalIdentifiers"}},{"kind":"Field","name":{"kind":"Name","value":"objectPermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"canReadObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canSoftDeleteObjectRecords"}},{"kind":"Field","name":{"kind":"Name","value":"canDestroyObjectRecords"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fieldPermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"objectUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"fieldUniversalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"canReadFieldValue"}},{"kind":"Field","name":{"kind":"Name","value":"canUpdateFieldValue"}}]}}]}}]}}]} as unknown as DocumentNode; export const FindMarketplaceAppManifestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindMarketplaceAppManifest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findMarketplaceAppDetail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"universalIdentifier"},"value":{"kind":"Variable","name":{"kind":"Name","value":"universalIdentifier"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"manifest"}}]}}]}}]} as unknown as DocumentNode; export const CreateManyNavigationMenuItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateManyNavigationMenuItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inputs"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateNavigationMenuItemInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createManyNavigationMenuItems"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inputs"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inputs"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordId"}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"viewId"}},{"kind":"Field","name":{"kind":"Name","value":"folderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"pageLayoutId"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NavigationMenuItemQueryFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NavigationMenuItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NavigationMenuItemFields"}},{"kind":"Field","name":{"kind":"Name","value":"targetRecordIdentifier"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifier"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/modules/marketplace/graphql/queries/findManyMarketplaceApps.ts b/packages/twenty-front/src/modules/marketplace/graphql/queries/findManyMarketplaceApps.ts index 3ca3e0131e..83ab9217a0 100644 --- a/packages/twenty-front/src/modules/marketplace/graphql/queries/findManyMarketplaceApps.ts +++ b/packages/twenty-front/src/modules/marketplace/graphql/queries/findManyMarketplaceApps.ts @@ -4,8 +4,8 @@ import { MARKETPLACE_APP_FRAGMENT } from '@/marketplace/graphql/fragments/market export const FIND_MANY_MARKETPLACE_APPS = gql` ${MARKETPLACE_APP_FRAGMENT} - query FindManyMarketplaceApps { - findManyMarketplaceApps { + query FindManyMarketplaceApps($universalIdentifiers: [String!]) { + findManyMarketplaceApps(universalIdentifiers: $universalIdentifiers) { ...MarketplaceAppFields } } diff --git a/packages/twenty-front/src/modules/marketplace/hooks/useMarketplaceApps.ts b/packages/twenty-front/src/modules/marketplace/hooks/useMarketplaceApps.ts index 6e6efb72d4..66bce26070 100644 --- a/packages/twenty-front/src/modules/marketplace/hooks/useMarketplaceApps.ts +++ b/packages/twenty-front/src/modules/marketplace/hooks/useMarketplaceApps.ts @@ -1,8 +1,12 @@ import { useQuery } from '@apollo/client/react'; import { FindManyMarketplaceAppsDocument } from '~/generated-metadata/graphql'; -export const useMarketplaceApps = () => { - const { data, loading, error } = useQuery(FindManyMarketplaceAppsDocument); +export const useMarketplaceApps = ({ + universalIdentifiers, +}: { universalIdentifiers?: string[] } = {}) => { + const { data, loading, error } = useQuery(FindManyMarketplaceAppsDocument, { + variables: { universalIdentifiers }, + }); return { data: data?.findManyMarketplaceApps ?? [], diff --git a/packages/twenty-front/src/modules/onboarding/effect-components/InstallAppsAutoSkipEffect.tsx b/packages/twenty-front/src/modules/onboarding/effect-components/InstallAppsAutoSkipEffect.tsx new file mode 100644 index 0000000000..3ba89fa758 --- /dev/null +++ b/packages/twenty-front/src/modules/onboarding/effect-components/InstallAppsAutoSkipEffect.tsx @@ -0,0 +1,35 @@ +import { useTriggerInstallAppsOnboardingStep } from '@/onboarding/hooks/useTriggerInstallAppsOnboardingStep'; +import { useEffect, useRef } from 'react'; + +type InstallAppsAutoSkipEffectProps = { + onError: () => void; +}; + +export const InstallAppsAutoSkipEffect = ({ + onError, +}: InstallAppsAutoSkipEffectProps) => { + const triggerInstallAppsOnboardingStep = + useTriggerInstallAppsOnboardingStep(); + + // oxlint-disable-next-line twenty/no-state-useref + const hasSkippedRef = useRef(false); + + useEffect(() => { + if (hasSkippedRef.current) { + return; + } + hasSkippedRef.current = true; + + const skip = async () => { + try { + await triggerInstallAppsOnboardingStep([]); + } catch { + onError(); + } + }; + + void skip(); + }, [triggerInstallAppsOnboardingStep, onError]); + + return null; +}; diff --git a/packages/twenty-front/src/pages/onboarding/InstallApps.tsx b/packages/twenty-front/src/pages/onboarding/InstallApps.tsx index fe5fb36bc1..3e739a5f2b 100644 --- a/packages/twenty-front/src/pages/onboarding/InstallApps.tsx +++ b/packages/twenty-front/src/pages/onboarding/InstallApps.tsx @@ -1,12 +1,24 @@ import { onboardingConfigState } from '@/client-config/states/onboardingConfigState'; import { useMarketplaceApps } from '@/marketplace/hooks/useMarketplaceApps'; import { ONBOARDING_INSTALLABLE_APPS } from '@/onboarding/constants/OnboardingInstallableApps'; +import { InstallAppsAutoSkipEffect } from '@/onboarding/effect-components/InstallAppsAutoSkipEffect'; import { useInstallOnboardingApps } from '@/onboarding/hooks/useInstallOnboardingApps'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useCallback, useState } from 'react'; +import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; import { InstallAppsContent } from '~/pages/onboarding/InstallAppsContent'; +const ONBOARDING_INSTALLABLE_APP_UNIVERSAL_IDENTIFIERS = + ONBOARDING_INSTALLABLE_APPS.map((app) => app.universalIdentifier); + export const InstallApps = () => { - const { data: marketplaceApps } = useMarketplaceApps(); + const { + data: marketplaceApps, + isLoading, + error, + } = useMarketplaceApps({ + universalIdentifiers: ONBOARDING_INSTALLABLE_APP_UNIVERSAL_IDENTIFIERS, + }); const onboardingConfig = useAtomStateValue(onboardingConfigState); const { selectedUniversalIdentifiers, @@ -15,18 +27,39 @@ export const InstallApps = () => { installSelectedAppsAndContinue, skip, } = useInstallOnboardingApps(); + const [hasAutoSkipFailed, setHasAutoSkipFailed] = useState(false); - const apps = ONBOARDING_INSTALLABLE_APPS.map((app) => ({ - ...app, - logo: - marketplaceApps.find( - (marketplaceApp) => marketplaceApp.id === app.universalIdentifier, - )?.logo ?? null, - })); + const availableApps = ONBOARDING_INSTALLABLE_APPS.flatMap((app) => { + const marketplaceApp = marketplaceApps.find( + (marketplaceApp) => marketplaceApp.id === app.universalIdentifier, + ); + + return isDefined(marketplaceApp) + ? [{ ...app, logo: marketplaceApp.logo ?? null }] + : []; + }); + + const handleAutoSkipError = useCallback(() => { + setHasAutoSkipFailed(true); + }, []); + + if (isLoading) { + return null; + } + + const hasLoadedAvailabilitySuccessfully = !isDefined(error); + const shouldAutoSkip = + hasLoadedAvailabilitySuccessfully && + !isNonEmptyArray(availableApps) && + !hasAutoSkipFailed; + + if (shouldAutoSkip) { + return ; + } return ( @@ -129,10 +131,12 @@ export const InstallAppsContent = ({ - {t`Get the most out of your CRM by installing some apps`} + {hasApps + ? t`Get the most out of your CRM by installing some apps` + : t`No apps are available to install right now`} - {isDefined(creditsRewardPerApp) && ( + {isDefined(creditsRewardPerApp) && hasApps && ( - - - {apps.map((app) => { - const labelText = t(app.label); - const isSelected = selectedUniversalIdentifiers.includes( - app.universalIdentifier, - ); + {hasApps && ( + + + {apps.map((app) => { + const labelText = t(app.label); + const isSelected = selectedUniversalIdentifiers.includes( + app.universalIdentifier, + ); - return ( - - - - {labelText} - - {t(app.description)} - - - onToggleApp(app.universalIdentifier)} - > - + - - - ); - })} - - + + {labelText} + + {t(app.description)} + + + onToggleApp(app.universalIdentifier)} + > + + + + ); + })} + + + )} - - - + {hasApps && ( + + + + )} diff --git a/packages/twenty-front/src/pages/onboarding/__stories__/InstallApps.stories.tsx b/packages/twenty-front/src/pages/onboarding/__stories__/InstallApps.stories.tsx index a3e134189c..3a2940db0d 100644 --- a/packages/twenty-front/src/pages/onboarding/__stories__/InstallApps.stories.tsx +++ b/packages/twenty-front/src/pages/onboarding/__stories__/InstallApps.stories.tsx @@ -1,7 +1,7 @@ import { getOperationName } from '~/utils/getOperationName'; import { type Meta, type StoryObj } from '@storybook/react-vite'; import { HttpResponse, graphql } from 'msw'; -import { within } from 'storybook/test'; +import { expect, within } from 'storybook/test'; import { AppPath } from 'twenty-shared/types'; import { FIND_MANY_MARKETPLACE_APPS } from '@/marketplace/graphql/queries/findManyMarketplaceApps'; @@ -13,35 +13,70 @@ import { type PageDecoratorArgs, } from '~/testing/decorators/PageDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; +import { mockedApolloClient } from '~/testing/mockedApolloClient'; import { mockedOnboardingUserData } from '~/testing/mock-data/users'; +const CALL_RECORDER_UNIVERSAL_IDENTIFIER = + '8da4b8b5-5edf-4880-b51f-ab6e679ec617'; +const ENRICHMENT_UNIVERSAL_IDENTIFIER = '4a1178c1-3535-4a47-b592-231d3216b36f'; +const LAST_CONTACT_UNIVERSAL_IDENTIFIER = + '66a504cc-0a75-410e-a43f-cdeae1db1522'; + +const buildMarketplaceApp = (id: string, name: string) => ({ + __typename: 'MarketplaceApp', + id, + name, + description: name, + author: 'Twenty', + category: 'productivity', + logo: null, + sourcePackage: name, + isVetted: true, +}); + +const currentUserHandler = graphql.query( + getOperationName(GET_CURRENT_USER) ?? '', + () => { + return HttpResponse.json({ + data: { + currentUser: mockedOnboardingUserData( + OnboardingStatus.APPS_INSTALLATION, + ), + }, + }); + }, +); + +const buildHandlers = ( + marketplaceApps: ReturnType[], +) => [ + currentUserHandler, + graphql.query(getOperationName(FIND_MANY_MARKETPLACE_APPS) ?? '', () => { + return HttpResponse.json({ + data: { findManyMarketplaceApps: marketplaceApps }, + }); + }), + graphqlMocks.handlers, +]; + const meta: Meta = { title: 'Pages/Onboarding/InstallApps', component: InstallApps, decorators: [PageDecorator], args: { routePath: AppPath.InstallApps }, + beforeEach: async () => { + await mockedApolloClient.clearStore(); + }, parameters: { msw: { - handlers: [ - graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { - return HttpResponse.json({ - data: { - currentUser: mockedOnboardingUserData( - OnboardingStatus.APPS_INSTALLATION, - ), - }, - }); - }), - graphql.query( - getOperationName(FIND_MANY_MARKETPLACE_APPS) ?? '', - () => { - return HttpResponse.json({ - data: { findManyMarketplaceApps: [] }, - }); - }, + handlers: buildHandlers([ + buildMarketplaceApp( + CALL_RECORDER_UNIVERSAL_IDENTIFIER, + 'Call recorder', ), - graphqlMocks.handlers, - ], + buildMarketplaceApp(ENRICHMENT_UNIVERSAL_IDENTIFIER, 'Enrichment'), + buildMarketplaceApp(LAST_CONTACT_UNIVERSAL_IDENTIFIER, 'Last contact'), + ]), }, }, }; @@ -54,5 +89,52 @@ export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement.ownerDocument.body); await canvas.findByText('Install your first apps'); + await canvas.findByText('Call recorder'); + await canvas.findByText('Enrichment'); + await canvas.findByText('Last contact'); + }, +}; + +export const OnlyAvailableApps: Story = { + parameters: { + msw: { + handlers: buildHandlers([ + buildMarketplaceApp( + CALL_RECORDER_UNIVERSAL_IDENTIFIER, + 'Call recorder', + ), + ]), + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await canvas.findByText('Call recorder'); + expect(canvas.queryByText('Enrichment')).toBeNull(); + expect(canvas.queryByText('Last contact')).toBeNull(); + }, +}; + +export const MarketplaceUnavailable: Story = { + parameters: { + msw: { + handlers: [ + currentUserHandler, + graphql.query( + getOperationName(FIND_MANY_MARKETPLACE_APPS) ?? '', + () => { + return HttpResponse.json({ + errors: [{ message: 'Marketplace unavailable' }], + }); + }, + ), + graphqlMocks.handlers, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement.ownerDocument.body); + await canvas.findByText('No apps are available to install right now'); + expect(canvas.queryByText('Install')).toBeNull(); + await canvas.findByText('Skip'); }, }; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-public.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-public.resolver.ts index b19a54fea9..a2beac8b29 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-public.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-public.resolver.ts @@ -26,7 +26,7 @@ export class MarketplacePublicResolver { @Args('isVetted', { type: () => Boolean, defaultValue: true }) isVetted: boolean, ): Promise { - return this.marketplaceQueryService.findManyMarketplaceApps(isVetted); + return this.marketplaceQueryService.findManyMarketplaceApps({ isVetted }); } @Query(() => MarketplaceAppDetailDTO, { name: 'publicMarketplaceAppDetail' }) diff --git a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-query.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-query.service.ts index ae2f274e41..0af47344e8 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-query.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace-query.service.ts @@ -22,16 +22,27 @@ export class MarketplaceQueryService { private readonly coreEntityCacheService: CoreEntityCacheService, ) {} - async findManyMarketplaceApps( - isVetted?: boolean, - ): Promise { + async findManyMarketplaceApps({ + universalIdentifiers, + isVetted, + }: { + universalIdentifiers?: string[]; + isVetted?: boolean; + } = {}): Promise { const appsByUniversalIdentifier = (await this.coreEntityCacheService.get( 'marketplaceCatalog', MARKETPLACE_CATALOG_CACHE_ENTITY_ID, )) ?? {}; - const apps = Object.values(appsByUniversalIdentifier); + const apps = isNonEmptyArray(universalIdentifiers) + ? universalIdentifiers + .map( + (universalIdentifier) => + appsByUniversalIdentifier[universalIdentifier], + ) + .filter(isDefined) + : Object.values(appsByUniversalIdentifier); if (!isDefined(isVetted)) { return apps; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace.resolver.ts index 287d626109..74e6509860 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-marketplace/marketplace.resolver.ts @@ -28,8 +28,17 @@ export class MarketplaceResolver { ) {} @Query(() => [MarketplaceAppDTO]) - async findManyMarketplaceApps(): Promise { - return this.marketplaceQueryService.findManyMarketplaceApps(); + async findManyMarketplaceApps( + @Args({ + name: 'universalIdentifiers', + type: () => [String], + nullable: true, + }) + universalIdentifiers?: string[], + ): Promise { + return this.marketplaceQueryService.findManyMarketplaceApps({ + universalIdentifiers, + }); } @Query(() => MarketplaceAppDetailDTO)