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:
@@ -592,7 +592,7 @@ type Application {
|
||||
canBeUninstalled: Boolean!
|
||||
autoUpgrade: Boolean!
|
||||
defaultRoleId: String
|
||||
settingsCustomTabFrontComponentId: UUID @deprecated(reason: "Custom settings tabs are no longer supported. This field is ignored.")
|
||||
settingsCustomTabFrontComponentId: UUID
|
||||
defaultLogicFunctionRole: Role
|
||||
agents: [Agent!]!
|
||||
frontComponents: [FrontComponent!]!
|
||||
|
||||
@@ -405,7 +405,6 @@ export interface Application {
|
||||
canBeUninstalled: Scalars['Boolean']
|
||||
autoUpgrade: Scalars['Boolean']
|
||||
defaultRoleId?: Scalars['String']
|
||||
/** @deprecated Custom settings tabs are no longer supported. This field is ignored. */
|
||||
settingsCustomTabFrontComponentId?: Scalars['UUID']
|
||||
defaultLogicFunctionRole?: Role
|
||||
agents: Agent[]
|
||||
@@ -3531,7 +3530,6 @@ export interface ApplicationGenqlSelection{
|
||||
canBeUninstalled?: boolean | number
|
||||
autoUpgrade?: boolean | number
|
||||
defaultRoleId?: boolean | number
|
||||
/** @deprecated Custom settings tabs are no longer supported. This field is ignored. */
|
||||
settingsCustomTabFrontComponentId?: boolean | number
|
||||
defaultLogicFunctionRole?: RoleGenqlSelection
|
||||
agents?: AgentGenqlSelection
|
||||
|
||||
@@ -36,6 +36,7 @@ Notes:
|
||||
- Pre-install, post-install, and uninstall functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`.
|
||||
- Passing `defaultRoleUniversalIdentifier` explicitly is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
|
||||
- `serverVariables` are instance-scoped configuration and secrets (e.g. API keys). Unlike `applicationVariables`, they declare no value in the manifest — the workspace operator fills them in from the app's settings, and they are injected into logic functions only once set.
|
||||
- To render a custom configuration UI inside the app's **Settings** tab (in place of the default variable configuration section), declare a front component with [`defineSettingsFrontComponent()`](/developers/extend/apps/layout/front-components#custom-settings-component) in its own file. Only one is allowed per app. System-managed sections (auto-upgrade, App URL, connections) always remain visible.
|
||||
|
||||
## Variable types
|
||||
|
||||
|
||||
@@ -8,15 +8,17 @@ Front components are React components that render directly inside Twenty's UI. T
|
||||
|
||||
## Where front components can be used
|
||||
|
||||
Front components can render in two locations within Twenty:
|
||||
Front components can render in three locations within Twenty:
|
||||
|
||||
- **Side panel** — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
|
||||
- **Widgets (dashboards and record pages)** — Front components can be embedded as widgets inside [page layouts](/developers/extend/apps/layout/page-layouts). When configuring a dashboard or a record page layout, users can add a front component widget.
|
||||
- **App settings** — Defined with [`defineSettingsFrontComponent()`](#custom-settings-component), the front component renders as a section inside the app's **Settings** tab, in place of the default variable configuration UI.
|
||||
|
||||
A front component on its own isn't reachable from the UI — you need to *surface* it. The two ways to do that are:
|
||||
A front component on its own isn't reachable from the UI — you need to *surface* it. The three ways to do that are:
|
||||
|
||||
- **Pair it with a [command menu item](/developers/extend/apps/layout/command-menu-items)** — registers it in the command menu (Cmd+K) and, optionally, as a pinned quick-action.
|
||||
- **Embed it as a widget in a [page layout](/developers/extend/apps/layout/page-layouts)** — places it on a record's detail page or dashboard.
|
||||
- **Define it with [`defineSettingsFrontComponent()`](#custom-settings-component)** — renders it as a section inside the app's **Settings** tab, in place of the default variable configuration UI.
|
||||
|
||||
## Basic example
|
||||
|
||||
@@ -77,6 +79,34 @@ Click it to render the component inline.
|
||||
|
||||
Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a **page layout**. See [Page Layouts](/developers/extend/apps/layout/page-layouts) for details.
|
||||
|
||||
## Custom settings component
|
||||
|
||||
To replace the auto-generated variable configuration UI in your app's **Settings** tab with your own component, define it with `defineSettingsFrontComponent` instead of `defineFrontComponent`. It takes the same [configuration fields](#configuration-fields) (except `isHeadless`, which is not accepted since a settings component always renders visible UI) and additionally marks the component as the app's settings UI.
|
||||
|
||||
The component renders as a section **inside** the Settings tab, not as a replacement for the whole tab. Twenty's system-managed sections — auto-upgrade, App URL, and connections — always render above it and cannot be overridden by the app.
|
||||
|
||||
```tsx src/front-components/app-settings.tsx
|
||||
import { defineSettingsFrontComponent } from 'twenty-sdk/define';
|
||||
|
||||
const AppSettings = () => {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h2>My app settings</h2>
|
||||
{/* render your own configuration UI here */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default defineSettingsFrontComponent({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'app-settings',
|
||||
description: "Custom UI for the app's Settings tab",
|
||||
component: AppSettings,
|
||||
});
|
||||
```
|
||||
|
||||
Only one settings front component is allowed per app; declaring more than one fails the build. When present, the app's **Settings** tab renders this component in place of the default variable configuration UI.
|
||||
|
||||
## Headless vs non-headless
|
||||
|
||||
Front components come in two rendering modes controlled by the `isHeadless` option:
|
||||
|
||||
@@ -51,6 +51,7 @@ A Twenty app's **layout layer** is everything the user sees: where the app surfa
|
||||
| **Record list** | A saved configuration for an object — visible columns, order, filters, groups | `defineView` |
|
||||
| **Record detail page** | The tabs and widgets on a record page (your own object's, or a standard one) | `definePageLayout`, `definePageLayoutTab` |
|
||||
| **Inside any of the above** | A custom React widget — buttons, forms, dashboards, integrations | `defineFrontComponent` |
|
||||
| **App settings** | A custom configuration section inside the app's Settings tab, in place of the default variables UI | `defineSettingsFrontComponent` |
|
||||
| **Command menu (Cmd+K)** | A pinned quick action or hidden command | `defineCommandMenuItem` |
|
||||
|
||||
Front components run inside an isolated Web Worker using Remote DOM — they render *natively* in the page (not inside an iframe), but cannot reach the host page or DOM directly. Communication with Twenty happens through a message-passing host API.
|
||||
|
||||
@@ -315,7 +315,6 @@ export type Application = {
|
||||
objects: Array<Object>;
|
||||
packageJsonChecksum?: Maybe<Scalars['String']['output']>;
|
||||
packageJsonFileId?: Maybe<Scalars['UUID']['output']>;
|
||||
/** @deprecated Custom settings tabs are no longer supported. This field is ignored. */
|
||||
settingsCustomTabFrontComponentId?: Maybe<Scalars['UUID']['output']>;
|
||||
universalIdentifier: Scalars['String']['output'];
|
||||
version?: Maybe<Scalars['String']['output']>;
|
||||
|
||||
@@ -252,10 +252,14 @@ export const SettingsApplicationDetails = () => {
|
||||
const hasHttpTriggeredFunctions =
|
||||
applicationHasHttpTriggeredFunctions(application);
|
||||
const canShowFunctionDomain = hasHttpTriggeredFunctions;
|
||||
const hasSettingsFrontComponent = isDefined(
|
||||
application?.settingsCustomTabFrontComponentId,
|
||||
);
|
||||
const hasNothingToConfigure =
|
||||
!hasVariables &&
|
||||
!hasConnectionProviders &&
|
||||
!canShowFunctionDomain &&
|
||||
!hasSettingsFrontComponent &&
|
||||
!isUpgradableApplicationSourceType(sourceType);
|
||||
|
||||
return {
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
import { FrontComponentSkeletonLoader } from '@/front-components/components/FrontComponentSkeletonLoader';
|
||||
|
||||
const FrontComponentRenderer = lazy(() =>
|
||||
import('@/front-components/components/FrontComponentRenderer').then(
|
||||
(module) => ({ default: module.FrontComponentRenderer }),
|
||||
),
|
||||
);
|
||||
|
||||
const StyledRendererContainer = styled.div`
|
||||
display: flex;
|
||||
min-height: 400px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type SettingsApplicationCustomSettingsSectionProps = {
|
||||
frontComponentId: string;
|
||||
};
|
||||
|
||||
export const SettingsApplicationCustomSettingsSection = ({
|
||||
frontComponentId,
|
||||
}: SettingsApplicationCustomSettingsSectionProps) => {
|
||||
return (
|
||||
<Section>
|
||||
<StyledRendererContainer>
|
||||
<Suspense fallback={<FrontComponentSkeletonLoader />}>
|
||||
<FrontComponentRenderer
|
||||
frontComponentId={frontComponentId}
|
||||
loadingFallback={<FrontComponentSkeletonLoader />}
|
||||
/>
|
||||
</Suspense>
|
||||
</StyledRendererContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+24
-12
@@ -1,6 +1,8 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type Application } from '~/generated-metadata/graphql';
|
||||
import { useUpdateOneApplicationVariable } from '~/pages/settings/applications/hooks/useUpdateOneApplicationVariable';
|
||||
import { SettingsApplicationConnectionsSection } from '~/pages/settings/applications/tabs/SettingsApplicationConnectionsSection';
|
||||
import { SettingsApplicationCustomSettingsSection } from '~/pages/settings/applications/tabs/SettingsApplicationCustomSettingsSection';
|
||||
import { SettingsApplicationDetailEnvironmentVariablesTable } from '~/pages/settings/applications/tabs/SettingsApplicationDetailEnvironmentVariablesTable';
|
||||
import { SettingsApplicationFunctionDomainSection } from '~/pages/settings/applications/tabs/SettingsApplicationFunctionDomainSection';
|
||||
import { SettingsApplicationGeneralSection } from '~/pages/settings/applications/tabs/SettingsApplicationGeneralSection';
|
||||
@@ -19,6 +21,7 @@ export const SettingsApplicationDetailSettingsTab = ({
|
||||
| 'autoUpgrade'
|
||||
| 'applicationRegistration'
|
||||
| 'logicFunctions'
|
||||
| 'settingsCustomTabFrontComponentId'
|
||||
>;
|
||||
}) => {
|
||||
const { updateOneApplicationVariable } = useUpdateOneApplicationVariable();
|
||||
@@ -34,6 +37,9 @@ export const SettingsApplicationDetailSettingsTab = ({
|
||||
application?.applicationRegistration?.sourceType,
|
||||
);
|
||||
|
||||
const settingsFrontComponentId =
|
||||
application?.settingsCustomTabFrontComponentId;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isUpgradable && application?.id && (
|
||||
@@ -50,18 +56,24 @@ export const SettingsApplicationDetailSettingsTab = ({
|
||||
{application?.id && (
|
||||
<SettingsApplicationConnectionsSection applicationId={application.id} />
|
||||
)}
|
||||
<SettingsApplicationDetailEnvironmentVariablesTable
|
||||
envVariables={envVariables}
|
||||
onUpdate={({ key, value }) =>
|
||||
application?.id
|
||||
? updateOneApplicationVariable({
|
||||
key,
|
||||
value,
|
||||
applicationId: application.id,
|
||||
})
|
||||
: null
|
||||
}
|
||||
/>
|
||||
{isDefined(settingsFrontComponentId) ? (
|
||||
<SettingsApplicationCustomSettingsSection
|
||||
frontComponentId={settingsFrontComponentId}
|
||||
/>
|
||||
) : (
|
||||
<SettingsApplicationDetailEnvironmentVariablesTable
|
||||
envVariables={envVariables}
|
||||
onUpdate={({ key, value }) =>
|
||||
application?.id
|
||||
? updateOneApplicationVariable({
|
||||
key,
|
||||
value,
|
||||
applicationId: application.id,
|
||||
})
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
@@ -84,6 +84,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
|
||||
"definePostInstallLogicFunction",
|
||||
"definePreInstallLogicFunction",
|
||||
"defineRole",
|
||||
"defineSettingsFrontComponent",
|
||||
"defineSkill",
|
||||
"defineUninstallLogicFunction",
|
||||
"defineView",
|
||||
|
||||
+26
@@ -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;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export type ApplicationConfig = Omit<
|
||||
| 'requiredServerVersionRange'
|
||||
| 'postInstallLogicFunction'
|
||||
| 'preInstallLogicFunction'
|
||||
| 'settingsFrontComponent'
|
||||
| 'defaultRoleUniversalIdentifier'
|
||||
| 'aboutDescription'
|
||||
> & {
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { defineSettingsFrontComponent } from '@/sdk/define';
|
||||
|
||||
const MockComponent = () => null;
|
||||
|
||||
describe('defineSettingsFrontComponent', () => {
|
||||
const validConfig = {
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'App Settings',
|
||||
component: MockComponent,
|
||||
};
|
||||
|
||||
it('should return successful validation result when valid', () => {
|
||||
const result = defineSettingsFrontComponent(validConfig);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.config).toEqual(validConfig);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return error when universalIdentifier is missing', () => {
|
||||
const config = {
|
||||
name: 'App Settings',
|
||||
component: MockComponent,
|
||||
};
|
||||
|
||||
const result = defineSettingsFrontComponent(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Settings front component must have a universalIdentifier',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when component is missing', () => {
|
||||
const config = {
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'App Settings',
|
||||
};
|
||||
|
||||
const result = defineSettingsFrontComponent(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toEqual([
|
||||
'Settings front component must have a component',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return error when component is not a function', () => {
|
||||
const config = {
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'App Settings',
|
||||
component: 'not-a-function',
|
||||
};
|
||||
|
||||
const result = defineSettingsFrontComponent(config as any);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
'Settings front component component must be a React component',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createValidationResult } from '@/sdk/define/common/utils/create-validation-result';
|
||||
import type { DefineEntity } from '@/sdk/define/common/types/define-entity.type';
|
||||
import { type SettingsFrontComponentConfig } from '@/sdk/define/front-component/settings-front-component-config';
|
||||
|
||||
export const defineSettingsFrontComponent: DefineEntity<
|
||||
SettingsFrontComponentConfig
|
||||
> = (config) => {
|
||||
const errors = [];
|
||||
|
||||
if (!config.universalIdentifier) {
|
||||
errors.push('Settings front component must have a universalIdentifier');
|
||||
}
|
||||
|
||||
if (!config.component) {
|
||||
errors.push('Settings front component must have a component');
|
||||
} else if (typeof config.component !== 'function') {
|
||||
errors.push('Settings front component component must be a React component');
|
||||
}
|
||||
|
||||
return createValidationResult({
|
||||
config,
|
||||
errors,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { type FrontComponentConfig } from '@/sdk/define/front-component/front-component-config';
|
||||
|
||||
// A settings front component always renders visible UI, so `isHeadless`
|
||||
// is not configurable.
|
||||
export type SettingsFrontComponentConfig = Omit<
|
||||
FrontComponentConfig,
|
||||
'isHeadless'
|
||||
>;
|
||||
@@ -70,10 +70,12 @@ export {
|
||||
} from '@/sdk/define/conditional-availability/conditional-availability-variables';
|
||||
|
||||
export { defineFrontComponent } from '@/sdk/define/front-component/define-front-component';
|
||||
export { defineSettingsFrontComponent } from '@/sdk/define/front-component/define-settings-front-component';
|
||||
export type {
|
||||
FrontComponentConfig,
|
||||
FrontComponentType,
|
||||
} from '@/sdk/define/front-component/front-component-config';
|
||||
export type { SettingsFrontComponentConfig } from '@/sdk/define/front-component/settings-front-component-config';
|
||||
|
||||
export { defineIndex } from '@/sdk/define/indexes/define-index';
|
||||
export type { IndexConfig } from '@/sdk/define/indexes/index-config';
|
||||
|
||||
+33
-10
@@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -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,
|
||||
|
||||
+6
@@ -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()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type PostInstallLogicFunctionApplicationManifest } from '@/application/postInstallLogicFunctionApplicationType';
|
||||
import { type PreInstallLogicFunctionApplicationManifest } from '@/application/preInstallLogicFunctionApplicationType';
|
||||
import { type SettingsFrontComponentApplicationManifest } from '@/application/settingsFrontComponentApplicationType';
|
||||
import { type UninstallLogicFunctionApplicationManifest } from '@/application/uninstallLogicFunctionApplicationType';
|
||||
import { type ApplicationCategory } from './applicationCategoryType';
|
||||
import { type ApplicationVariables } from './applicationVariablesType';
|
||||
@@ -32,10 +33,10 @@ export type ApplicationManifest = SyncableEntityOptions & {
|
||||
postInstallLogicFunction?: PostInstallLogicFunctionApplicationManifest;
|
||||
preInstallLogicFunction?: PreInstallLogicFunctionApplicationManifest;
|
||||
uninstallLogicFunction?: UninstallLogicFunctionApplicationManifest;
|
||||
settingsFrontComponent?: SettingsFrontComponentApplicationManifest;
|
||||
/**
|
||||
* @deprecated Custom settings tabs are no longer supported. This property is
|
||||
* kept for backward compatibility with older manifests but is now ignored.
|
||||
* Use typed `applicationVariables` / `serverVariables` instead.
|
||||
* @deprecated Use `defineSettingsFrontComponent()` (exposed on the manifest
|
||||
* as `settingsFrontComponent`) instead. This property is ignored.
|
||||
*/
|
||||
settingsCustomTabFrontComponentUniversalIdentifier?: string;
|
||||
packageJsonChecksum: string | null;
|
||||
|
||||
@@ -135,6 +135,7 @@ export type { RunAgentInput, RunAgentResult } from './runAgentType';
|
||||
export type { ServerVariables } from './server-variables.type';
|
||||
export type { ServerRouteDispatchResult } from './serverRouteDispatchResultType';
|
||||
export type { ServerRouteTriggerSettings } from './serverRouteTriggerSettingsType';
|
||||
export type { SettingsFrontComponentApplicationManifest } from './settingsFrontComponentApplicationType';
|
||||
export type { SkillManifest } from './skillManifestType';
|
||||
export type { StoredOAuthConnectionProviderConfig } from './storedOAuthConnectionProviderConfigType';
|
||||
export type { SyncableEntityOptions } from './syncableEntityOptionsType';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type SettingsFrontComponentApplicationManifest = {
|
||||
universalIdentifier: string;
|
||||
};
|
||||
Reference in New Issue
Block a user