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
@@ -84,6 +84,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
"definePostInstallLogicFunction",
"definePreInstallLogicFunction",
"defineRole",
"defineSettingsFrontComponent",
"defineSkill",
"defineUninstallLogicFunction",
"defineView",
@@ -227,6 +227,32 @@ describe('manifestValidate', () => {
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should not flag a front component referenced via settingsFrontComponent as a duplicate', () => {
const frontComponentId = '550e8400-e29b-41d4-a716-446655440050';
const frontComponent = {
universalIdentifier: frontComponentId,
name: 'app-settings',
componentName: 'AppSettings',
sourceComponentPath: 'src/front-components/app-settings.tsx',
builtComponentPath: 'dist/app-settings.mjs',
builtComponentChecksum: '00000000-0000-4000-8000-000000000000',
isHeadless: false,
} as unknown as Manifest['frontComponents'][number];
const result = manifestValidate({
...validManifest,
application: {
...validApplication,
settingsFrontComponent: { universalIdentifier: frontComponentId },
},
frontComponents: [frontComponent],
});
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
});
describe('relation field validation', () => {
@@ -122,6 +122,7 @@ export const buildManifest = async (
[];
const uninstallLogicFunctions: UninstallLogicFunctionApplicationManifest[] =
[];
const settingsFrontComponentUniversalIdentifiers: string[] = [];
const applicationRoleUniversalIdentifiers: string[] = [];
const applicationFilePaths: string[] = [];
const objectsFilePaths: string[] = [];
@@ -377,6 +378,14 @@ export const buildManifest = async (
frontComponents.push(config);
frontComponentsFilePaths.push(relativePath);
if (
targetFunctionName === TargetFunction.DefineSettingsFrontComponent
) {
settingsFrontComponentUniversalIdentifiers.push(
extract.config.universalIdentifier,
);
}
break;
}
case ManifestEntityKey.Views: {
@@ -559,6 +568,12 @@ export const buildManifest = async (
);
}
if (settingsFrontComponentUniversalIdentifiers.length > 1) {
errors.push(
'Only one settings front component is allowed per application',
);
}
if (applicationRoleUniversalIdentifiers.length > 1) {
errors.push('Only one defineApplicationRole is allowed per application');
}
@@ -611,6 +626,14 @@ export const buildManifest = async (
...(uninstallLogicFunctions.length >= 1
? { uninstallLogicFunction: uninstallLogicFunctions[0] }
: {}),
...(settingsFrontComponentUniversalIdentifiers.length >= 1
? {
settingsFrontComponent: {
universalIdentifier:
settingsFrontComponentUniversalIdentifiers[0],
},
}
: {}),
};
})()
: undefined;
@@ -16,6 +16,7 @@ export enum TargetFunction {
DefineAgent = 'defineAgent',
DefineConnectionProvider = 'defineConnectionProvider',
DefineFrontComponent = 'defineFrontComponent',
DefineSettingsFrontComponent = 'defineSettingsFrontComponent',
DefineView = 'defineView',
DefineViewField = 'defineViewField',
DefineNavigationMenuItem = 'defineNavigationMenuItem',
@@ -70,6 +71,8 @@ export const TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING: Record<
[TargetFunction.DefineConnectionProvider]:
ManifestEntityKey.ConnectionProviders,
[TargetFunction.DefineFrontComponent]: ManifestEntityKey.FrontComponents,
[TargetFunction.DefineSettingsFrontComponent]:
ManifestEntityKey.FrontComponents,
[TargetFunction.DefineView]: ManifestEntityKey.Views,
[TargetFunction.DefineViewField]: ManifestEntityKey.ViewFields,
[TargetFunction.DefineNavigationMenuItem]:
@@ -74,7 +74,8 @@ const findUniversalIdentifiers = (obj: object): string[] => {
key === 'postInstallLogicFunction' ||
key === 'preInstallLogicFunction' ||
key === 'uninstallLogicFunction' ||
key === 'onConnectLogicFunction'
key === 'onConnectLogicFunction' ||
key === 'settingsFrontComponent'
) {
continue;
}