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:
@@ -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;
|
||||
|
||||
+26
-3
@@ -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({
|
||||
|
||||
+3
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+1
@@ -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(),
|
||||
|
||||
+1
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user