Only propose configured apps during onboarding install step (#22712)
## What - Onboarding "Install your first apps" now proposes only apps that are actually installable: it intersects the onboarding list with `findManyMarketplaceApps`, which the backend already filters to listed + configured apps (all required server variables set). - If none are available, the step auto-skips. If the marketplace query fails, it shows an intentional fallback (heading + Skip) instead of silently skipping or rendering an empty install card. - `findManyMarketplaceApps` now accepts `universalIdentifiers`, so onboarding fetches and configuration-checks only its own apps instead of the entire catalog. ## Why Previously the step rendered all hardcoded apps regardless of configuration, only borrowing logos from the marketplace, so a user could be offered an app the admin never configured. This centralizes onboarding availability on the marketplace's existing logic and keeps the query bounded as the marketplace grows. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22712?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -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!
|
||||
|
||||
@@ -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']} })
|
||||
|
||||
@@ -6529,7 +6529,13 @@ export default {
|
||||
}
|
||||
],
|
||||
"findManyMarketplaceApps": [
|
||||
228
|
||||
228,
|
||||
{
|
||||
"universalIdentifiers": [
|
||||
1,
|
||||
"[String!]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"findMarketplaceAppDetail": [
|
||||
232,
|
||||
|
||||
@@ -4593,6 +4593,11 @@ export type QueryFindApplicationRegistrationVariablesArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindManyMarketplaceAppsArgs = {
|
||||
universalIdentifiers?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
};
|
||||
|
||||
|
||||
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<Array<Scalars['String']['input']> | 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<GetLogicFunctionSourceCodeQuery, GetLogicFunctionSourceCodeQueryVariables>;
|
||||
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<InstallApplicationMutation, InstallApplicationMutationVariables>;
|
||||
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<UpgradeApplicationMutation, UpgradeApplicationMutationVariables>;
|
||||
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<FindManyMarketplaceAppsQuery, FindManyMarketplaceAppsQueryVariables>;
|
||||
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<FindManyMarketplaceAppsQuery, FindManyMarketplaceAppsQueryVariables>;
|
||||
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<FindMarketplaceAppDetailQuery, FindMarketplaceAppDetailQueryVariables>;
|
||||
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<FindMarketplaceAppManifestQuery, FindMarketplaceAppManifestQueryVariables>;
|
||||
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<CreateManyNavigationMenuItemsMutation, CreateManyNavigationMenuItemsMutationVariables>;
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? [],
|
||||
|
||||
+35
@@ -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;
|
||||
};
|
||||
@@ -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 <InstallAppsAutoSkipEffect onError={handleAutoSkipError} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<InstallAppsContent
|
||||
apps={apps}
|
||||
apps={availableApps}
|
||||
selectedUniversalIdentifiers={selectedUniversalIdentifiers}
|
||||
creditsRewardPerApp={onboardingConfig?.installAppsCreditsRewardPerApp}
|
||||
isCompleting={isCompleting}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ONBOARDING_CONTENT_BLOCK_WIDTH } from '@/onboarding/constants/Onboardin
|
||||
import { type OnboardingInstallableApp } from '@/onboarding/types/OnboardingInstallableApp';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { IconCheck, IconPlus } from 'twenty-ui/icon';
|
||||
import { IconButton, MainButton } from 'twenty-ui/input';
|
||||
@@ -118,6 +118,8 @@ export const InstallAppsContent = ({
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
|
||||
const hasApps = isNonEmptyArray(apps);
|
||||
|
||||
return (
|
||||
<StyledOnboardingStepPage>
|
||||
<StyledOnboardingStepHeading>
|
||||
@@ -129,10 +131,12 @@ export const InstallAppsContent = ({
|
||||
</OnboardingStepAnimatedItem>
|
||||
<OnboardingStepAnimatedItem index={1}>
|
||||
<StyledOnboardingStepSubtitle>
|
||||
{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`}
|
||||
</StyledOnboardingStepSubtitle>
|
||||
</OnboardingStepAnimatedItem>
|
||||
{isDefined(creditsRewardPerApp) && (
|
||||
{isDefined(creditsRewardPerApp) && hasApps && (
|
||||
<OnboardingStepAnimatedItem index={2}>
|
||||
<StyledOnboardingStepTagsRow>
|
||||
<OnboardingCreditsRewardTag
|
||||
@@ -144,63 +148,67 @@ export const InstallAppsContent = ({
|
||||
)}
|
||||
</StyledOnboardingStepHeading>
|
||||
|
||||
<OnboardingStepAnimatedItem index={3}>
|
||||
<StyledCard>
|
||||
{apps.map((app) => {
|
||||
const labelText = t(app.label);
|
||||
const isSelected = selectedUniversalIdentifiers.includes(
|
||||
app.universalIdentifier,
|
||||
);
|
||||
{hasApps && (
|
||||
<OnboardingStepAnimatedItem index={3}>
|
||||
<StyledCard>
|
||||
{apps.map((app) => {
|
||||
const labelText = t(app.label);
|
||||
const isSelected = selectedUniversalIdentifiers.includes(
|
||||
app.universalIdentifier,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledAppRow key={app.universalIdentifier}>
|
||||
<Avatar
|
||||
avatarUrl={getAbsoluteImageUrl(app.logo)}
|
||||
placeholder={labelText}
|
||||
placeholderColorSeed={app.universalIdentifier}
|
||||
size="lg"
|
||||
type="squared"
|
||||
/>
|
||||
<StyledAppText>
|
||||
<StyledAppLabel>{labelText}</StyledAppLabel>
|
||||
<StyledAppDescription>
|
||||
{t(app.description)}
|
||||
</StyledAppDescription>
|
||||
</StyledAppText>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent={isSelected ? 'blue' : 'default'}
|
||||
ariaLabel={
|
||||
isSelected
|
||||
? t`Deselect ${labelText}`
|
||||
: t`Select ${labelText}`
|
||||
}
|
||||
onClick={() => onToggleApp(app.universalIdentifier)}
|
||||
>
|
||||
<AnimatedIconCrossfade
|
||||
isActive={isSelected}
|
||||
ActiveIcon={IconCheck}
|
||||
InactiveIcon={IconPlus}
|
||||
size={theme.icon.size.md}
|
||||
return (
|
||||
<StyledAppRow key={app.universalIdentifier}>
|
||||
<Avatar
|
||||
avatarUrl={getAbsoluteImageUrl(app.logo)}
|
||||
placeholder={labelText}
|
||||
placeholderColorSeed={app.universalIdentifier}
|
||||
size="lg"
|
||||
type="squared"
|
||||
/>
|
||||
</IconButton>
|
||||
</StyledAppRow>
|
||||
);
|
||||
})}
|
||||
</StyledCard>
|
||||
</OnboardingStepAnimatedItem>
|
||||
<StyledAppText>
|
||||
<StyledAppLabel>{labelText}</StyledAppLabel>
|
||||
<StyledAppDescription>
|
||||
{t(app.description)}
|
||||
</StyledAppDescription>
|
||||
</StyledAppText>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent={isSelected ? 'blue' : 'default'}
|
||||
ariaLabel={
|
||||
isSelected
|
||||
? t`Deselect ${labelText}`
|
||||
: t`Select ${labelText}`
|
||||
}
|
||||
onClick={() => onToggleApp(app.universalIdentifier)}
|
||||
>
|
||||
<AnimatedIconCrossfade
|
||||
isActive={isSelected}
|
||||
ActiveIcon={IconCheck}
|
||||
InactiveIcon={IconPlus}
|
||||
size={theme.icon.size.md}
|
||||
/>
|
||||
</IconButton>
|
||||
</StyledAppRow>
|
||||
);
|
||||
})}
|
||||
</StyledCard>
|
||||
</OnboardingStepAnimatedItem>
|
||||
)}
|
||||
|
||||
<OnboardingStepAnimatedItem index={4}>
|
||||
<StyledFooter>
|
||||
<StyledInstallButton>
|
||||
<MainButton
|
||||
title={t`Install`}
|
||||
onClick={onInstall}
|
||||
disabled={isCompleting}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledInstallButton>
|
||||
{hasApps && (
|
||||
<StyledInstallButton>
|
||||
<MainButton
|
||||
title={t`Install`}
|
||||
onClick={onInstall}
|
||||
disabled={isCompleting}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledInstallButton>
|
||||
)}
|
||||
<OnboardingSkipButton onClick={onSkip} disabled={isCompleting} />
|
||||
</StyledFooter>
|
||||
</OnboardingStepAnimatedItem>
|
||||
|
||||
@@ -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<typeof buildMarketplaceApp>[],
|
||||
) => [
|
||||
currentUserHandler,
|
||||
graphql.query(getOperationName(FIND_MANY_MARKETPLACE_APPS) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: { findManyMarketplaceApps: marketplaceApps },
|
||||
});
|
||||
}),
|
||||
graphqlMocks.handlers,
|
||||
];
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
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');
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export class MarketplacePublicResolver {
|
||||
@Args('isVetted', { type: () => Boolean, defaultValue: true })
|
||||
isVetted: boolean,
|
||||
): Promise<MarketplaceAppDTO[]> {
|
||||
return this.marketplaceQueryService.findManyMarketplaceApps(isVetted);
|
||||
return this.marketplaceQueryService.findManyMarketplaceApps({ isVetted });
|
||||
}
|
||||
|
||||
@Query(() => MarketplaceAppDetailDTO, { name: 'publicMarketplaceAppDetail' })
|
||||
|
||||
+15
-4
@@ -22,16 +22,27 @@ export class MarketplaceQueryService {
|
||||
private readonly coreEntityCacheService: CoreEntityCacheService,
|
||||
) {}
|
||||
|
||||
async findManyMarketplaceApps(
|
||||
isVetted?: boolean,
|
||||
): Promise<MarketplaceAppDTO[]> {
|
||||
async findManyMarketplaceApps({
|
||||
universalIdentifiers,
|
||||
isVetted,
|
||||
}: {
|
||||
universalIdentifiers?: string[];
|
||||
isVetted?: boolean;
|
||||
} = {}): Promise<MarketplaceAppDTO[]> {
|
||||
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;
|
||||
|
||||
+11
-2
@@ -28,8 +28,17 @@ export class MarketplaceResolver {
|
||||
) {}
|
||||
|
||||
@Query(() => [MarketplaceAppDTO])
|
||||
async findManyMarketplaceApps(): Promise<MarketplaceAppDTO[]> {
|
||||
return this.marketplaceQueryService.findManyMarketplaceApps();
|
||||
async findManyMarketplaceApps(
|
||||
@Args({
|
||||
name: 'universalIdentifiers',
|
||||
type: () => [String],
|
||||
nullable: true,
|
||||
})
|
||||
universalIdentifiers?: string[],
|
||||
): Promise<MarketplaceAppDTO[]> {
|
||||
return this.marketplaceQueryService.findManyMarketplaceApps({
|
||||
universalIdentifiers,
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => MarketplaceAppDetailDTO)
|
||||
|
||||
Reference in New Issue
Block a user