Add isConfigured to application registration in App admin panel (#20326)

## After
Added "Configured" column in admin panel apps tab
<img width="1171" height="611" alt="image"
src="https://github.com/user-attachments/assets/e6850b39-5789-4ad2-b3df-192a28cb03e2"
/>

Add a banner to ask to configure the application 
<img width="1218" height="483" alt="image"
src="https://github.com/user-attachments/assets/1491363c-3479-41ca-97b7-c7dc9e07ff16"
/>
This commit is contained in:
martmull
2026-05-07 09:59:01 +02:00
committed by GitHub
parent eddd2c979a
commit 948ed964bb
16 changed files with 190 additions and 37 deletions
@@ -0,0 +1,18 @@
import { Context, Parent, ResolveField } from '@nestjs/graphql';
import { AdminResolver } from 'src/engine/api/graphql/graphql-config/decorators/admin-resolver.decorator';
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
@AdminResolver(() => ApplicationRegistrationEntity)
export class AdminPanelApplicationRegistrationResolver {
@ResolveField(() => Boolean)
async isConfigured(
@Parent() registration: ApplicationRegistrationEntity,
@Context() context: { loaders: IDataloaders },
): Promise<boolean> {
return context.loaders.isConfiguredLoader.load({
applicationRegistrationId: registration.id,
});
}
}
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminPanelApplicationRegistrationResolver } from 'src/engine/core-modules/admin-panel/admin-panel-application-registration.resolver';
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-panel.resolver';
@@ -78,6 +79,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
],
providers: [
AdminPanelResolver,
AdminPanelApplicationRegistrationResolver,
AdminPanelUserLookupService,
AdminPanelStatisticsService,
AdminPanelBillingService,
@@ -7,6 +7,7 @@ import {
ApplicationRegistrationException,
ApplicationRegistrationExceptionCode,
} from 'src/engine/core-modules/application/application-registration/application-registration.exception';
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/application/application-marketplace/crons/marketplace-catalog-sync.cron.job';
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app.dto';
@@ -22,6 +23,7 @@ export class MarketplaceQueryService {
constructor(
private readonly applicationRegistrationService: ApplicationRegistrationService,
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@@ -46,9 +48,14 @@ export class MarketplaceQueryService {
return [];
}
return registrations.map((registration) =>
this.toMarketplaceAppDTO(registration),
);
const configuredStatuses =
await this.applicationRegistrationVariableService.isConfiguredBatch(
registrations.map((registration) => registration.id),
);
return registrations
.filter((registration) => configuredStatuses.get(registration.id) ?? true)
.map((registration) => this.toMarketplaceAppDTO(registration));
}
async findMarketplaceAppDetail(
@@ -175,6 +175,46 @@ export class ApplicationRegistrationVariableService {
}
}
async isConfiguredBatch(
applicationRegistrationIds: string[],
): Promise<Map<string, boolean>> {
const variables = await this.variableRepository.find({
where: { applicationRegistrationId: In(applicationRegistrationIds) },
});
const variablesByRegistrationId = new Map<
string,
ApplicationRegistrationVariableEntity[]
>();
for (const variable of variables) {
const existing =
variablesByRegistrationId.get(variable.applicationRegistrationId) ?? [];
existing.push(variable);
variablesByRegistrationId.set(
variable.applicationRegistrationId,
existing,
);
}
const result = new Map<string, boolean>();
for (const id of applicationRegistrationIds) {
const registrationVariables = variablesByRegistrationId.get(id) ?? [];
const requiredVariables = registrationVariables.filter(
(v) => v.isRequired,
);
result.set(
id,
requiredVariables.every((v) => v.isFilled),
);
}
return result;
}
private async assertRegistrationOwnedByWorkspace(
registrationId: string,
workspaceId: string,
@@ -1,11 +1,20 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Mutation, Query } from '@nestjs/graphql';
import {
Args,
Context,
Mutation,
Parent,
Query,
ResolveField,
} from '@nestjs/graphql';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { PermissionFlagType } from 'twenty-shared/constants';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
@@ -47,7 +56,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@UsePipes(ResolverValidationPipe)
@MetadataResolver()
@MetadataResolver(() => ApplicationRegistrationEntity)
@UseFilters(
ApplicationRegistrationExceptionFilter,
AuthGraphqlApiExceptionFilter,
@@ -309,4 +318,14 @@ export class ApplicationRegistrationResolver {
currentOwnerWorkspaceId: workspaceId,
});
}
@ResolveField(() => Boolean)
async isConfigured(
@Parent() registration: ApplicationRegistrationEntity,
@Context() context: { loaders: IDataloaders },
): Promise<boolean> {
return context.loaders.isConfiguredLoader.load({
applicationRegistrationId: registration.id,
});
}
}