From 9cdd0fdac0ba709df143ac3150d06df8a8397665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Tue, 27 May 2025 09:04:47 +0200 Subject: [PATCH] Revert "Client config not render blocking (#12300)" (#12302) This reverts commit 4ce7fc69878859e9ea3c65b6671b35f852326ee1, to take more time to address PR comments --- .../utils/__tests__/downloadFile.test.ts | 6 + .../components/ClientConfigProvider.tsx | 17 +- .../components/ClientConfigProviderEffect.tsx | 12 +- .../client-config/hooks/useClientConfig.ts | 43 --- .../utils/__tests__/clientConfigUtils.test.ts | 163 ----------- .../client-config/utils/clientConfigUtils.ts | 38 --- .../hooks/useConfigVariableActions.ts | 10 + .../twenty-front/src/testing/graphqlMocks.ts | 3 - .../src/testing/mock-data/config.ts | 7 + .../captcha/interfaces/captcha.interface.ts | 1 + .../client-config.controller.spec.ts | 96 ------- .../client-config/client-config.controller.ts | 14 - .../client-config/client-config.module.ts | 6 +- .../client-config.resolver.spec.ts | 13 +- .../client-config/client-config.resolver.ts | 98 ++++++- .../services/client-config.service.spec.ts | 252 ------------------ .../services/client-config.service.ts | 144 ---------- packages/twenty-ui/src/utilities/index.ts | 1 - 18 files changed, 148 insertions(+), 776 deletions(-) delete mode 100644 packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts delete mode 100644 packages/twenty-front/src/modules/client-config/utils/__tests__/clientConfigUtils.test.ts delete mode 100644 packages/twenty-front/src/modules/client-config/utils/clientConfigUtils.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts diff --git a/packages/twenty-front/src/modules/activities/files/utils/__tests__/downloadFile.test.ts b/packages/twenty-front/src/modules/activities/files/utils/__tests__/downloadFile.test.ts index ecc8c9aa1..2261b584e 100644 --- a/packages/twenty-front/src/modules/activities/files/utils/__tests__/downloadFile.test.ts +++ b/packages/twenty-front/src/modules/activities/files/utils/__tests__/downloadFile.test.ts @@ -1,5 +1,6 @@ import { downloadFile } from '../downloadFile'; +// Mock fetch global.fetch = jest.fn(() => Promise.resolve({ status: 200, @@ -15,10 +16,13 @@ window.URL.revokeObjectURL = jest.fn(); // `global.fetch` and `window.fetch` are also undefined describe.skip('downloadFile', () => { it('should download a file', () => { + // Call downloadFile downloadFile('url/to/file.pdf', 'file.pdf'); + // Assert on fetch expect(fetch).toHaveBeenCalledWith('url/to/file.pdf'); + // Assert on element creation const link = document.querySelector( 'a[href="mock-url"][download="file.pdf"]', ); @@ -28,8 +32,10 @@ describe.skip('downloadFile', () => { // @ts-ignore expect(link?.style?.display).toBe('none'); + // Assert on element click expect(link).toHaveBeenCalledTimes(1); + // Clean up mocks jest.clearAllMocks(); }); }); diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx index 774bb2361..b0a7346cf 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx @@ -2,11 +2,26 @@ import { useRecoilValue } from 'recoil'; import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback'; +import { AppPath } from '@/types/AppPath'; +import { useLocation } from 'react-router-dom'; +import { isMatchingLocation } from '~/utils/isMatchingLocation'; export const ClientConfigProvider: React.FC = ({ children, }) => { - const { isErrored, error } = useRecoilValue(clientConfigApiStatusState); + const { isLoaded, isErrored, error } = useRecoilValue( + clientConfigApiStatusState, + ); + + const location = useLocation(); + + // TODO: Implement a better loading strategy + if ( + !isLoaded && + !isMatchingLocation(location, AppPath.Verify) && + !isMatchingLocation(location, AppPath.VerifyEmail) + ) + return null; return isErrored && error instanceof Error ? ( { const setIsDebugMode = useSetRecoilState(isDebugModeState); @@ -87,13 +87,9 @@ export const ClientConfigProviderEffect = () => { isConfigVariablesInDbEnabledState, ); - const { data, loading, error, fetchClientConfig } = useClientConfig(); - - useEffect(() => { - if (!clientConfigApiStatus.isLoaded) { - fetchClientConfig(); - } - }, [clientConfigApiStatus.isLoaded, fetchClientConfig]); + const { data, loading, error } = useGetClientConfigQuery({ + skip: clientConfigApiStatus.isLoaded, + }); useEffect(() => { if (loading) return; diff --git a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts deleted file mode 100644 index 344220c1b..000000000 --- a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useCallback, useState } from 'react'; -import { ClientConfig } from '~/generated/graphql'; -import { getClientConfig } from '../utils/clientConfigUtils'; - -interface UseClientConfigResult { - data: { clientConfig: ClientConfig } | undefined; - loading: boolean; - error: Error | undefined; - fetchClientConfig: () => Promise; - refetchClientConfig: () => Promise; -} - -export const useClientConfig = (): UseClientConfigResult => { - const [data, setData] = useState<{ clientConfig: ClientConfig } | undefined>( - undefined, - ); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(undefined); - - const fetchClientConfig = useCallback(async () => { - setLoading(true); - setError(undefined); - - try { - const clientConfig = await getClientConfig(); - setData({ clientConfig }); - } catch (err) { - setError( - err instanceof Error ? err : new Error('Failed to fetch client config'), - ); - } finally { - setLoading(false); - } - }, []); - - return { - data, - loading, - error, - fetchClientConfig, - refetchClientConfig: fetchClientConfig, - }; -}; diff --git a/packages/twenty-front/src/modules/client-config/utils/__tests__/clientConfigUtils.test.ts b/packages/twenty-front/src/modules/client-config/utils/__tests__/clientConfigUtils.test.ts deleted file mode 100644 index 46fd5e121..000000000 --- a/packages/twenty-front/src/modules/client-config/utils/__tests__/clientConfigUtils.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { - clearClientConfigCache, - getClientConfig, - refreshClientConfig, -} from '../clientConfigUtils'; - -import { REACT_APP_SERVER_BASE_URL } from '~/config'; - -global.fetch = jest.fn(); - -const mockClientConfig = { - billing: { - isBillingEnabled: true, - billingUrl: 'https://billing.example.com', - trialPeriods: [], - }, - authProviders: { - google: true, - magicLink: false, - password: true, - microsoft: false, - sso: [], - }, - signInPrefilled: false, - isMultiWorkspaceEnabled: true, - isEmailVerificationRequired: false, - defaultSubdomain: 'app', - frontDomain: 'localhost', - debugMode: true, - support: { - supportDriver: 'none', - supportFrontChatId: undefined, - }, - sentry: { - environment: 'development', - release: '1.0.0', - dsn: undefined, - }, - captcha: { - provider: undefined, - siteKey: undefined, - }, - chromeExtensionId: undefined, - api: { - mutationMaximumAffectedRecords: 100, - }, - isAttachmentPreviewEnabled: true, - analyticsEnabled: false, - canManageFeatureFlags: true, - publicFeatureFlags: [], - isMicrosoftMessagingEnabled: false, - isMicrosoftCalendarEnabled: false, - isGoogleMessagingEnabled: false, - isGoogleCalendarEnabled: false, - isConfigVariablesInDbEnabled: false, -}; - -describe('clientConfigUtils', () => { - beforeEach(() => { - clearClientConfigCache(); - jest.clearAllMocks(); - }); - - afterEach(() => { - clearClientConfigCache(); - }); - - describe('getClientConfig', () => { - it('should fetch client config from API', async () => { - (fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: async () => mockClientConfig, - }); - - const result = await getClientConfig(); - - expect(fetch).toHaveBeenCalledWith( - REACT_APP_SERVER_BASE_URL + '/client-config', - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - expect(result).toEqual(mockClientConfig); - }); - - it('should cache the result', async () => { - (fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: async () => mockClientConfig, - }); - - // First call - await getClientConfig(); - - // Second call should use cache - const result = await getClientConfig(); - - expect(fetch).toHaveBeenCalledTimes(1); - expect(result).toEqual(mockClientConfig); - }); - - it('should handle fetch errors', async () => { - (fetch as jest.Mock).mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Internal Server Error', - }); - - await expect(getClientConfig()).rejects.toThrow( - 'Failed to fetch client config: 500 Internal Server Error', - ); - }); - }); - - describe('refreshClientConfig', () => { - it('should clear cache and fetch fresh data', async () => { - // First call to populate cache - (fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: async () => mockClientConfig, - }); - await getClientConfig(); - - // Mock a different response for refresh - const updatedConfig = { ...mockClientConfig, debugMode: false }; - (fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: async () => updatedConfig, - }); - - const result = await refreshClientConfig(); - - expect(fetch).toHaveBeenCalledTimes(2); - expect(result).toEqual(updatedConfig); - }); - }); - - describe('clearClientConfigCache', () => { - it('should clear the cache', async () => { - // Populate cache - (fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: async () => mockClientConfig, - }); - await getClientConfig(); - - // Clear cache - clearClientConfigCache(); - - // Next call should fetch again - (fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - json: async () => mockClientConfig, - }); - await getClientConfig(); - - expect(fetch).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/packages/twenty-front/src/modules/client-config/utils/clientConfigUtils.ts b/packages/twenty-front/src/modules/client-config/utils/clientConfigUtils.ts deleted file mode 100644 index 382329c6e..000000000 --- a/packages/twenty-front/src/modules/client-config/utils/clientConfigUtils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ClientConfig } from '~/generated/graphql'; - -import { REACT_APP_SERVER_BASE_URL } from '~/config'; - -let cachedClientConfig: ClientConfig | null = null; - -export const getClientConfig = async (): Promise => { - if (cachedClientConfig !== null) { - return cachedClientConfig; - } - - const response = await fetch(`${REACT_APP_SERVER_BASE_URL}/client-config`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - throw new Error( - `Failed to fetch client config: ${response.status} ${response.statusText}`, - ); - } - - const clientConfig: ClientConfig = await response.json(); - cachedClientConfig = clientConfig; - - return clientConfig; -}; - -export const refreshClientConfig = async (): Promise => { - cachedClientConfig = null; - return getClientConfig(); -}; - -export const clearClientConfigCache = (): void => { - cachedClientConfig = null; -}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts index 40f987ca4..bf17bd91e 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts @@ -1,5 +1,6 @@ import { useLingui } from '@lingui/react/macro'; +import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig'; import { GET_DATABASE_CONFIG_VARIABLE } from '@/settings/admin-panel/config-variables/graphql/queries/getDatabaseConfigVariable'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; @@ -47,6 +48,9 @@ export const useConfigVariableActions = (variableName: string) => { query: GET_DATABASE_CONFIG_VARIABLE, variables: { key: variableName }, }, + { + query: GET_CLIENT_CONFIG, + }, ], }); } else { @@ -60,6 +64,9 @@ export const useConfigVariableActions = (variableName: string) => { query: GET_DATABASE_CONFIG_VARIABLE, variables: { key: variableName }, }, + { + query: GET_CLIENT_CONFIG, + }, ], }); } @@ -89,6 +96,9 @@ export const useConfigVariableActions = (variableName: string) => { query: GET_DATABASE_CONFIG_VARIABLE, variables: { key: variableName }, }, + { + query: GET_CLIENT_CONFIG, + }, ], }); } catch (error) { diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index eb672713f..08af596ee 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -124,9 +124,6 @@ export const graphqlMocks = { }, }); }), - http.get(`${REACT_APP_SERVER_BASE_URL}/client-config`, () => { - return HttpResponse.json(mockedClientConfig); - }), metadataGraphql.query( getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? '', () => { diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index 4df865abd..585185f13 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -10,6 +10,7 @@ export const mockedClientConfig: ClientConfig = { password: true, microsoft: false, sso: [], + __typename: 'AuthProviders', }, frontDomain: 'localhost', defaultSubdomain: 'app', @@ -19,29 +20,35 @@ export const mockedClientConfig: ClientConfig = { support: { supportDriver: 'front', supportFrontChatId: null, + __typename: 'Support', }, sentry: { dsn: 'MOCKED_DSN', release: 'MOCKED_RELEASE', environment: 'MOCKED_ENVIRONMENT', + __typename: 'Sentry', }, billing: { isBillingEnabled: true, billingUrl: '', trialPeriods: [ { + __typename: 'BillingTrialPeriodDTO', duration: 30, isCreditCardRequired: true, }, { + __typename: 'BillingTrialPeriodDTO', duration: 7, isCreditCardRequired: false, }, ], + __typename: 'Billing', }, captcha: { provider: CaptchaDriverType.GoogleRecaptcha, siteKey: 'MOCKED_SITE_KEY', + __typename: 'Captcha', }, api: { mutationMaximumAffectedRecords: 100 }, canManageFeatureFlags: true, diff --git a/packages/twenty-server/src/engine/core-modules/captcha/interfaces/captcha.interface.ts b/packages/twenty-server/src/engine/core-modules/captcha/interfaces/captcha.interface.ts index 6688c084a..f6f61b814 100644 --- a/packages/twenty-server/src/engine/core-modules/captcha/interfaces/captcha.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/captcha/interfaces/captcha.interface.ts @@ -36,4 +36,5 @@ export type CaptchaModuleAsyncOptions = { ) => CaptchaModuleOptions | Promise | undefined; } & Pick & Pick; + export type CaptchaValidateResult = { success: boolean; error?: string }; diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts deleted file mode 100644 index 4686adf32..000000000 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service'; - -import { ClientConfigController } from './client-config.controller'; - -describe('ClientConfigController', () => { - let controller: ClientConfigController; - let clientConfigService: ClientConfigService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [ClientConfigController], - providers: [ - { - provide: ClientConfigService, - useValue: { - getClientConfig: jest.fn(), - }, - }, - ], - }).compile(); - - controller = module.get(ClientConfigController); - clientConfigService = module.get(ClientConfigService); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('getClientConfig', () => { - it('should return client config from service', async () => { - const mockClientConfig = { - billing: { - isBillingEnabled: true, - billingUrl: 'https://billing.example.com', - trialPeriods: [ - { - duration: 7, - isCreditCardRequired: false, - }, - ], - }, - authProviders: { - google: true, - magicLink: false, - password: true, - microsoft: false, - sso: [], - }, - signInPrefilled: false, - isMultiWorkspaceEnabled: true, - isEmailVerificationRequired: false, - defaultSubdomain: 'app', - frontDomain: 'localhost', - debugMode: true, - support: { - supportDriver: 'none', - supportFrontChatId: undefined, - }, - sentry: { - environment: 'development', - release: '1.0.0', - dsn: undefined, - }, - captcha: { - provider: undefined, - siteKey: undefined, - }, - chromeExtensionId: undefined, - api: { - mutationMaximumAffectedRecords: 100, - }, - isAttachmentPreviewEnabled: true, - analyticsEnabled: false, - canManageFeatureFlags: true, - publicFeatureFlags: [], - isMicrosoftMessagingEnabled: false, - isMicrosoftCalendarEnabled: false, - isGoogleMessagingEnabled: false, - isGoogleCalendarEnabled: false, - isConfigVariablesInDbEnabled: false, - }; - - jest - .spyOn(clientConfigService, 'getClientConfig') - .mockResolvedValue(mockClientConfig); - - const result = await controller.getClientConfig(); - - expect(clientConfigService.getClientConfig).toHaveBeenCalled(); - expect(result).toEqual(mockClientConfig); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.ts deleted file mode 100644 index fa619d512..000000000 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.controller.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -import { ClientConfig } from 'src/engine/core-modules/client-config/client-config.entity'; -import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service'; - -@Controller('/client-config') -export class ClientConfigController { - constructor(private readonly clientConfigService: ClientConfigService) {} - - @Get() - async getClientConfig(): Promise { - return this.clientConfigService.getClientConfig(); - } -} diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.module.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.module.ts index 63f7d0d8b..70f57128f 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.module.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.module.ts @@ -2,14 +2,10 @@ import { Module } from '@nestjs/common'; import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; -import { ClientConfigController } from './client-config.controller'; import { ClientConfigResolver } from './client-config.resolver'; -import { ClientConfigService } from './services/client-config.service'; - @Module({ imports: [DomainManagerModule], - controllers: [ClientConfigController], - providers: [ClientConfigResolver, ClientConfigService], + providers: [ClientConfigResolver], }) export class ClientConfigModule {} diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.spec.ts index ae1774cac..e67b141c0 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { ClientConfigResolver } from './client-config.resolver'; @@ -12,10 +13,12 @@ describe('ClientConfigResolver', () => { providers: [ ClientConfigResolver, { - provide: ClientConfigService, - useValue: { - getClientConfig: jest.fn(), - }, + provide: TwentyConfigService, + useValue: {}, + }, + { + provide: DomainManagerService, + useValue: {}, }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index 37ab9f7de..91bc864e0 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -1,15 +1,107 @@ import { Query, Resolver } from '@nestjs/graphql'; -import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service'; +import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; + +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { ClientConfig } from './client-config.entity'; @Resolver() export class ClientConfigResolver { - constructor(private clientConfigService: ClientConfigService) {} + constructor( + private twentyConfigService: TwentyConfigService, + private domainManagerService: DomainManagerService, + ) {} @Query(() => ClientConfig) async clientConfig(): Promise { - return this.clientConfigService.getClientConfig(); + const clientConfig: ClientConfig = { + billing: { + isBillingEnabled: this.twentyConfigService.get('IS_BILLING_ENABLED'), + billingUrl: this.twentyConfigService.get('BILLING_PLAN_REQUIRED_LINK'), + trialPeriods: [ + { + duration: this.twentyConfigService.get( + 'BILLING_FREE_TRIAL_WITH_CREDIT_CARD_DURATION_IN_DAYS', + ), + isCreditCardRequired: true, + }, + { + duration: this.twentyConfigService.get( + 'BILLING_FREE_TRIAL_WITHOUT_CREDIT_CARD_DURATION_IN_DAYS', + ), + isCreditCardRequired: false, + }, + ], + }, + authProviders: { + google: this.twentyConfigService.get('AUTH_GOOGLE_ENABLED'), + magicLink: false, + password: this.twentyConfigService.get('AUTH_PASSWORD_ENABLED'), + microsoft: this.twentyConfigService.get('AUTH_MICROSOFT_ENABLED'), + sso: [], + }, + signInPrefilled: this.twentyConfigService.get('SIGN_IN_PREFILLED'), + isMultiWorkspaceEnabled: this.twentyConfigService.get( + 'IS_MULTIWORKSPACE_ENABLED', + ), + isEmailVerificationRequired: this.twentyConfigService.get( + 'IS_EMAIL_VERIFICATION_REQUIRED', + ), + defaultSubdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'), + frontDomain: this.domainManagerService.getFrontUrl().hostname, + debugMode: + this.twentyConfigService.get('NODE_ENV') === + NodeEnvironment.development, + support: { + supportDriver: this.twentyConfigService.get('SUPPORT_DRIVER'), + supportFrontChatId: this.twentyConfigService.get( + 'SUPPORT_FRONT_CHAT_ID', + ), + }, + sentry: { + environment: this.twentyConfigService.get('SENTRY_ENVIRONMENT'), + release: this.twentyConfigService.get('APP_VERSION'), + dsn: this.twentyConfigService.get('SENTRY_FRONT_DSN'), + }, + captcha: { + provider: this.twentyConfigService.get('CAPTCHA_DRIVER'), + siteKey: this.twentyConfigService.get('CAPTCHA_SITE_KEY'), + }, + chromeExtensionId: this.twentyConfigService.get('CHROME_EXTENSION_ID'), + api: { + mutationMaximumAffectedRecords: this.twentyConfigService.get( + 'MUTATION_MAXIMUM_AFFECTED_RECORDS', + ), + }, + isAttachmentPreviewEnabled: this.twentyConfigService.get( + 'IS_ATTACHMENT_PREVIEW_ENABLED', + ), + analyticsEnabled: this.twentyConfigService.get('ANALYTICS_ENABLED'), + canManageFeatureFlags: + this.twentyConfigService.get('NODE_ENV') === + NodeEnvironment.development || + this.twentyConfigService.get('IS_BILLING_ENABLED'), + publicFeatureFlags: PUBLIC_FEATURE_FLAGS, + isMicrosoftMessagingEnabled: this.twentyConfigService.get( + 'MESSAGING_PROVIDER_MICROSOFT_ENABLED', + ), + isMicrosoftCalendarEnabled: this.twentyConfigService.get( + 'CALENDAR_PROVIDER_MICROSOFT_ENABLED', + ), + isGoogleMessagingEnabled: this.twentyConfigService.get( + 'MESSAGING_PROVIDER_GMAIL_ENABLED', + ), + isGoogleCalendarEnabled: this.twentyConfigService.get( + 'CALENDAR_PROVIDER_GOOGLE_ENABLED', + ), + isConfigVariablesInDbEnabled: this.twentyConfigService.get( + 'IS_CONFIG_VARIABLES_IN_DB_ENABLED', + ), + }; + + return Promise.resolve(clientConfig); } } diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts deleted file mode 100644 index becdcadf5..000000000 --- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; -import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface'; - -import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces'; -import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; -import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const'; -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; - -describe('ClientConfigService', () => { - let service: ClientConfigService; - let twentyConfigService: TwentyConfigService; - let domainManagerService: DomainManagerService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ClientConfigService, - { - provide: TwentyConfigService, - useValue: { - get: jest.fn(), - }, - }, - { - provide: DomainManagerService, - useValue: { - getFrontUrl: jest.fn(), - }, - }, - ], - }).compile(); - - service = module.get(ClientConfigService); - twentyConfigService = module.get(TwentyConfigService); - domainManagerService = - module.get(DomainManagerService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('getClientConfig', () => { - beforeEach(() => { - // Setup default mock values - jest - .spyOn(twentyConfigService, 'get') - .mockImplementation((key: string) => { - const mockValues: Record = { - IS_BILLING_ENABLED: true, - BILLING_PLAN_REQUIRED_LINK: 'https://billing.example.com', - BILLING_FREE_TRIAL_WITH_CREDIT_CARD_DURATION_IN_DAYS: 30, - BILLING_FREE_TRIAL_WITHOUT_CREDIT_CARD_DURATION_IN_DAYS: 7, - AUTH_GOOGLE_ENABLED: true, - AUTH_PASSWORD_ENABLED: true, - AUTH_MICROSOFT_ENABLED: false, - SIGN_IN_PREFILLED: false, - IS_MULTIWORKSPACE_ENABLED: true, - IS_EMAIL_VERIFICATION_REQUIRED: true, - DEFAULT_SUBDOMAIN: 'app', - NODE_ENV: NodeEnvironment.development, - SUPPORT_DRIVER: SupportDriver.Front, - SUPPORT_FRONT_CHAT_ID: 'chat-123', - SENTRY_ENVIRONMENT: 'development', - APP_VERSION: '1.0.0', - SENTRY_FRONT_DSN: 'https://sentry.example.com', - CAPTCHA_DRIVER: CaptchaDriverType.GoogleRecaptcha, - CAPTCHA_SITE_KEY: 'site-key-123', - CHROME_EXTENSION_ID: 'extension-123', - MUTATION_MAXIMUM_AFFECTED_RECORDS: 1000, - IS_ATTACHMENT_PREVIEW_ENABLED: true, - ANALYTICS_ENABLED: true, - MESSAGING_PROVIDER_MICROSOFT_ENABLED: false, - CALENDAR_PROVIDER_MICROSOFT_ENABLED: false, - MESSAGING_PROVIDER_GMAIL_ENABLED: true, - CALENDAR_PROVIDER_GOOGLE_ENABLED: true, - IS_CONFIG_VARIABLES_IN_DB_ENABLED: false, - }; - - return mockValues[key]; - }); - - jest.spyOn(domainManagerService, 'getFrontUrl').mockReturnValue({ - hostname: 'app.twenty.com', - } as URL); - }); - - it('should return complete client config with all properties', async () => { - const result = await service.getClientConfig(); - - expect(result).toEqual({ - billing: { - isBillingEnabled: true, - billingUrl: 'https://billing.example.com', - trialPeriods: [ - { - duration: 30, - isCreditCardRequired: true, - }, - { - duration: 7, - isCreditCardRequired: false, - }, - ], - }, - authProviders: { - google: true, - magicLink: false, - password: true, - microsoft: false, - sso: [], - }, - signInPrefilled: false, - isMultiWorkspaceEnabled: true, - isEmailVerificationRequired: true, - defaultSubdomain: 'app', - frontDomain: 'app.twenty.com', - debugMode: true, - support: { - supportDriver: 'Front', - supportFrontChatId: 'chat-123', - }, - sentry: { - environment: 'development', - release: '1.0.0', - dsn: 'https://sentry.example.com', - }, - captcha: { - provider: 'GoogleRecaptcha', - siteKey: 'site-key-123', - }, - chromeExtensionId: 'extension-123', - api: { - mutationMaximumAffectedRecords: 1000, - }, - isAttachmentPreviewEnabled: true, - analyticsEnabled: true, - canManageFeatureFlags: true, - publicFeatureFlags: PUBLIC_FEATURE_FLAGS, - isMicrosoftMessagingEnabled: false, - isMicrosoftCalendarEnabled: false, - isGoogleMessagingEnabled: true, - isGoogleCalendarEnabled: true, - isConfigVariablesInDbEnabled: false, - }); - }); - - it('should handle production environment correctly', async () => { - jest - .spyOn(twentyConfigService, 'get') - .mockImplementation((key: string) => { - if (key === 'NODE_ENV') return NodeEnvironment.production; - if (key === 'IS_BILLING_ENABLED') return false; - - return undefined; - }); - - const result = await service.getClientConfig(); - - expect(result.debugMode).toBe(false); - expect(result.canManageFeatureFlags).toBe(false); - }); - - it('should handle missing captcha driver', async () => { - jest - .spyOn(twentyConfigService, 'get') - .mockImplementation((key: string) => { - if (key === 'CAPTCHA_DRIVER') return undefined; - if (key === 'CAPTCHA_SITE_KEY') return 'site-key'; - - return undefined; - }); - - const result = await service.getClientConfig(); - - expect(result.captcha.provider).toBeUndefined(); - expect(result.captcha.siteKey).toBe('site-key'); - }); - - it('should handle missing support driver', async () => { - jest - .spyOn(twentyConfigService, 'get') - .mockImplementation((key: string) => { - if (key === 'SUPPORT_DRIVER') return undefined; - - return undefined; - }); - - const result = await service.getClientConfig(); - - expect(result.support.supportDriver).toBe(SupportDriver.None); - }); - - it('should handle billing enabled with feature flags', async () => { - jest - .spyOn(twentyConfigService, 'get') - .mockImplementation((key: string) => { - if (key === 'NODE_ENV') return NodeEnvironment.production; - if (key === 'IS_BILLING_ENABLED') return true; - - return undefined; - }); - - const result = await service.getClientConfig(); - - expect(result.canManageFeatureFlags).toBe(true); - }); - }); - - describe('transformEnum', () => { - it('should transform enum by direct key match', () => { - const result = (service as any).transformEnum( - 'GoogleRecaptcha', - CaptchaDriverType, - ); - - expect(result).toBe(CaptchaDriverType.GoogleRecaptcha); - }); - - it('should transform enum by value match', () => { - const result = (service as any).transformEnum( - 'google-recaptcha', - CaptchaDriverType, - ); - - expect(result).toBe('GoogleRecaptcha'); - }); - - it('should transform SupportDriver enum correctly', () => { - const result = (service as any).transformEnum('front', SupportDriver); - - expect(result).toBe('Front'); - }); - - it('should throw error for unknown enum value', () => { - expect(() => { - (service as any).transformEnum('unknown-value', CaptchaDriverType); - }).toThrow( - 'Unknown enum value: unknown-value. Available keys: GoogleRecaptcha, Turnstile. Available values: google-recaptcha, turnstile', - ); - }); - - it('should handle direct key match for SupportDriver', () => { - const result = (service as any).transformEnum('Front', SupportDriver); - - expect(result).toBe(SupportDriver.Front); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts deleted file mode 100644 index c8e5a3e64..000000000 --- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; -import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface'; - -import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces'; -import { ClientConfig } from 'src/engine/core-modules/client-config/client-config.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; -import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const'; -import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; - -@Injectable() -export class ClientConfigService { - constructor( - private twentyConfigService: TwentyConfigService, - private domainManagerService: DomainManagerService, - ) {} - - async getClientConfig(): Promise { - const captchaProvider = this.twentyConfigService.get('CAPTCHA_DRIVER'); - const supportDriver = this.twentyConfigService.get('SUPPORT_DRIVER'); - - const clientConfig: ClientConfig = { - billing: { - isBillingEnabled: this.twentyConfigService.get('IS_BILLING_ENABLED'), - billingUrl: this.twentyConfigService.get('BILLING_PLAN_REQUIRED_LINK'), - trialPeriods: [ - { - duration: this.twentyConfigService.get( - 'BILLING_FREE_TRIAL_WITH_CREDIT_CARD_DURATION_IN_DAYS', - ), - isCreditCardRequired: true, - }, - { - duration: this.twentyConfigService.get( - 'BILLING_FREE_TRIAL_WITHOUT_CREDIT_CARD_DURATION_IN_DAYS', - ), - isCreditCardRequired: false, - }, - ], - }, - authProviders: { - google: this.twentyConfigService.get('AUTH_GOOGLE_ENABLED'), - magicLink: false, - password: this.twentyConfigService.get('AUTH_PASSWORD_ENABLED'), - microsoft: this.twentyConfigService.get('AUTH_MICROSOFT_ENABLED'), - sso: [], - }, - signInPrefilled: this.twentyConfigService.get('SIGN_IN_PREFILLED'), - isMultiWorkspaceEnabled: this.twentyConfigService.get( - 'IS_MULTIWORKSPACE_ENABLED', - ), - isEmailVerificationRequired: this.twentyConfigService.get( - 'IS_EMAIL_VERIFICATION_REQUIRED', - ), - defaultSubdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'), - frontDomain: this.domainManagerService.getFrontUrl().hostname, - debugMode: - this.twentyConfigService.get('NODE_ENV') === - NodeEnvironment.development, - support: { - supportDriver: supportDriver - ? this.transformEnum(supportDriver, SupportDriver) - : SupportDriver.None, - supportFrontChatId: this.twentyConfigService.get( - 'SUPPORT_FRONT_CHAT_ID', - ), - }, - sentry: { - environment: this.twentyConfigService.get('SENTRY_ENVIRONMENT'), - release: this.twentyConfigService.get('APP_VERSION'), - dsn: this.twentyConfigService.get('SENTRY_FRONT_DSN'), - }, - captcha: { - provider: captchaProvider - ? this.transformEnum(captchaProvider, CaptchaDriverType) - : undefined, - siteKey: this.twentyConfigService.get('CAPTCHA_SITE_KEY'), - }, - chromeExtensionId: this.twentyConfigService.get('CHROME_EXTENSION_ID'), - api: { - mutationMaximumAffectedRecords: this.twentyConfigService.get( - 'MUTATION_MAXIMUM_AFFECTED_RECORDS', - ), - }, - isAttachmentPreviewEnabled: this.twentyConfigService.get( - 'IS_ATTACHMENT_PREVIEW_ENABLED', - ), - analyticsEnabled: this.twentyConfigService.get('ANALYTICS_ENABLED'), - canManageFeatureFlags: - this.twentyConfigService.get('NODE_ENV') === - NodeEnvironment.development || - this.twentyConfigService.get('IS_BILLING_ENABLED'), - publicFeatureFlags: PUBLIC_FEATURE_FLAGS, - isMicrosoftMessagingEnabled: this.twentyConfigService.get( - 'MESSAGING_PROVIDER_MICROSOFT_ENABLED', - ), - isMicrosoftCalendarEnabled: this.twentyConfigService.get( - 'CALENDAR_PROVIDER_MICROSOFT_ENABLED', - ), - isGoogleMessagingEnabled: this.twentyConfigService.get( - 'MESSAGING_PROVIDER_GMAIL_ENABLED', - ), - isGoogleCalendarEnabled: this.twentyConfigService.get( - 'CALENDAR_PROVIDER_GOOGLE_ENABLED', - ), - isConfigVariablesInDbEnabled: this.twentyConfigService.get( - 'IS_CONFIG_VARIABLES_IN_DB_ENABLED', - ), - }; - - return clientConfig; - } - - // GraphQL enum values are in PascalCase, but the config values are in kebab-case - // This function transforms the config values, the same way GraphQL does - private transformEnum>( - value: string, - enumObject: T, - ): T[keyof T] { - const directMatch = Object.keys(enumObject).find( - (key) => key === value, - ) as keyof T; - - if (directMatch) { - return enumObject[directMatch]; - } - - const valueMatch = Object.entries(enumObject).find( - ([, enumValue]) => enumValue === value, - ); - - if (valueMatch) { - return valueMatch[0] as T[keyof T]; - } - - const availableKeys = Object.keys(enumObject); - const availableValues = Object.values(enumObject); - - throw new Error( - `Unknown enum value: ${value}. Available keys: ${availableKeys.join(', ')}. Available values: ${availableValues.join(', ')}`, - ); - } -} diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts index 6562033b8..c2774c4c2 100644 --- a/packages/twenty-ui/src/utilities/index.ts +++ b/packages/twenty-ui/src/utilities/index.ts @@ -24,6 +24,5 @@ export { isModifiedEvent } from './events/isModifiedEvent'; export { useIsMobile } from './responsive/hooks/useIsMobile'; export { useScreenSize } from './screen-size/hooks/useScreenSize'; export { createState } from './state/utils/createState'; -export type { ClickOutsideAttributes } from './types/ClickOutsideAttributes'; export type { Nullable } from './types/Nullable'; export { getDisplayValueByUrlType } from './utils/getDisplayValueByUrlType';