Add Twenty app settings custom app (#18273)

## Context
This PR adds the ability to define a front-component as a custom tab for
the application settings, allowing app creators to inject some
logic/rendering the their app settings.


## Example
```typescript
// packages/twenty-apps/my-test-app/src/front-components/settings-custom-tab.tsx
import { defineFrontComponent } from 'twenty-sdk';

export const SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER =
  'a42a88a8-21ce-4d22-bc44-d5146da64726';

export const SettingsCustomTab = () => {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2>My Test App Settings</h2>
      <p>This is a custom settings tab provided by My Test App.</p>
      <p>Application creators can customize this component freely.</p>
    </div>
  );
};

export default defineFrontComponent({
  universalIdentifier: SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER,
  name: 'settings-custom-tab',
  description: 'Custom settings tab for the application',
  component: SettingsCustomTab,
});
```

```typescript
// packages/twenty-apps/my-test-app/src/application-config.ts
export default defineApplication({
  universalIdentifier: '52870ce6-e584-4bd4-bc1a-6c63c508982e',
  displayName: 'My test app',
  description: '',
  defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
  settingsCustomTabFrontComponentUniversalIdentifier:
    SETTINGS_CUSTOM_TAB_UNIVERSAL_IDENTIFIER,
});
```
<img width="786" height="757" alt="Screenshot 2026-02-26 at 14 54 09"
src="https://github.com/user-attachments/assets/fcba70db-35da-48f1-bd65-359e894a691d"
/>
This commit is contained in:
Weiko
2026-02-26 16:26:49 +01:00
committed by GitHub
parent 2d2fc06265
commit 3bbaff801a
12 changed files with 114 additions and 5 deletions
@@ -88,6 +88,9 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
@Field(() => RoleDTO, { nullable: true })
defaultRole: RoleDTO | null;
@Column({ nullable: true, type: 'uuid' })
settingsCustomTabFrontComponentId: string | null;
@Column({ nullable: false, type: 'boolean', default: true })
canBeUninstalled: boolean;
@@ -73,6 +73,11 @@ export class ApplicationDTO {
@Field({ nullable: true })
defaultRoleId?: string;
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, { nullable: true })
settingsCustomTabFrontComponentId?: string;
@IsOptional()
@Field(() => RoleDTO, { nullable: true })
defaultLogicFunctionRole?: RoleDTO;
@@ -144,10 +144,12 @@ export class ApplicationManifestMigrationService {
flatRoleMaps: refreshedFlatRoleMaps,
flatObjectMetadataMaps: refreshedFlatObjectMetadataMaps,
flatFieldMetadataMaps: refreshedFlatFieldMetadataMaps,
flatFrontComponentMaps: refreshedFlatFrontComponentMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatRoleMaps',
'flatObjectMetadataMaps',
'flatFieldMetadataMaps',
'flatFrontComponentMaps',
]);
let defaultRoleId: string | null = null;
@@ -181,11 +183,32 @@ export class ApplicationManifestMigrationService {
}
}
if (isDefined(defaultRoleId)) {
await this.applicationService.update(ownerFlatApplication.id, {
defaultRoleId,
let settingsCustomTabFrontComponentId: string | null = null;
const settingsCustomTabUniversalIdentifier =
manifest.application.settingsCustomTabFrontComponentUniversalIdentifier;
if (isDefined(settingsCustomTabUniversalIdentifier)) {
const flatFrontComponent = findFlatEntityByUniversalIdentifier({
flatEntityMaps: refreshedFlatFrontComponentMaps,
universalIdentifier: settingsCustomTabUniversalIdentifier,
});
if (!isDefined(flatFrontComponent)) {
throw new ApplicationException(
`Failed to resolve front component for settingsCustomTabFrontComponentUniversalIdentifier ${settingsCustomTabUniversalIdentifier}`,
ApplicationExceptionCode.ENTITY_NOT_FOUND,
);
}
settingsCustomTabFrontComponentId = flatFrontComponent.id;
}
await this.applicationService.update(ownerFlatApplication.id, {
workspaceId,
settingsCustomTabFrontComponentId,
...(isDefined(defaultRoleId) ? { defaultRoleId } : {}),
});
}
private async syncApplicationRolePermissions({
@@ -13,6 +13,7 @@ export const fromFlatApplicationToApplicationDto = ({
availablePackages,
universalIdentifier,
version,
settingsCustomTabFrontComponentId,
}: FlatApplication): ApplicationDTO => {
return {
canBeUninstalled,
@@ -27,5 +28,7 @@ export const fromFlatApplicationToApplicationDto = ({
availablePackages: availablePackages ?? {},
universalIdentifier,
version: version ?? undefined,
settingsCustomTabFrontComponentId:
settingsCustomTabFrontComponentId ?? undefined,
};
};
@@ -36,6 +36,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = {
logicFunctionLayerId: null,
defaultRoleId: null,
defaultRole: null,
settingsCustomTabFrontComponentId: null,
canBeUninstalled: false,
createdAt: new Date(),
updatedAt: new Date(),
@@ -31,6 +31,7 @@ const MOCK_FLAT_APPLICATION: FlatApplication = {
logicFunctionLayerId: null,
defaultRoleId: null,
defaultRole: null,
settingsCustomTabFrontComponentId: null,
canBeUninstalled: false,
createdAt: new Date(),
updatedAt: new Date(),