1518 extensibility front add an application section in settings (#15056)

Protected by IS_APPLICATION_ENABLED featureFlag

Add `Application` section in settings

<img width="301" height="137" alt="image"
src="https://github.com/user-attachments/assets/ee53bdd2-36f6-45c6-8646-17b1e08abf00"
/>


A `settings/applications` route listing all installed applications

<img width="661" height="428" alt="image"
src="https://github.com/user-attachments/assets/69d534c4-4e9e-452a-a3d9-ded0223bb457"
/>

Introduce a new Tag for application managed items

<img width="885" height="759" alt="image"
src="https://github.com/user-attachments/assets/19767be5-61e5-4bd2-a51d-54ed9bfb1923"
/>



A `settings/applications/<application_id>` details setting page listing
all objects, serverlessFunctions and agents created by the application:

<img width="917" height="778" alt="image"
src="https://github.com/user-attachments/assets/7fc056a6-1d73-4242-b2eb-6f8955d8597d"
/>

A `settings/applications/<application_id>/<serverless_function_id>`

<img width="905" height="652" alt="image"
src="https://github.com/user-attachments/assets/56ca0021-26bf-42cb-9abf-34879f16050a"
/>

Add trigger tab in serverless function details (readonly for now)

<img width="899" height="724" alt="image"
src="https://github.com/user-attachments/assets/5eeefa35-f2a4-4fd8-a640-7b5c5891f226"
/>

Set object, serverless and agent setting detail pages readonly for
managed items
<img width="1075" height="859" alt="image"
src="https://github.com/user-attachments/assets/57c73d69-4980-47a2-b752-8dc5ab494530"
/>
<img width="648" height="582" alt="image"
src="https://github.com/user-attachments/assets/5ad5f3f7-3bc3-4e40-870a-4981c6492524"
/>
<img width="982" height="692" alt="image"
src="https://github.com/user-attachments/assets/7ad756c4-5d33-4a0a-9eb8-416c040362b9"
/>
<img width="1077" height="647" alt="image"
src="https://github.com/user-attachments/assets/e086b9f5-4062-4d10-82a9-4023de3cad3f"
/>
This commit is contained in:
martmull
2025-10-15 17:13:29 +02:00
committed by GitHub
parent c0ed246a03
commit b16ab1b7c9
109 changed files with 2464 additions and 1357 deletions
@@ -0,0 +1,25 @@
import { Catch, ExceptionFilter } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@Catch(ApplicationException)
export class ApplicationExceptionFilter implements ExceptionFilter {
catch(exception: ApplicationException) {
switch (exception.code) {
case ApplicationExceptionCode.OBJECT_NOT_FOUND:
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
case ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
throw new NotFoundError(exception);
default: {
assertUnreachable(exception.code);
}
}
}
}
@@ -6,4 +6,5 @@ export enum ApplicationExceptionCode {
OBJECT_NOT_FOUND = 'OBJECT_NOT_FOUND',
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
APPLICATION_NOT_FOUND = 'APPLICATION_NOT_FOUND',
}
@@ -1,5 +1,5 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@@ -8,15 +8,36 @@ import { ApplicationSyncService } from 'src/engine/core-modules/application/appl
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
import { DeleteApplicationInput } from 'src/engine/core-modules/application/dtos/deleteApplication.input';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
@UseGuards(WorkspaceAuthGuard)
@Resolver()
@UseFilters(ApplicationExceptionFilter)
export class ApplicationResolver {
constructor(
private readonly applicationSyncService: ApplicationSyncService,
private readonly applicationService: ApplicationService,
) {}
@Query(() => [ApplicationDTO])
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async findManyApplications(@AuthWorkspace() { id: workspaceId }: Workspace) {
return this.applicationService.findManyApplications(workspaceId);
}
@Query(() => ApplicationDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
async findOneApplication(
@Args('id', { type: () => UUIDScalarType }) id: string,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return await this.applicationService.findOneApplication(id, workspaceId);
}
@Mutation(() => Boolean)
async syncApplication(
@Args() { manifest, packageJson, yarnLock }: ApplicationInput,
@@ -7,6 +7,10 @@ import { Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
@Injectable()
export class ApplicationService {
@@ -16,6 +20,34 @@ export class ApplicationService {
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async findManyApplications(
workspaceId: string,
): Promise<ApplicationEntity[]> {
return this.applicationRepository.find({
where: { workspaceId },
relations: ['serverlessFunctions', 'agents', 'objects'],
});
}
async findOneApplication(
applicationId: string,
workspaceId: string,
): Promise<ApplicationEntity> {
const application = await this.applicationRepository.findOne({
where: { workspaceId, id: applicationId },
relations: ['serverlessFunctions', 'agents', 'objects'],
});
if (!isDefined(application)) {
throw new ApplicationException(
`Application with id ${applicationId} not found`,
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
);
}
return application;
}
async findById(id: string): Promise<ApplicationEntity | null> {
return this.applicationRepository.findOne({
where: { id },
@@ -0,0 +1,33 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
@ObjectType('Application')
export class ApplicationDTO {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
id: string;
@IsString()
@Field()
name: string;
@IsString()
@Field()
description: string;
@Field(() => [AgentDTO])
agents: AgentDTO[];
@Field(() => [ServerlessFunctionDTO])
serverlessFunctions: ServerlessFunctionDTO[];
@Field(() => [ObjectMetadataDTO])
objects: ObjectMetadataDTO[];
}
@@ -5,6 +5,7 @@ export enum FeatureFlagKey {
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
@@ -244,6 +244,7 @@ export class WorkspaceResolver {
return {
...agent,
roleId: agent.roleId ?? undefined,
applicationId: agent.applicationId ?? undefined,
};
} catch {
// If agent is not found, return null instead of throwing