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:
Raphaël Bosi
2026-07-09 18:12:11 +02:00
committed by GitHub
parent b9cd62f396
commit 9c405384f3
13 changed files with 295 additions and 100 deletions
@@ -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' })
@@ -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;
@@ -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)