feat(applications): restore the application custom settings tab (#23256)

## Summary

Restores the application **custom settings tab** feature that was
removed in #22156. This reverts that removal so applications can again
expose a custom settings tab via a front component.

## Changes

- Restore the `SettingsApplicationCustomTab` component and its tab
entry/rendering in `SettingsApplicationDetails`.
- `ApplicationManifestMigrationService` syncs
`settingsCustomTabFrontComponent` from application manifests again
(`syncDefaultRoleAndSettingsCustomTab`), resolving the front component
from `settingsCustomTabFrontComponentUniversalIdentifier`.
- Remove the deprecation annotations added by #22156:
- `ApplicationDTO.settingsCustomTabFrontComponentId` (drop GraphQL
`@deprecated`)
-
`ApplicationManifest.settingsCustomTabFrontComponentUniversalIdentifier`
- the `settingsCustomTabFrontComponentId` column comment on
`ApplicationEntity`
- Regenerate the corresponding GraphQL schema/types to drop the
`@deprecated` reason.

The DB column was never dropped, so no schema migration is required.


---
_Generated by [Claude
Code](https://claude.ai/code/session_01A6aoLa5kZjba9C3uwo6nay)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23256?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
martmull
2026-07-27 08:52:04 +02:00
committed by GitHub
parent a94f2443b3
commit 4f9fd6f674
27 changed files with 307 additions and 47 deletions
@@ -249,7 +249,7 @@ export class ApplicationManifestMigrationService {
);
if (!dryRun) {
await this.syncDefaultRole({
await this.syncDefaultRoleAndSettingsFrontComponent({
manifest,
workspaceId,
ownerFlatApplication,
@@ -262,7 +262,7 @@ export class ApplicationManifestMigrationService {
};
}
private async syncDefaultRole({
private async syncDefaultRoleAndSettingsFrontComponent({
manifest,
workspaceId,
ownerFlatApplication,
@@ -271,10 +271,13 @@ export class ApplicationManifestMigrationService {
workspaceId: string;
ownerFlatApplication: FlatApplication;
}) {
const { flatRoleMaps: refreshedFlatRoleMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatRoleMaps',
]);
const {
flatRoleMaps: refreshedFlatRoleMaps,
flatFrontComponentMaps: refreshedFlatFrontComponentMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatRoleMaps',
'flatFrontComponentMaps',
]);
let defaultRoleId: string | null = null;
@@ -299,11 +302,31 @@ export class ApplicationManifestMigrationService {
}
}
if (isDefined(defaultRoleId)) {
await this.applicationService.update(ownerFlatApplication.id, {
workspaceId,
defaultRoleId,
let settingsCustomTabFrontComponentId: string | null = null;
const settingsFrontComponentUniversalIdentifier =
manifest.application.settingsFrontComponent?.universalIdentifier;
if (isDefined(settingsFrontComponentUniversalIdentifier)) {
const flatFrontComponent = findFlatEntityByUniversalIdentifier({
flatEntityMaps: refreshedFlatFrontComponentMaps,
universalIdentifier: settingsFrontComponentUniversalIdentifier,
});
if (!isDefined(flatFrontComponent)) {
throw new ApplicationException(
`Failed to resolve front component for settings front component universalIdentifier ${settingsFrontComponentUniversalIdentifier}`,
ApplicationExceptionCode.ENTITY_NOT_FOUND,
);
}
settingsCustomTabFrontComponentId = flatFrontComponent.id;
}
await this.applicationService.update(ownerFlatApplication.id, {
workspaceId,
settingsCustomTabFrontComponentId,
...(isDefined(defaultRoleId) ? { defaultRoleId } : {}),
});
}
}
@@ -5,10 +5,12 @@ import { type UniversalFlatFrontComponent } from 'src/engine/workspace-manager/w
export const fromFrontComponentManifestToUniversalFlatFrontComponent = ({
frontComponentManifest,
applicationUniversalIdentifier,
isSettingsFrontComponent,
now,
}: {
frontComponentManifest: FrontComponentManifest;
applicationUniversalIdentifier: string;
isSettingsFrontComponent: boolean;
now: string;
}): UniversalFlatFrontComponent => {
return {
@@ -20,7 +22,10 @@ export const fromFrontComponentManifestToUniversalFlatFrontComponent = ({
builtComponentPath: frontComponentManifest.builtComponentPath,
componentName: frontComponentManifest.componentName,
builtComponentChecksum: frontComponentManifest.builtComponentChecksum,
isHeadless: frontComponentManifest.isHeadless ?? false,
// A settings front component always renders visible UI.
isHeadless: isSettingsFrontComponent
? false
: (frontComponentManifest.isHeadless ?? false),
usesSdkClient: frontComponentManifest.usesSdkClient ?? false,
createdAt: now,
updatedAt: now,
@@ -215,12 +215,18 @@ export class ComputeApplicationManifestAllUniversalFlatEntityMapsService {
});
}
const settingsFrontComponentUniversalIdentifier =
manifest.application.settingsFrontComponent?.universalIdentifier;
for (const frontComponentManifest of manifest.frontComponents) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity:
fromFrontComponentManifestToUniversalFlatFrontComponent({
frontComponentManifest,
applicationUniversalIdentifier,
isSettingsFrontComponent:
frontComponentManifest.universalIdentifier ===
settingsFrontComponentUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
@@ -113,11 +113,6 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
@Field(() => RoleDTO, { nullable: true })
defaultRole: RoleDTO | null;
/**
* @deprecated Custom settings tabs are no longer supported. The column is
* kept (not dropped) so existing installations upgrade cleanly, but the
* value is no longer read or synced from manifests.
*/
@Column({ nullable: true, type: 'uuid' })
settingsCustomTabFrontComponentId: string | null;
@@ -95,17 +95,9 @@ export class ApplicationDTO {
@Field({ nullable: true })
defaultRoleId?: string;
/**
* @deprecated Custom settings tabs are no longer supported. Kept for
* backward compatibility with existing installations; the value is ignored.
*/
@IsOptional()
@IsUUID()
@Field(() => UUIDScalarType, {
nullable: true,
deprecationReason:
'Custom settings tabs are no longer supported. This field is ignored.',
})
@Field(() => UUIDScalarType, { nullable: true })
settingsCustomTabFrontComponentId?: string;
@IsOptional()