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
@@ -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,
});
}
}