From 524962a3d8f00a6afb27f8001e76d4cf945e5e49 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 9 Jan 2025 17:48:36 +0100 Subject: [PATCH 01/22] add httpExceptionHandlerService to oauthfilter (#9518) ## Context 500 from oauth controllers are never captured, fixing this --- .../controllers/google-auth.controller.ts | 7 +++++-- .../filters/auth-oauth-exception.filter.ts | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts index 31b83489f461..ebe6efef876b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts @@ -11,7 +11,10 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Response } from 'express'; import { Repository } from 'typeorm'; -import { AuthException } from 'src/engine/core-modules/auth/auth.exception'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter'; import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oauth.guard'; @@ -121,7 +124,7 @@ export class GoogleAuthController { }), ); } - throw err; + throw new AuthException(AuthExceptionCode.INTERNAL_SERVER_ERROR, err); } } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts index 24ac236a2d9e..8adf85481814 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts @@ -1,9 +1,4 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - InternalServerErrorException, -} from '@nestjs/common'; +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; import { Response } from 'express'; @@ -12,10 +7,14 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service'; @Catch(AuthException) export class AuthOAuthExceptionFilter implements ExceptionFilter { - constructor(private readonly domainManagerService: DomainManagerService) {} + constructor( + private readonly domainManagerService: DomainManagerService, + private readonly httpExceptionHandlerService: HttpExceptionHandlerService, + ) {} catch(exception: AuthException, host: ArgumentsHost) { const ctx = host.switchToHttp(); @@ -28,7 +27,11 @@ export class AuthOAuthExceptionFilter implements ExceptionFilter { .redirect(this.domainManagerService.getBaseUrl().toString()); break; default: - throw new InternalServerErrorException(exception.message); + return this.httpExceptionHandlerService.handleError( + exception, + response, + 500, + ); } } } From f44d99d2ff308f8b7a52569e181e57f0985e3fa7 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 9 Jan 2025 18:18:27 +0100 Subject: [PATCH 02/22] Refactored object filter dropdown states (#9507) Refactored object filter dropdown states while keeping the existing structure to avoid creating a big PR. The goal is to extract each sub hook returned by the useFilterDropdown hook and create a PR for each function and the associated refactor for the dependent zones in the code, so that we proceed by small increments. --- .../__tests__/useFilterDropdown.test.tsx | 8 +- .../hooks/useFilterDropdown.ts | 13 ++-- .../hooks/useFilterDropdownStates.ts | 73 +++++++++---------- ...etFilterDefinitionUsedInDropdownInScope.ts | 15 ++-- .../scopes/ObjectFilterDropdownScope.tsx | 7 +- ...bjectFilterDropdownScopeInternalContext.ts | 7 -- ...edFilterViewFilterGroupIdComponentState.ts | 6 +- ...dvancedFilterViewFilterIdComponentState.ts | 6 +- ...vailableFilterDefinitionsComponentState.ts | 6 +- ...rDefinitionUsedInDropdownComponentState.ts | 7 +- ...downOperandSelectUnfoldedComponentState.ts | 6 +- ...ectFilterDropdownUnfoldedComponentState.ts | 8 +- ...FilterDropdownSearchInputComponentState.ts | 6 +- ...pdownSelectedOptionValuesComponentState.ts | 6 +- ...DropdownSelectedRecordIdsComponentState.ts | 6 +- .../states/onFilterSelectComponentState.ts | 7 +- .../states/selectedFilterComponentState.ts | 7 +- ...selectedOperandInDropdownComponentState.ts | 6 +- 18 files changed, 104 insertions(+), 96 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/scopes/scope-internal-context/ObjectFilterDropdownScopeInternalContext.ts diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx index 2d77ce2b840a..a3007acdccc6 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx @@ -313,7 +313,7 @@ describe('useFilterDropdown', () => { }); }); - it('should handle scopeId undefined on initial values', () => { + it('should handle componentInstanceId undefined on initial values', () => { global.console.error = jest.fn(); const renderFunction = () => { @@ -322,16 +322,16 @@ describe('useFilterDropdown', () => { expect(renderFunction).toThrow(Error); expect(renderFunction).toThrow( - 'Scope id is not provided and cannot be found in context.', + 'Instance id is not provided and cannot be found in context.', ); }); - it('should scopeId have been defined on initial values', () => { + it('should componentInstanceId have been defined on initial values', () => { const { result } = renderHook( () => useFilterDropdown({ filterDropdownId }), renderHookConfig, ); - expect(result.current.scopeId).toBeDefined(); + expect(result.current.componentInstanceId).toBeDefined(); }); }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts index e492f5eb24a8..df5acd6765a6 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts @@ -1,25 +1,24 @@ import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useFilterDropdownStates } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownStates'; -import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; import { isDefined } from 'twenty-ui'; -import { ObjectFilterDropdownScopeInternalContext } from '../scopes/scope-internal-context/ObjectFilterDropdownScopeInternalContext'; import { Filter } from '../types/Filter'; type UseFilterDropdownProps = { filterDropdownId?: string; - advancedFilterViewFilterId?: string; }; export const useFilterDropdown = (props?: UseFilterDropdownProps) => { - const scopeId = useAvailableScopeIdOrThrow( - ObjectFilterDropdownScopeInternalContext, + const componentInstanceId = useAvailableComponentInstanceIdOrThrow( + ObjectFilterDropdownComponentInstanceContext, props?.filterDropdownId, ); @@ -33,7 +32,7 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { onFilterSelectState, advancedFilterViewFilterGroupIdState, advancedFilterViewFilterIdState, - } = useFilterDropdownStates(scopeId); + } = useFilterDropdownStates(componentInstanceId); const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters(); @@ -129,7 +128,7 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { ); return { - scopeId, + componentInstanceId, selectFilter, resetFilter, setSelectedFilter, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts index 9068c113edd4..4b530d43f1ed 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts @@ -7,53 +7,50 @@ import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-re import { onFilterSelectComponentState } from '@/object-record/object-filter-dropdown/states/onFilterSelectComponentState'; import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; -import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; -export const useFilterDropdownStates = (scopeId: string) => { - const filterDefinitionUsedInDropdownState = extractComponentState( - filterDefinitionUsedInDropdownComponentState, - scopeId, - ); +export const useFilterDropdownStates = (componentInstanceId: string) => { + const filterDefinitionUsedInDropdownState = + filterDefinitionUsedInDropdownComponentState.atomFamily({ + instanceId: componentInstanceId, + }); - const objectFilterDropdownSearchInputState = extractComponentState( - objectFilterDropdownSearchInputComponentState, - scopeId, - ); + const objectFilterDropdownSearchInputState = + objectFilterDropdownSearchInputComponentState.atomFamily({ + instanceId: componentInstanceId, + }); - const objectFilterDropdownSelectedRecordIdsState = extractComponentState( - objectFilterDropdownSelectedRecordIdsComponentState, - scopeId, - ); + const objectFilterDropdownSelectedRecordIdsState = + objectFilterDropdownSelectedRecordIdsComponentState.atomFamily({ + instanceId: componentInstanceId, + }); - const objectFilterDropdownSelectedOptionValuesState = extractComponentState( - objectFilterDropdownSelectedOptionValuesComponentState, - scopeId, - ); + const objectFilterDropdownSelectedOptionValuesState = + objectFilterDropdownSelectedOptionValuesComponentState.atomFamily({ + instanceId: componentInstanceId, + }); - const selectedFilterState = extractComponentState( - selectedFilterComponentState, - scopeId, - ); + const selectedFilterState = selectedFilterComponentState.atomFamily({ + instanceId: componentInstanceId, + }); - const selectedOperandInDropdownState = extractComponentState( - selectedOperandInDropdownComponentState, - scopeId, - ); + const selectedOperandInDropdownState = + selectedOperandInDropdownComponentState.atomFamily({ + instanceId: componentInstanceId, + }); - const onFilterSelectState = extractComponentState( - onFilterSelectComponentState, - scopeId, - ); + const onFilterSelectState = onFilterSelectComponentState.atomFamily({ + instanceId: componentInstanceId, + }); - const advancedFilterViewFilterGroupIdState = extractComponentState( - advancedFilterViewFilterGroupIdComponentState, - scopeId, - ); + const advancedFilterViewFilterGroupIdState = + advancedFilterViewFilterGroupIdComponentState.atomFamily({ + instanceId: componentInstanceId, + }); - const advancedFilterViewFilterIdState = extractComponentState( - advancedFilterViewFilterIdComponentState, - scopeId, - ); + const advancedFilterViewFilterIdState = + advancedFilterViewFilterIdComponentState.atomFamily({ + instanceId: componentInstanceId, + }); return { filterDefinitionUsedInDropdownState, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSetFilterDefinitionUsedInDropdownInScope.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSetFilterDefinitionUsedInDropdownInScope.ts index 85a515c6d15c..142f76a5d2c9 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSetFilterDefinitionUsedInDropdownInScope.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSetFilterDefinitionUsedInDropdownInScope.ts @@ -1,16 +1,19 @@ import { useRecoilCallback } from 'recoil'; import { filterDefinitionUsedInDropdownComponentState } from '../states/filterDefinitionUsedInDropdownComponentState'; import { FilterDefinition } from '../types/FilterDefinition'; -import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; export const useSetFilterDefinitionUsedInDropdownInScope = () => { const setFilterDefinitionUsedInDropdownInScope = useRecoilCallback( ({ set }) => - (scopeId: string, filterDefinition: FilterDefinition | null) => { - const filterDefinitionUsedInDropdownState = extractComponentState( - filterDefinitionUsedInDropdownComponentState, - scopeId, - ); + ( + componentInstanceId: string, + filterDefinition: FilterDefinition | null, + ) => { + const filterDefinitionUsedInDropdownState = + filterDefinitionUsedInDropdownComponentState.atomFamily({ + instanceId: componentInstanceId, + }); + set(filterDefinitionUsedInDropdownState, filterDefinition); }, [], diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope.tsx index f7950d494804..25122ffd31ff 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope.tsx @@ -1,7 +1,6 @@ import { ReactNode } from 'react'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; -import { ObjectFilterDropdownScopeInternalContext } from './scope-internal-context/ObjectFilterDropdownScopeInternalContext'; type ObjectFilterDropdownScopeProps = { children: ReactNode; @@ -16,11 +15,7 @@ export const ObjectFilterDropdownScope = ({ - - {children} - + {children} ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/scopes/scope-internal-context/ObjectFilterDropdownScopeInternalContext.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/scopes/scope-internal-context/ObjectFilterDropdownScopeInternalContext.ts deleted file mode 100644 index be81417c1ca9..000000000000 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/scopes/scope-internal-context/ObjectFilterDropdownScopeInternalContext.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext'; -import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey'; - -type ObjectFilterDropdownScopeInternalContextProps = RecoilComponentStateKey; - -export const ObjectFilterDropdownScopeInternalContext = - createScopeInternalContext(); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState.ts index b2c1dad91cf0..64d8238bcc2e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState.ts @@ -1,7 +1,9 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const advancedFilterViewFilterGroupIdComponentState = - createComponentState({ + createComponentStateV2({ key: 'advancedFilterViewFilterGroupIdComponentState', defaultValue: undefined, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState.ts index 59053b825af0..cc3e3678ddc0 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState.ts @@ -1,8 +1,10 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -export const advancedFilterViewFilterIdComponentState = createComponentState< +export const advancedFilterViewFilterIdComponentState = createComponentStateV2< string | undefined >({ key: 'advancedFilterViewFilterIdComponentState', defaultValue: undefined, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/availableFilterDefinitionsComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/availableFilterDefinitionsComponentState.ts index 9a3f649ea491..49d20cb08a8c 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/availableFilterDefinitionsComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/availableFilterDefinitionsComponentState.ts @@ -1,9 +1,11 @@ +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -export const availableFilterDefinitionsComponentState = createComponentState< +export const availableFilterDefinitionsComponentState = createComponentStateV2< FilterDefinition[] >({ key: 'availableFilterDefinitionsComponentState', defaultValue: [], + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState.ts index c7fd89f92902..dff9ad352888 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState.ts @@ -1,9 +1,10 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { FilterDefinition } from '../types/FilterDefinition'; export const filterDefinitionUsedInDropdownComponentState = - createComponentState({ + createComponentStateV2({ key: 'filterDefinitionUsedInDropdownComponentState', defaultValue: null, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState.ts index 389c6d30f928..eacaa84cc05c 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState.ts @@ -1,7 +1,9 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const isObjectFilterDropdownOperandSelectUnfoldedComponentState = - createComponentState({ + createComponentStateV2({ key: 'isObjectFilterDropdownOperandSelectUnfoldedComponentState', defaultValue: false, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState.ts index cfd6dea10c8f..ac62700b8d99 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState.ts @@ -1,7 +1,9 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const isObjectFilterDropdownUnfoldedComponentState = - createComponentState({ - key: 'isObjectFilterDropdownUnfoldedScopedState', + createComponentStateV2({ + key: 'isObjectFilterDropdownUnfoldedComponentState', defaultValue: false, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState.ts index 8addf2fb6cc6..47ed066c2f7b 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState.ts @@ -1,7 +1,9 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const objectFilterDropdownSearchInputComponentState = - createComponentState({ + createComponentStateV2({ key: 'objectFilterDropdownSearchInputComponentState', defaultValue: '', + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState.ts index feef87220c1e..da1b563a6a59 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState.ts @@ -1,7 +1,9 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const objectFilterDropdownSelectedOptionValuesComponentState = - createComponentState({ + createComponentStateV2({ key: 'objectFilterDropdownSelectedOptionValuesComponentState', defaultValue: [], + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState.ts index de23b3b0daa5..b579beec63bd 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState.ts @@ -1,7 +1,9 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const objectFilterDropdownSelectedRecordIdsComponentState = - createComponentState({ + createComponentStateV2({ key: 'objectFilterDropdownSelectedRecordIdsComponentState', defaultValue: [], + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectComponentState.ts index b73c2cbe962f..2efbd31db7e1 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectComponentState.ts @@ -1,10 +1,11 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { Filter } from '../types/Filter'; -export const onFilterSelectComponentState = createComponentState< +export const onFilterSelectComponentState = createComponentStateV2< ((filter: Filter | null) => void) | undefined >({ key: 'onFilterSelectComponentState', defaultValue: undefined, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterComponentState.ts index 7b4183c74841..543f47806c23 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterComponentState.ts @@ -1,10 +1,11 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { Filter } from '../types/Filter'; -export const selectedFilterComponentState = createComponentState< +export const selectedFilterComponentState = createComponentStateV2< Filter | undefined | null >({ key: 'selectedFilterComponentState', defaultValue: undefined, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState.ts index eef8fe552a96..7ed790b5155b 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState.ts @@ -1,8 +1,10 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; export const selectedOperandInDropdownComponentState = - createComponentState({ + createComponentStateV2({ key: 'selectedOperandInDropdownComponentState', defaultValue: null, + componentInstanceContext: ObjectFilterDropdownComponentInstanceContext, }); From 4ed1db3845a12b125fcb7dc999bc2ae52f2fe331 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 9 Jan 2025 18:29:09 +0100 Subject: [PATCH 03/22] fix AuthException parameters --- .../core-modules/auth/controllers/google-auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts index ebe6efef876b..d4a1cd485052 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts @@ -124,7 +124,7 @@ export class GoogleAuthController { }), ); } - throw new AuthException(AuthExceptionCode.INTERNAL_SERVER_ERROR, err); + throw new AuthException(err, AuthExceptionCode.INTERNAL_SERVER_ERROR); } } } From c39af5f0636c116c5b05595922b926bb73dad2f9 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:30:41 -0300 Subject: [PATCH 04/22] Add Integration and unit tests on Billing (#9317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Solves [ https://github.com/twentyhq/private-issues/issues/214 ] **TLDR** Add unit and integration tests to Billing. First approach to run jest integration tests directly from VSCode. **In order to run the unit tests:** Run unit test using the CLI or with the jest extension directly from VSCode. **In order to run the integration tests:** Ensure that your database has the billingTables. If that's not the case, migrate the database with IS_BILLING_ENABLED set to true: ` npx nx run twenty-server:test:integration test/integration/billing/suites/billing-controller.integration-spec.ts` **Doing:** - Unit test on transformSubscriptionEventToSubscriptionItem - More tests cases in billingController integration tests. --------- Co-authored-by: Félix Malfait Co-authored-by: Weiko Co-authored-by: Charles Bochet --- .eslintrc.cjs | 6 +- .github/workflows/ci-server.yaml | 9 + .vscode/settings.json | 2 + nx.json | 1 + .../twenty-server/jest-integration.config.ts | 5 +- .../billing/billing.controller.ts | 104 ++++---- .../core-modules/billing/billing.exception.ts | 1 + .../core-modules/billing/billing.resolver.ts | 9 +- .../billing-sync-customer-data.command.ts | 6 +- .../billing-sync-plans-data.command.ts | 14 +- .../enums/billing-webhook-events.enum.ts | 2 +- .../filters/billing-api-exception.filter.ts | 6 + .../jobs/update-subscription-quantity.job.ts | 6 +- .../billing-portal.workspace-service.ts | 17 +- .../services/billing-subscription.service.ts | 21 +- .../billing-webhook-entitlement.service.ts | 4 + .../services/billing-webhook-price.service.ts | 11 +- .../billing-webhook-product.service.ts | 4 + .../billing-webhook-subscription.service.ts | 13 +- .../services/stripe-billing-meter.service.ts | 34 +++ .../services/stripe-billing-portal.service.ts | 37 +++ .../services/stripe-checkout.service.ts | 67 +++++ .../services/stripe-customer.service.ts | 33 +++ .../stripe/services/stripe-price.service.ts | 85 +++++++ .../stripe/services/stripe-product.service.ts | 30 +++ .../stripe-subscription-item.service.ts | 45 ++++ .../services/stripe-subscription.service.ts | 59 +++++ .../stripe/services/stripe-webhook.service.ts | 37 +++ .../mocks/stripe-sdk-mock.service.ts | 13 + .../stripe-sdk/mocks/stripe-sdk.mock.ts | 29 +++ .../stripe-sdk/services/stripe-sdk.service.ts | 10 + .../stripe/stripe-sdk/stripe-sdk.module.ts | 9 + .../billing/stripe/stripe.module.ts | 37 ++- .../billing/stripe/stripe.service.ts | 233 ----------------- ...stripe-valid-product-metadata.util.spec.ts | 56 +++++ ...o-entitlement-repository-data.util.spec.ts | 84 +++++++ ...data-to-meter-repository-data.util.spec.ts | 94 +++++++ ...data-to-price-repository-data.util.spec.ts | 219 ++++++++++++++++ ...vent-to-price-repository-data.util.spec.ts | 234 ++++++++++++++++++ ...ta-to-product-repository-data.util.spec.ts | 86 +++++++ ...nt-to-product-repository-data.util.spec.ts | 89 +++++++ ...t-to-customer-repository-data.util.spec.ts | 85 +++++++ ...-subscription-repository-data.util.spec.ts | 197 +++++++++++++++ .../billing-controller.integration-spec.ts | 112 +++++++++ ...ck-stripe-entitlement-updated-data.util.ts | 25 ++ ...ate-mock-stripe-price-created-data.util.ts | 34 +++ ...e-mock-stripe-product-updated-data.util.ts | 32 +++ ...k-stripe-subscription-created-data.util.ts | 130 ++++++++++ .../test/integration/utils/create-app.ts | 12 +- 49 files changed, 2155 insertions(+), 333 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-checkout.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-customer.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-price.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-product.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-webhook.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk-mock.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk.mock.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/stripe-sdk.module.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/is-stripe-valid-product-metadata.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-data-to-meter-repository-data.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-data-to-price-repository-data.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-event-to-price-repository-data.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-data-to-product-repository-data.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-event-to-product-repository-data.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-customer-repository-data.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-subscription-repository-data.util.spec.ts create mode 100644 packages/twenty-server/test/integration/billing/suites/billing-controller.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/billing/utils/create-mock-stripe-entitlement-updated-data.util.ts create mode 100644 packages/twenty-server/test/integration/billing/utils/create-mock-stripe-price-created-data.util.ts create mode 100644 packages/twenty-server/test/integration/billing/utils/create-mock-stripe-product-updated-data.util.ts create mode 100644 packages/twenty-server/test/integration/billing/utils/create-mock-stripe-subscription-created-data.util.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 95f91b2a78be..632eac421525 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -96,7 +96,11 @@ module.exports = { rules: {}, }, { - files: ['*.spec.@(ts|tsx|js|jsx)', '*.test.@(ts|tsx|js|jsx)'], + files: [ + '*.spec.@(ts|tsx|js|jsx)', + '*.integration-spec.@(ts|tsx|js|jsx)', + '*.test.@(ts|tsx|js|jsx)', + ], env: { jest: true, }, diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index 5e71284fdc01..806bfe725f75 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -184,6 +184,15 @@ jobs: - name: Install dependencies if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install + - name: Update .env.test for billing + if: steps.changed-files.outputs.any_changed == 'true' + run: | + sed -i '$ a\ +IS_BILLING_ENABLED=true\ +BILLING_STRIPE_API_KEY=test-api-key\ +BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id\ +BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret' .env.test + - name: Server / Restore Task Cache if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache diff --git a/.vscode/settings.json b/.vscode/settings.json index 15a0022d81d1..03197b7470c5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -49,4 +49,6 @@ "files.associations": { ".cursorrules": "markdown" }, + "jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}" + } } diff --git a/nx.json b/nx.json index d73a57ed16e1..6e477b4fcd60 100644 --- a/nx.json +++ b/nx.json @@ -14,6 +14,7 @@ "!{projectRoot}/**/tsconfig.spec.json", "!{projectRoot}/**/*.test.(ts|tsx)", "!{projectRoot}/**/*.spec.(ts|tsx)", + "!{projectRoot}/**/*.integration-spec.ts", "!{projectRoot}/**/__tests__/*" ], "production": [ diff --git a/packages/twenty-server/jest-integration.config.ts b/packages/twenty-server/jest-integration.config.ts index cbb53051d1fd..d8d0d975edf0 100644 --- a/packages/twenty-server/jest-integration.config.ts +++ b/packages/twenty-server/jest-integration.config.ts @@ -1,5 +1,6 @@ import { JestConfigWithTsJest, pathsToModuleNameMapper } from 'ts-jest'; +const isBillingEnabled = process.env.IS_BILLING_ENABLED === 'true'; // eslint-disable-next-line @typescript-eslint/no-var-requires const tsConfig = require('./tsconfig.json'); @@ -9,7 +10,9 @@ const jestConfig: JestConfigWithTsJest = { moduleFileExtensions: ['js', 'json', 'ts'], rootDir: '.', testEnvironment: 'node', - testRegex: '.integration-spec.ts$', + testRegex: isBillingEnabled + ? 'integration-spec.ts' + : '^(?!.*billing).*\\.integration-spec\\.ts$', modulePathIgnorePatterns: ['/dist'], globalSetup: '/test/integration/utils/setup-test.ts', globalTeardown: '/test/integration/utils/teardown-test.ts', diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts index b5113fe6ce6a..19c95edefeb8 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts @@ -10,26 +10,27 @@ import { } from '@nestjs/common'; import { Response } from 'express'; +import Stripe from 'stripe'; import { BillingException, BillingExceptionCode, } from 'src/engine/core-modules/billing/billing.exception'; -import { WebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum'; +import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum'; import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service'; import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service'; import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service'; import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service'; @Controller('billing') @UseFilters(BillingRestApiExceptionFilter) export class BillingController { protected readonly logger = new Logger(BillingController.name); constructor( - private readonly stripeService: StripeService, + private readonly stripeWebhookService: StripeWebhookService, private readonly billingWebhookSubscriptionService: BillingWebhookSubscriptionService, private readonly billingWebhookEntitlementService: BillingWebhookEntitlementService, private readonly billingSubscriptionService: BillingSubscriptionService, @@ -48,72 +49,63 @@ export class BillingController { return; } - const event = this.stripeService.constructEventFromPayload( + const event = this.stripeWebhookService.constructEventFromPayload( signature, req.rawBody, ); - if (event.type === WebhookEvent.SETUP_INTENT_SUCCEEDED) { - await this.billingSubscriptionService.handleUnpaidInvoices(event.data); - } - - if ( - event.type === WebhookEvent.CUSTOMER_SUBSCRIPTION_CREATED || - event.type === WebhookEvent.CUSTOMER_SUBSCRIPTION_UPDATED || - event.type === WebhookEvent.CUSTOMER_SUBSCRIPTION_DELETED - ) { - const workspaceId = event.data.object.metadata?.workspaceId; + try { + const result = await this.handleStripeEvent(event); - if (!workspaceId) { + res.status(200).send(result).end(); + } catch (error) { + if (error instanceof BillingException) { res.status(404).end(); - - return; } - - await this.billingWebhookSubscriptionService.processStripeEvent( - workspaceId, - event.data, - ); } - if ( - event.type === WebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED - ) { - try { - await this.billingWebhookEntitlementService.processStripeEvent( + } + + private async handleStripeEvent(event: Stripe.Event) { + switch (event.type) { + case BillingWebhookEvent.SETUP_INTENT_SUCCEEDED: + return await this.billingSubscriptionService.handleUnpaidInvoices( + event.data, + ); + case BillingWebhookEvent.PRICE_UPDATED: + case BillingWebhookEvent.PRICE_CREATED: + return await this.billingWebhookPriceService.processStripeEvent( event.data, ); - } catch (error) { - if ( - error instanceof BillingException && - error.code === BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND - ) { - res.status(404).end(); - } - } - } - if ( - event.type === WebhookEvent.PRODUCT_CREATED || - event.type === WebhookEvent.PRODUCT_UPDATED - ) { - await this.billingWebhookProductService.processStripeEvent(event.data); - } - if ( - event.type === WebhookEvent.PRICE_CREATED || - event.type === WebhookEvent.PRICE_UPDATED - ) { - try { - await this.billingWebhookPriceService.processStripeEvent(event.data); - } catch (error) { - if ( - error instanceof BillingException && - error.code === BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND - ) { - res.status(404).end(); + case BillingWebhookEvent.PRODUCT_UPDATED: + case BillingWebhookEvent.PRODUCT_CREATED: + return await this.billingWebhookProductService.processStripeEvent( + event.data, + ); + case BillingWebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED: + return await this.billingWebhookEntitlementService.processStripeEvent( + event.data, + ); + + case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_CREATED: + case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_UPDATED: + case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_DELETED: { + const workspaceId = event.data.object.metadata?.workspaceId; + + if (!workspaceId) { + throw new BillingException( + 'Workspace ID is required for subscription events', + BillingExceptionCode.BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND, + ); } + + return await this.billingWebhookSubscriptionService.processStripeEvent( + workspaceId, + event.data, + ); } + default: + return {}; } - - res.status(200).end(); } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts index 59ef9502749b..093b0c481028 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts @@ -12,4 +12,5 @@ export class BillingException extends CustomException { export enum BillingExceptionCode { BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND', BILLING_PRODUCT_NOT_FOUND = 'BILLING_PRODUCT_NOT_FOUND', + BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND = 'BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND', } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts index d32d3bf554f3..5109c1ffecaa 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts @@ -10,7 +10,7 @@ import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update- import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum'; import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; @@ -23,12 +23,13 @@ export class BillingResolver { constructor( private readonly billingSubscriptionService: BillingSubscriptionService, private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService, - private readonly stripeService: StripeService, + private readonly stripePriceService: StripePriceService, ) {} @Query(() => ProductPricesEntity) async getProductPrices(@Args() { product }: ProductInput) { - const productPrices = await this.stripeService.getStripePrices(product); + const productPrices = + await this.stripePriceService.getStripePrices(product); return { totalNumberOfPrices: productPrices.length, @@ -63,7 +64,7 @@ export class BillingResolver { requirePaymentMethod, }: CheckoutSessionInput, ) { - const productPrice = await this.stripeService.getStripePrice( + const productPrice = await this.stripePriceService.getStripePrice( AvailableProduct.BasePlan, recurringInterval, ); diff --git a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts index 70fcb981a080..9733c4bc6756 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts @@ -9,7 +9,7 @@ import { ActiveWorkspacesCommandRunner, } from 'src/database/commands/active-workspaces.command'; import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; interface SyncCustomerDataCommandOptions @@ -23,7 +23,7 @@ export class BillingSyncCustomerDataCommand extends ActiveWorkspacesCommandRunne constructor( @InjectRepository(Workspace, 'core') protected readonly workspaceRepository: Repository, - private readonly stripeService: StripeService, + private readonly stripeSubscriptionService: StripeSubscriptionService, @InjectRepository(BillingCustomer, 'core') protected readonly billingCustomerRepository: Repository, ) { @@ -71,7 +71,7 @@ export class BillingSyncCustomerDataCommand extends ActiveWorkspacesCommandRunne if (!options.dryRun && !billingCustomer) { const stripeCustomerId = - await this.stripeService.getStripeCustomerIdFromWorkspaceId( + await this.stripeSubscriptionService.getStripeCustomerIdFromWorkspaceId( workspaceId, ); diff --git a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-plans-data.command.ts b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-plans-data.command.ts index a75809a3cefe..a220c6a5954b 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-plans-data.command.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-plans-data.command.ts @@ -11,7 +11,9 @@ import { import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity'; import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity'; import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service'; +import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service'; +import { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service'; import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util'; import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util'; import { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util'; @@ -30,7 +32,9 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner { private readonly billingProductRepository: Repository, @InjectRepository(BillingMeter, 'core') private readonly billingMeterRepository: Repository, - private readonly stripeService: StripeService, + private readonly stripeBillingMeterService: StripeBillingMeterService, + private readonly stripeProductService: StripeProductService, + private readonly stripePriceService: StripePriceService, ) { super(); } @@ -92,7 +96,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner { } await this.upsertProductRepositoryData(product, options); - const prices = await this.stripeService.getPricesByProductId( + const prices = await this.stripePriceService.getPricesByProductId( product.id, ); @@ -133,11 +137,11 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner { passedParams: string[], options: BaseCommandOptions, ): Promise { - const billingMeters = await this.stripeService.getAllMeters(); + const billingMeters = await this.stripeBillingMeterService.getAllMeters(); await this.upsertMetersRepositoryData(billingMeters, options); - const billingProducts = await this.stripeService.getAllProducts(); + const billingProducts = await this.stripeProductService.getAllProducts(); const billingPrices = await this.processBillingPricesByProductBatches( billingProducts, diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts index d275746add4c..220b55a5b55f 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts @@ -1,4 +1,4 @@ -export enum WebhookEvent { +export enum BillingWebhookEvent { CUSTOMER_SUBSCRIPTION_CREATED = 'customer.subscription.created', CUSTOMER_SUBSCRIPTION_UPDATED = 'customer.subscription.updated', CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted', diff --git a/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts index ace8707ee9ca..55bb32ffac58 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/filters/billing-api-exception.filter.ts @@ -26,6 +26,12 @@ export class BillingRestApiExceptionFilter implements ExceptionFilter { response, 404, ); + case BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND: + return this.httpExceptionHandlerService.handleError( + exception, + response, + 404, + ); default: return this.httpExceptionHandlerService.handleError( exception, diff --git a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts index 301322448f3e..b81d67b11fcb 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts @@ -1,7 +1,7 @@ import { Logger, Scope } from '@nestjs/common'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; @@ -18,7 +18,7 @@ export class UpdateSubscriptionQuantityJob { constructor( private readonly billingSubscriptionService: BillingSubscriptionService, - private readonly stripeService: StripeService, + private readonly stripeSubscriptionItemService: StripeSubscriptionItemService, private readonly twentyORMManager: TwentyORMManager, ) {} @@ -41,7 +41,7 @@ export class UpdateSubscriptionQuantityJob { data.workspaceId, ); - await this.stripeService.updateSubscriptionItem( + await this.stripeSubscriptionItemService.updateSubscriptionItem( billingSubscriptionItem.stripeSubscriptionItemId, workspaceMembersCount, ); diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts index f422b07dc895..788f2bf6e9ce 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts @@ -6,7 +6,8 @@ import { Repository } from 'typeorm'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service'; +import { StripeCheckoutService } from 'src/engine/core-modules/billing/stripe/services/stripe-checkout.service'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; @@ -17,7 +18,8 @@ import { assert } from 'src/utils/assert'; export class BillingPortalWorkspaceService { protected readonly logger = new Logger(BillingPortalWorkspaceService.name); constructor( - private readonly stripeService: StripeService, + private readonly stripeCheckoutService: StripeCheckoutService, + private readonly stripeBillingPortalService: StripeBillingPortalService, private readonly domainManagerService: DomainManagerService, @InjectRepository(BillingSubscription, 'core') private readonly billingSubscriptionRepository: Repository, @@ -52,7 +54,7 @@ export class BillingPortalWorkspaceService { }) )?.stripeCustomerId; - const session = await this.stripeService.createCheckoutSession( + const session = await this.stripeCheckoutService.createCheckoutSession( user, workspace.id, priceId, @@ -97,10 +99,11 @@ export class BillingPortalWorkspaceService { } const returnUrl = frontBaseUrl.toString(); - const session = await this.stripeService.createBillingPortalSession( - stripeCustomerId, - returnUrl, - ); + const session = + await this.stripeBillingPortalService.createBillingPortalSession( + stripeCustomerId, + returnUrl, + ); assert(session.url, 'Error: missing billingPortal.session.url'); diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts index ce550fe3d92c..a43125e5bd2a 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts @@ -12,7 +12,9 @@ import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing- import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service'; +import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service'; +import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -20,7 +22,9 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; export class BillingSubscriptionService { protected readonly logger = new Logger(BillingSubscriptionService.name); constructor( - private readonly stripeService: StripeService, + private readonly stripeSubscriptionService: StripeSubscriptionService, + private readonly stripePriceService: StripePriceService, + private readonly stripeSubscriptionItemService: StripeSubscriptionItemService, private readonly environmentService: EnvironmentService, @InjectRepository(BillingEntitlement, 'core') private readonly billingEntitlementRepository: Repository, @@ -78,7 +82,7 @@ export class BillingSubscriptionService { }); if (subscriptionToCancel) { - await this.stripeService.cancelSubscription( + await this.stripeSubscriptionService.cancelSubscription( subscriptionToCancel.stripeSubscriptionId, ); await this.billingSubscriptionRepository.delete(subscriptionToCancel.id); @@ -91,10 +95,15 @@ export class BillingSubscriptionService { ); if (billingSubscription?.status === 'unpaid') { - await this.stripeService.collectLastInvoice( + await this.stripeSubscriptionService.collectLastInvoice( billingSubscription.stripeSubscriptionId, ); } + + return { + handleUnpaidInvoiceStripeSubscriptionId: + billingSubscription.stripeSubscriptionId, + }; } async getWorkspaceEntitlementByKey( @@ -127,7 +136,7 @@ export class BillingSubscriptionService { const billingSubscriptionItem = await this.getCurrentBillingSubscriptionItemOrThrow(workspace.id); - const productPrice = await this.stripeService.getStripePrice( + const productPrice = await this.stripePriceService.getStripePrice( AvailableProduct.BasePlan, newInterval, ); @@ -138,7 +147,7 @@ export class BillingSubscriptionService { ); } - await this.stripeService.updateBillingSubscriptionItem( + await this.stripeSubscriptionItemService.updateBillingSubscriptionItem( billingSubscriptionItem, productPrice.stripePriceId, ); diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts index 1f34877b7ec2..2c3e285030b9 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts @@ -48,5 +48,9 @@ export class BillingWebhookEntitlementService { skipUpdateIfNoValuesChanged: true, }, ); + + return { + stripeEntitlementCustomerId: data.object.customer, + }; } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts index 8e87fc095979..660b1d95d8e3 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts @@ -11,14 +11,14 @@ import { import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity'; import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity'; import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service'; import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util'; import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util'; @Injectable() export class BillingWebhookPriceService { protected readonly logger = new Logger(BillingWebhookPriceService.name); constructor( - private readonly stripeService: StripeService, + private readonly stripeBillingMeterService: StripeBillingMeterService, @InjectRepository(BillingPrice, 'core') private readonly billingPriceRepository: Repository, @InjectRepository(BillingMeter, 'core') @@ -45,7 +45,7 @@ export class BillingWebhookPriceService { const meterId = data.object.recurring?.meter; if (meterId) { - const meterData = await this.stripeService.getMeter(meterId); + const meterData = await this.stripeBillingMeterService.getMeter(meterId); await this.billingMeterRepository.upsert( transformStripeMeterDataToMeterRepositoryData(meterData), @@ -63,5 +63,10 @@ export class BillingWebhookPriceService { skipUpdateIfNoValuesChanged: true, }, ); + + return { + stripePriceId: data.object.id, + stripeMeterId: meterId, + }; } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts index 11e2238bf87d..21d09abb4e48 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts @@ -33,6 +33,10 @@ export class BillingWebhookProductService { conflictPaths: ['stripeProductId'], skipUpdateIfNoValuesChanged: true, }); + + return { + stripeProductId: data.object.id, + }; } isStripeValidProductMetadata( diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts index 9cd4c62551e8..970137160b9c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts @@ -8,7 +8,7 @@ import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billin import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service'; import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util'; import { transformStripeSubscriptionEventToSubscriptionItemRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util'; import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util'; @@ -22,7 +22,7 @@ export class BillingWebhookSubscriptionService { BillingWebhookSubscriptionService.name, ); constructor( - private readonly stripeService: StripeService, + private readonly stripeCustomerService: StripeCustomerService, @InjectRepository(BillingSubscription, 'core') private readonly billingSubscriptionRepository: Repository, @InjectRepository(BillingSubscriptionItem, 'core') @@ -45,7 +45,7 @@ export class BillingWebhookSubscriptionService { }); if (!workspace) { - return; + return { noWorkspace: true }; } await this.billingCustomerRepository.upsert( @@ -106,9 +106,14 @@ export class BillingWebhookSubscriptionService { }); } - await this.stripeService.updateCustomerMetadataWorkspaceId( + await this.stripeCustomerService.updateCustomerMetadataWorkspaceId( String(data.object.customer), workspaceId, ); + + return { + stripeSubscriptionId: data.object.id, + stripeCustomerId: data.object.customer, + }; } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service.ts new file mode 100644 index 000000000000..e2b156e879be --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service.ts @@ -0,0 +1,34 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class StripeBillingMeterService { + protected readonly logger = new Logger(StripeBillingMeterService.name); + private readonly stripe: Stripe; + + constructor( + private readonly environmentService: EnvironmentService, + private readonly stripeSDKService: StripeSDKService, + ) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = this.stripeSDKService.getStripe( + this.environmentService.get('BILLING_STRIPE_API_KEY'), + ); + } + + async getMeter(stripeMeterId: string) { + return await this.stripe.billing.meters.retrieve(stripeMeterId); + } + + async getAllMeters() { + const meters = await this.stripe.billing.meters.list(); + + return meters.data; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts new file mode 100644 index 000000000000..f899d8457db2 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts @@ -0,0 +1,37 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class StripeBillingPortalService { + protected readonly logger = new Logger(StripeBillingPortalService.name); + private readonly stripe: Stripe; + + constructor( + private readonly environmentService: EnvironmentService, + private readonly domainManagerService: DomainManagerService, + private readonly stripeSDKService: StripeSDKService, + ) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = this.stripeSDKService.getStripe( + this.environmentService.get('BILLING_STRIPE_API_KEY'), + ); + } + + async createBillingPortalSession( + stripeCustomerId: string, + returnUrl?: string, + ): Promise { + return await this.stripe.billingPortal.sessions.create({ + customer: stripeCustomerId, + return_url: + returnUrl ?? this.domainManagerService.getBaseUrl().toString(), + }); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-checkout.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-checkout.service.ts new file mode 100644 index 000000000000..fd7715275671 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-checkout.service.ts @@ -0,0 +1,67 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum'; +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { User } from 'src/engine/core-modules/user/user.entity'; + +@Injectable() +export class StripeCheckoutService { + protected readonly logger = new Logger(StripeCheckoutService.name); + private readonly stripe: Stripe; + + constructor( + private readonly environmentService: EnvironmentService, + private readonly stripeSDKService: StripeSDKService, + ) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = this.stripeSDKService.getStripe( + this.environmentService.get('BILLING_STRIPE_API_KEY'), + ); + } + + async createCheckoutSession( + user: User, + workspaceId: string, + priceId: string, + quantity: number, + successUrl?: string, + cancelUrl?: string, + stripeCustomerId?: string, + plan: BillingPlanKey = BillingPlanKey.PRO, + requirePaymentMethod = true, + ): Promise { + return await this.stripe.checkout.sessions.create({ + line_items: [ + { + price: priceId, + quantity, + }, + ], + mode: 'subscription', + subscription_data: { + metadata: { + workspaceId, + plan, + }, + trial_period_days: this.environmentService.get( + 'BILLING_FREE_TRIAL_DURATION_IN_DAYS', + ), + }, + automatic_tax: { enabled: !!requirePaymentMethod }, + tax_id_collection: { enabled: !!requirePaymentMethod }, + customer: stripeCustomerId, + customer_update: stripeCustomerId ? { name: 'auto' } : undefined, + customer_email: stripeCustomerId ? undefined : user.email, + success_url: successUrl, + cancel_url: cancelUrl, + payment_method_collection: requirePaymentMethod + ? 'always' + : 'if_required', + }); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-customer.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-customer.service.ts new file mode 100644 index 000000000000..e8ab972d011e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-customer.service.ts @@ -0,0 +1,33 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class StripeCustomerService { + protected readonly logger = new Logger(StripeCustomerService.name); + private readonly stripe: Stripe; + + constructor( + private readonly environmentService: EnvironmentService, + private readonly stripeSDKService: StripeSDKService, + ) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = this.stripeSDKService.getStripe( + this.environmentService.get('BILLING_STRIPE_API_KEY'), + ); + } + + async updateCustomerMetadataWorkspaceId( + stripeCustomerId: string, + workspaceId: string, + ) { + await this.stripe.customers.update(stripeCustomerId, { + metadata: { workspaceId: workspaceId }, + }); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-price.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-price.service.ts new file mode 100644 index 000000000000..83e4058f41e1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-price.service.ts @@ -0,0 +1,85 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity'; +import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum'; +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class StripePriceService { + protected readonly logger = new Logger(StripePriceService.name); + private readonly stripe: Stripe; + + constructor( + private readonly environmentService: EnvironmentService, + private readonly stripeSDKService: StripeSDKService, + ) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = this.stripeSDKService.getStripe( + this.environmentService.get('BILLING_STRIPE_API_KEY'), + ); + } + + async getStripePrices(product: AvailableProduct) { + const stripeProductId = this.getStripeProductId(product); + + const prices = await this.stripe.prices.search({ + query: `product: '${stripeProductId}'`, + }); + + return this.formatProductPrices(prices.data); + } + + async getStripePrice(product: AvailableProduct, recurringInterval: string) { + const productPrices = await this.getStripePrices(product); + + return productPrices.find( + (price) => price.recurringInterval === recurringInterval, + ); + } + + getStripeProductId(product: AvailableProduct) { + if (product === AvailableProduct.BasePlan) { + return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID'); + } + } // PD:,will be eliminated after refactoring + + formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] { + const productPrices: ProductPriceEntity[] = Object.values( + prices + .filter((item) => item.recurring?.interval && item.unit_amount) + .reduce((acc, item: Stripe.Price) => { + const interval = item.recurring?.interval; + + if (!interval || !item.unit_amount) { + return acc; + } + + if (!acc[interval] || item.created > acc[interval].created) { + acc[interval] = { + unitAmount: item.unit_amount, + recurringInterval: interval, + created: item.created, + stripePriceId: item.id, + }; + } + + return acc satisfies Record; + }, {}), + ); + + return productPrices.sort((a, b) => a.unitAmount - b.unitAmount); + } + + async getPricesByProductId(productId: string) { + const prices = await this.stripe.prices.search({ + query: `product:'${productId}'`, + }); + + return prices.data; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-product.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-product.service.ts new file mode 100644 index 000000000000..51aa3f9c47dd --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-product.service.ts @@ -0,0 +1,30 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class StripeProductService { + protected readonly logger = new Logger(StripeProductService.name); + private readonly stripe: Stripe; + + constructor( + private readonly environmentService: EnvironmentService, + private readonly stripeSDKService: StripeSDKService, + ) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = this.stripeSDKService.getStripe( + this.environmentService.get('BILLING_STRIPE_API_KEY'), + ); + } + + async getAllProducts() { + const products = await this.stripe.products.list(); + + return products.data; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service.ts new file mode 100644 index 000000000000..3b0c4029e827 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service.ts @@ -0,0 +1,45 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class StripeSubscriptionItemService { + protected readonly logger = new Logger(StripeSubscriptionItemService.name); + private readonly stripe: Stripe; + + constructor( + private readonly environmentService: EnvironmentService, + private readonly stripeSDKService: StripeSDKService, + ) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = this.stripeSDKService.getStripe( + this.environmentService.get('BILLING_STRIPE_API_KEY'), + ); + } + + async updateSubscriptionItem(stripeItemId: string, quantity: number) { + await this.stripe.subscriptionItems.update(stripeItemId, { quantity }); + } + + async updateBillingSubscriptionItem( + stripeSubscriptionItem: BillingSubscriptionItem, + stripePriceId: string, + ) { + await this.stripe.subscriptionItems.update( + stripeSubscriptionItem.stripeSubscriptionItemId, + { + price: stripePriceId, + quantity: + stripeSubscriptionItem.quantity === null + ? undefined + : stripeSubscriptionItem.quantity, + }, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription.service.ts new file mode 100644 index 000000000000..686aafe479cc --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription.service.ts @@ -0,0 +1,59 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class StripeSubscriptionService { + protected readonly logger = new Logger(StripeSubscriptionService.name); + private readonly stripe: Stripe; + + constructor( + private readonly environmentService: EnvironmentService, + private readonly stripeSDKService: StripeSDKService, + ) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = this.stripeSDKService.getStripe( + this.environmentService.get('BILLING_STRIPE_API_KEY'), + ); + } + + async cancelSubscription(stripeSubscriptionId: string) { + await this.stripe.subscriptions.cancel(stripeSubscriptionId); + } + + async getStripeCustomerIdFromWorkspaceId(workspaceId: string) { + const subscription = await this.stripe.subscriptions.search({ + query: `metadata['workspaceId']:'${workspaceId}'`, + limit: 1, + }); + const stripeCustomerId = subscription.data[0].customer + ? String(subscription.data[0].customer) + : undefined; + + return stripeCustomerId; + } + + async collectLastInvoice(stripeSubscriptionId: string) { + const subscription = await this.stripe.subscriptions.retrieve( + stripeSubscriptionId, + { expand: ['latest_invoice'] }, + ); + const latestInvoice = subscription.latest_invoice; + + if ( + !( + latestInvoice && + typeof latestInvoice !== 'string' && + latestInvoice.status === 'draft' + ) + ) { + return; + } + await this.stripe.invoices.pay(latestInvoice.id); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-webhook.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-webhook.service.ts new file mode 100644 index 000000000000..bc218a704996 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-webhook.service.ts @@ -0,0 +1,37 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +@Injectable() +export class StripeWebhookService { + protected readonly logger = new Logger(StripeWebhookService.name); + private stripe: Stripe; + + constructor( + private readonly environmentService: EnvironmentService, + private readonly stripeSDKService: StripeSDKService, + ) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return; + } + this.stripe = this.stripeSDKService.getStripe( + this.environmentService.get('BILLING_STRIPE_API_KEY'), + ); + } + + constructEventFromPayload(signature: string, payload: Buffer) { + const webhookSecret = this.environmentService.get( + 'BILLING_STRIPE_WEBHOOK_SECRET', + ); + + const returnValue = this.stripe.webhooks.constructEvent( + payload, + signature, + webhookSecret, + ); + + return returnValue; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk-mock.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk-mock.service.ts new file mode 100644 index 000000000000..e26bb3cd952b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk-mock.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { StripeSDKMock } from 'src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk.mock'; +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; + +@Injectable() +export class StripeSDKMockService implements StripeSDKService { + getStripe(stripeApiKey: string) { + return new StripeSDKMock(stripeApiKey) as unknown as Stripe; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk.mock.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk.mock.ts new file mode 100644 index 000000000000..719cb87fa49e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk.mock.ts @@ -0,0 +1,29 @@ +import Stripe from 'stripe'; + +export class StripeSDKMock { + constructor(private readonly apiKey: string) {} + + customers = { + update: (_id: string, _params?: Stripe.CustomerUpdateParams) => { + return; + }, + }; + + webhooks = { + constructEvent: ( + payload: Buffer, + signature: string, + _webhookSecret: string, + ) => { + if (signature === 'correct-signature') { + const body = JSON.parse(payload.toString()); + + return { + type: body.type, + data: body.data, + }; + } + throw new Error('Invalid signature'); + }, + }; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service.ts new file mode 100644 index 000000000000..a8b95dc5196d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; + +import Stripe from 'stripe'; + +@Injectable() +export class StripeSDKService { + getStripe(stripeApiKey: string) { + return new Stripe(stripeApiKey, {}); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/stripe-sdk.module.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/stripe-sdk.module.ts new file mode 100644 index 000000000000..5256a28cf27c --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe-sdk/stripe-sdk.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; + +@Module({ + providers: [StripeSDKService], + exports: [StripeSDKService], +}) +export class StripeSDKModule {} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.module.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.module.ts index f85a8930fe64..c7e2dfca4df3 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.module.ts @@ -1,11 +1,40 @@ import { Module } from '@nestjs/common'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service'; +import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service'; +import { StripeCheckoutService } from 'src/engine/core-modules/billing/stripe/services/stripe-checkout.service'; +import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service'; +import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service'; +import { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service'; +import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service'; +import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service'; +import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service'; +import { StripeSDKModule } from 'src/engine/core-modules/billing/stripe/stripe-sdk/stripe-sdk.module'; import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; @Module({ - imports: [DomainManagerModule], - providers: [StripeService], - exports: [StripeService], + imports: [DomainManagerModule, StripeSDKModule], + providers: [ + StripeSubscriptionItemService, + StripeWebhookService, + StripeCheckoutService, + StripeSubscriptionService, + StripeBillingPortalService, + StripeBillingMeterService, + StripeCustomerService, + StripePriceService, + StripeProductService, + ], + exports: [ + StripeWebhookService, + StripeBillingPortalService, + StripeBillingMeterService, + StripeCustomerService, + StripePriceService, + StripeCheckoutService, + StripeSubscriptionItemService, + StripeSubscriptionService, + StripeProductService, + ], }) export class StripeModule {} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts deleted file mode 100644 index ae44628c22fc..000000000000 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import Stripe from 'stripe'; - -import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity'; -import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; -import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum'; -import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { User } from 'src/engine/core-modules/user/user.entity'; - -@Injectable() -export class StripeService { - protected readonly logger = new Logger(StripeService.name); - private readonly stripe: Stripe; - - constructor( - private readonly environmentService: EnvironmentService, - private readonly domainManagerService: DomainManagerService, - ) { - if (!this.environmentService.get('IS_BILLING_ENABLED')) { - return; - } - this.stripe = new Stripe( - this.environmentService.get('BILLING_STRIPE_API_KEY'), - {}, - ); - } - - constructEventFromPayload(signature: string, payload: Buffer) { - const webhookSecret = this.environmentService.get( - 'BILLING_STRIPE_WEBHOOK_SECRET', - ); - - return this.stripe.webhooks.constructEvent( - payload, - signature, - webhookSecret, - ); - } - - async getStripePrices(product: AvailableProduct) { - const stripeProductId = this.getStripeProductId(product); - - const prices = await this.stripe.prices.search({ - query: `product: '${stripeProductId}'`, - }); - - return this.formatProductPrices(prices.data); - } - - async getStripePrice(product: AvailableProduct, recurringInterval: string) { - const productPrices = await this.getStripePrices(product); - - return productPrices.find( - (price) => price.recurringInterval === recurringInterval, - ); - } - - getStripeProductId(product: AvailableProduct) { - if (product === AvailableProduct.BasePlan) { - return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID'); - } - } - - async updateSubscriptionItem(stripeItemId: string, quantity: number) { - await this.stripe.subscriptionItems.update(stripeItemId, { quantity }); - } - - async cancelSubscription(stripeSubscriptionId: string) { - await this.stripe.subscriptions.cancel(stripeSubscriptionId); - } - - async createBillingPortalSession( - stripeCustomerId: string, - returnUrl?: string, - ): Promise { - return await this.stripe.billingPortal.sessions.create({ - customer: stripeCustomerId, - return_url: - returnUrl ?? this.domainManagerService.getBaseUrl().toString(), - }); - } - - async createCheckoutSession( - user: User, - workspaceId: string, - priceId: string, - quantity: number, - successUrl?: string, - cancelUrl?: string, - stripeCustomerId?: string, - plan: BillingPlanKey = BillingPlanKey.PRO, - requirePaymentMethod = true, - ): Promise { - return await this.stripe.checkout.sessions.create({ - line_items: [ - { - price: priceId, - quantity, - }, - ], - mode: 'subscription', - subscription_data: { - metadata: { - workspaceId, - plan, - }, - trial_period_days: this.environmentService.get( - 'BILLING_FREE_TRIAL_DURATION_IN_DAYS', - ), - }, - automatic_tax: { enabled: !!requirePaymentMethod }, - tax_id_collection: { enabled: !!requirePaymentMethod }, - customer: stripeCustomerId, - customer_update: stripeCustomerId ? { name: 'auto' } : undefined, - customer_email: stripeCustomerId ? undefined : user.email, - success_url: successUrl, - cancel_url: cancelUrl, - payment_method_collection: requirePaymentMethod - ? 'always' - : 'if_required', - }); - } - - async collectLastInvoice(stripeSubscriptionId: string) { - const subscription = await this.stripe.subscriptions.retrieve( - stripeSubscriptionId, - { expand: ['latest_invoice'] }, - ); - const latestInvoice = subscription.latest_invoice; - - if ( - !( - latestInvoice && - typeof latestInvoice !== 'string' && - latestInvoice.status === 'draft' - ) - ) { - return; - } - await this.stripe.invoices.pay(latestInvoice.id); - } - - async updateBillingSubscriptionItem( - stripeSubscriptionItem: BillingSubscriptionItem, - stripePriceId: string, - ) { - await this.stripe.subscriptionItems.update( - stripeSubscriptionItem.stripeSubscriptionItemId, - { - price: stripePriceId, - quantity: - stripeSubscriptionItem.quantity === null - ? undefined - : stripeSubscriptionItem.quantity, - }, - ); - } - - async updateCustomerMetadataWorkspaceId( - stripeCustomerId: string, - workspaceId: string, - ) { - await this.stripe.customers.update(stripeCustomerId, { - metadata: { workspaceId: workspaceId }, - }); - } - - async getMeter(stripeMeterId: string) { - return await this.stripe.billing.meters.retrieve(stripeMeterId); - } - - formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] { - const productPrices: ProductPriceEntity[] = Object.values( - prices - .filter((item) => item.recurring?.interval && item.unit_amount) - .reduce((acc, item: Stripe.Price) => { - const interval = item.recurring?.interval; - - if (!interval || !item.unit_amount) { - return acc; - } - - if (!acc[interval] || item.created > acc[interval].created) { - acc[interval] = { - unitAmount: item.unit_amount, - recurringInterval: interval, - created: item.created, - stripePriceId: item.id, - }; - } - - return acc satisfies Record; - }, {}), - ); - - return productPrices.sort((a, b) => a.unitAmount - b.unitAmount); - } - - async getStripeCustomerIdFromWorkspaceId(workspaceId: string) { - const subscription = await this.stripe.subscriptions.search({ - query: `metadata['workspaceId']:'${workspaceId}'`, - limit: 1, - }); - const stripeCustomerId = subscription.data[0].customer - ? String(subscription.data[0].customer) - : undefined; - - return stripeCustomerId; - } - - async getAllProducts() { - const products = await this.stripe.products.list(); - - return products.data; - } - - async getPricesByProductId(productId: string) { - const prices = await this.stripe.prices.search({ - query: `product:'${productId}'`, - }); - - return prices.data; - } - - async getAllMeters() { - const meters = await this.stripe.billing.meters.list(); - - return meters.data; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/is-stripe-valid-product-metadata.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/is-stripe-valid-product-metadata.util.spec.ts new file mode 100644 index 000000000000..0cbc5f5d87a4 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/is-stripe-valid-product-metadata.util.spec.ts @@ -0,0 +1,56 @@ +import Stripe from 'stripe'; + +import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum'; +import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum'; +import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util'; +describe('isStripeValidProductMetadata', () => { + it('should return true if metadata is empty', () => { + const metadata: Stripe.Metadata = {}; + + expect(isStripeValidProductMetadata(metadata)).toBe(true); + }); + it('should return true if metadata has the correct keys with correct values', () => { + const metadata: Stripe.Metadata = { + planKey: BillingPlanKey.PRO, + priceUsageBased: BillingUsageType.METERED, + }; + + expect(isStripeValidProductMetadata(metadata)).toBe(true); + }); + + it('should return true if metadata has extra keys', () => { + const metadata: Stripe.Metadata = { + planKey: BillingPlanKey.ENTERPRISE, + priceUsageBased: BillingUsageType.METERED, + randomKey: 'randomValue', + }; + + expect(isStripeValidProductMetadata(metadata)).toBe(true); + }); + + it('should return false if metadata has invalid keys', () => { + const metadata: Stripe.Metadata = { + planKey: 'invalid', + priceUsageBased: BillingUsageType.METERED, + }; + + expect(isStripeValidProductMetadata(metadata)).toBe(false); + }); + + it('should return false if metadata has invalid values', () => { + const metadata: Stripe.Metadata = { + planKey: BillingPlanKey.PRO, + priceUsageBased: 'invalid', + }; + + expect(isStripeValidProductMetadata(metadata)).toBe(false); + }); + + it('should return false if the metadata does not have the required keys', () => { + const metadata: Stripe.Metadata = { + randomKey: 'randomValue', + }; + + expect(isStripeValidProductMetadata(metadata)).toBe(false); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.spec.ts new file mode 100644 index 000000000000..36b87bdd51ea --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.spec.ts @@ -0,0 +1,84 @@ +import Stripe from 'stripe'; + +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; +import { transformStripeEntitlementUpdatedEventToEntitlementRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util'; + +describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', () => { + it('should return the SSO key with true value', () => { + const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = { + object: { + customer: 'cus_123', + entitlements: { + data: [ + { + lookup_key: 'SSO', + feature: 'SSO', + livemode: false, + id: 'ent_123', + object: 'entitlements.active_entitlement', + }, + ], + object: 'list', + has_more: false, + url: '', + }, + livemode: false, + object: 'entitlements.active_entitlement_summary', + }, + }; + + const result = + transformStripeEntitlementUpdatedEventToEntitlementRepositoryData( + 'workspaceId', + data, + ); + + expect(result).toEqual([ + { + workspaceId: 'workspaceId', + key: BillingEntitlementKey.SSO, + value: true, + stripeCustomerId: 'cus_123', + }, + ]); + }); + + it('should return the SSO key with false value,should only render the values that are listed in BillingEntitlementKeys', () => { + const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = { + object: { + customer: 'cus_123', + entitlements: { + data: [ + { + id: 'ent_123', + object: 'entitlements.active_entitlement', + lookup_key: 'DIFFERENT_KEY', + feature: 'DIFFERENT_FEATURE', + livemode: false, + }, + ], + object: 'list', + has_more: false, + url: '', + }, + livemode: false, + object: 'entitlements.active_entitlement_summary', + }, + }; + + const result = + transformStripeEntitlementUpdatedEventToEntitlementRepositoryData( + 'workspaceId', + data, + ); + + expect(result).toEqual([ + { + workspaceId: 'workspaceId', + key: BillingEntitlementKey.SSO, + value: false, + stripeCustomerId: 'cus_123', + }, + ]); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-data-to-meter-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-data-to-meter-repository-data.util.spec.ts new file mode 100644 index 000000000000..912bc557c511 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-data-to-meter-repository-data.util.spec.ts @@ -0,0 +1,94 @@ +import Stripe from 'stripe'; + +import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum'; +import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum'; +import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util'; + +describe('transformStripeMeterDataToMeterRepositoryData', () => { + it('should return the correct data with customer mapping', () => { + const data: Stripe.Billing.Meter = { + id: 'met_123', + object: 'billing.meter', + created: 1719859200, + display_name: 'Meter 1', + event_name: 'event_1', + status: 'active', + customer_mapping: { + event_payload_key: 'event_payload_key_1', + type: 'by_id', + }, + default_aggregation: { + formula: 'count', + }, + event_time_window: 'day', + livemode: false, + status_transitions: { + deactivated_at: null, + }, + updated: 1719859200, + value_settings: { + event_payload_key: 'event_payload_key_1', + }, + }; + + const result = transformStripeMeterDataToMeterRepositoryData(data); + + expect(result).toEqual({ + stripeMeterId: 'met_123', + displayName: 'Meter 1', + eventName: 'event_1', + status: BillingMeterStatus.ACTIVE, + customerMapping: { + event_payload_key: 'event_payload_key_1', + type: 'by_id', + }, + eventTimeWindow: BillingMeterEventTimeWindow.DAY, + valueSettings: { + event_payload_key: 'event_payload_key_1', + }, + }); + }); + it('should return the correct data with null values', () => { + const data: Stripe.Billing.Meter = { + id: 'met_1234', + object: 'billing.meter', + created: 1719859200, + display_name: 'Meter 2', + event_name: 'event_2', + status: 'inactive', + customer_mapping: { + event_payload_key: 'event_payload_key_2', + type: 'by_id', + }, + default_aggregation: { + formula: 'sum', + }, + event_time_window: null, + livemode: false, + status_transitions: { + deactivated_at: 1719859200, + }, + updated: 1719859200, + value_settings: { + event_payload_key: 'event_payload_key_2', + }, + }; + + const result = transformStripeMeterDataToMeterRepositoryData(data); + + expect(result).toEqual({ + stripeMeterId: 'met_1234', + displayName: 'Meter 2', + eventName: 'event_2', + status: BillingMeterStatus.INACTIVE, + customerMapping: { + event_payload_key: 'event_payload_key_2', + type: 'by_id', + }, + eventTimeWindow: undefined, + valueSettings: { + event_payload_key: 'event_payload_key_2', + }, + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-data-to-price-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-data-to-price-repository-data.util.spec.ts new file mode 100644 index 000000000000..81e8e24df4fd --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-data-to-price-repository-data.util.spec.ts @@ -0,0 +1,219 @@ +import Stripe from 'stripe'; + +import { BillingPriceBillingScheme } from 'src/engine/core-modules/billing/enums/billing-price-billing-scheme.enum'; +import { BillingPriceTaxBehavior } from 'src/engine/core-modules/billing/enums/billing-price-tax-behavior.enum'; +import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum'; +import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum'; +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; +import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum'; +import { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util'; +describe('transformStripePriceDataToPriceRepositoryData', () => { + const createMockPrice = (overrides = {}): Stripe.Price => + ({ + id: 'price_123', + active: true, + product: 'prod_123', + currency: 'usd', + nickname: null, + tax_behavior: null, + type: 'recurring', + billing_scheme: 'per_unit', + unit_amount_decimal: '1000', + unit_amount: 1000, + transform_quantity: null, + recurring: { + usage_type: 'licensed', + interval: 'month', + meter: null, + }, + currency_options: null, + tiers: null, + tiers_mode: null, + ...overrides, + }) as unknown as Stripe.Price; + + it('should transform basic price data correctly', () => { + const mockPrice = createMockPrice(); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result).toEqual({ + stripePriceId: 'price_123', + active: true, + stripeProductId: 'prod_123', + stripeMeterId: null, + currency: 'USD', + nickname: undefined, + taxBehavior: undefined, + type: BillingPriceType.RECURRING, + billingScheme: BillingPriceBillingScheme.PER_UNIT, + unitAmountDecimal: '1000', + unitAmount: 1000, + transformQuantity: undefined, + usageType: BillingUsageType.LICENSED, + interval: SubscriptionInterval.Month, + currencyOptions: undefined, + tiers: undefined, + tiersMode: undefined, + recurring: { + usage_type: 'licensed', + interval: 'month', + meter: null, + }, + }); + }); + + describe('tax behavior transformations', () => { + it.each([ + ['exclusive', BillingPriceTaxBehavior.EXCLUSIVE], + ['inclusive', BillingPriceTaxBehavior.INCLUSIVE], + ['unspecified', BillingPriceTaxBehavior.UNSPECIFIED], + ])( + 'should transform tax behavior %s correctly', + (stripeTaxBehavior, expected) => { + const mockPrice = createMockPrice({ + tax_behavior: stripeTaxBehavior as Stripe.Price.TaxBehavior, + }); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result.taxBehavior).toBe(expected); + }, + ); + }); + + describe('price type transformations', () => { + it.each([ + ['one_time', BillingPriceType.ONE_TIME], + ['recurring', BillingPriceType.RECURRING], + ])('should transform price type %s correctly', (stripeType, expected) => { + const mockPrice = createMockPrice({ + type: stripeType as Stripe.Price.Type, + }); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result.type).toBe(expected); + }); + }); + + describe('billing scheme transformations', () => { + it.each([ + ['per_unit', BillingPriceBillingScheme.PER_UNIT], + ['tiered', BillingPriceBillingScheme.TIERED], + ])( + 'should transform billing scheme %s correctly', + (stripeScheme, expected) => { + const mockPrice = createMockPrice({ + billing_scheme: stripeScheme as Stripe.Price.BillingScheme, + }); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result.billingScheme).toBe(expected); + }, + ); + }); + + describe('recurring price configurations', () => { + it('should handle metered pricing with meter ID', () => { + const mockPrice = createMockPrice({ + recurring: { + usage_type: 'metered', + interval: 'month', + meter: 'meter_123', + }, + }); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result.stripeMeterId).toBe('meter_123'); + expect(result.usageType).toBe(BillingUsageType.METERED); + }); + + it.each([ + ['month', SubscriptionInterval.Month], + ['day', SubscriptionInterval.Day], + ['week', SubscriptionInterval.Week], + ['year', SubscriptionInterval.Year], + ])('should transform interval %s correctly', (stripeInterval, expected) => { + const mockPrice = createMockPrice({ + recurring: { + usage_type: 'licensed', + interval: stripeInterval as Stripe.Price.Recurring.Interval, + meter: null, + }, + }); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result.interval).toBe(expected); + }); + }); + + describe('tiered pricing configurations', () => { + const mockTiers = [ + { up_to: 10, unit_amount: 1000 }, + { up_to: 20, unit_amount: 800 }, + ]; + + it.each([ + ['graduated', BillingPriceTiersMode.GRADUATED], + ['volume', BillingPriceTiersMode.VOLUME], + ])( + 'should transform tiers mode %s correctly', + (stripeTiersMode, expected) => { + const mockPrice = createMockPrice({ + billing_scheme: 'tiered', + tiers: mockTiers, + tiers_mode: stripeTiersMode as Stripe.Price.TiersMode, + }); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result.tiersMode).toBe(expected); + expect(result.tiers).toEqual(mockTiers); + }, + ); + }); + + describe('optional fields handling', () => { + it('should handle transform quantity configuration', () => { + const transformQuantity = { + divide_by: 100, + round: 'up', + }; + const mockPrice = createMockPrice({ + transform_quantity: transformQuantity, + }); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result.transformQuantity).toEqual(transformQuantity); + }); + + it('should handle currency options', () => { + const currencyOptions = { + eur: { + unit_amount: 850, + unit_amount_decimal: '850', + }, + }; + const mockPrice = createMockPrice({ currency_options: currencyOptions }); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result.currencyOptions).toEqual(currencyOptions); + }); + + it('should handle null and undefined fields correctly', () => { + const mockPrice = createMockPrice({ + nickname: null, + unit_amount: null, + unit_amount_decimal: null, + transform_quantity: null, + tiers: null, + currency_options: null, + }); + const result = transformStripePriceDataToPriceRepositoryData(mockPrice); + + expect(result.nickname).toBeUndefined(); + expect(result.unitAmount).toBeUndefined(); + expect(result.unitAmountDecimal).toBeUndefined(); + expect(result.transformQuantity).toBeUndefined(); + expect(result.tiers).toBeUndefined(); + expect(result.currencyOptions).toBeUndefined(); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-event-to-price-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-event-to-price-repository-data.util.spec.ts new file mode 100644 index 000000000000..5faa1385b36b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-event-to-price-repository-data.util.spec.ts @@ -0,0 +1,234 @@ +import { BillingPriceBillingScheme } from 'src/engine/core-modules/billing/enums/billing-price-billing-scheme.enum'; +import { BillingPriceTaxBehavior } from 'src/engine/core-modules/billing/enums/billing-price-tax-behavior.enum'; +import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum'; +import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum'; +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; +import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum'; +import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util'; + +describe('transformStripePriceEventToPriceRepositoryData', () => { + const createMockPriceData = (overrides = {}) => ({ + object: { + id: 'price_123', + active: true, + product: 'prod_123', + meter: null, + currency: 'usd', + nickname: null, + tax_behavior: null, + type: 'recurring', + billing_scheme: 'per_unit', + unit_amount_decimal: '1000', + unit_amount: 1000, + transform_quantity: null, + recurring: { + usage_type: 'licensed', + interval: 'month', + }, + currency_options: null, + tiers: null, + tiers_mode: null, + ...overrides, + }, + }); + + it('should transform basic price data correctly', () => { + const mockData = createMockPriceData(); + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result).toEqual({ + stripePriceId: 'price_123', + active: true, + stripeProductId: 'prod_123', + stripeMeterId: undefined, + currency: 'USD', + nickname: undefined, + taxBehavior: undefined, + type: BillingPriceType.RECURRING, + billingScheme: BillingPriceBillingScheme.PER_UNIT, + unitAmountDecimal: '1000', + unitAmount: 1000, + transformQuantity: undefined, + usageType: BillingUsageType.LICENSED, + interval: SubscriptionInterval.Month, + currencyOptions: undefined, + tiers: undefined, + tiersMode: undefined, + recurring: { + usage_type: 'licensed', + interval: 'month', + }, + }); + }); + + it('should handle all tax behaviors correctly', () => { + const taxBehaviors = [ + ['exclusive', BillingPriceTaxBehavior.EXCLUSIVE], + ['inclusive', BillingPriceTaxBehavior.INCLUSIVE], + ['unspecified', BillingPriceTaxBehavior.UNSPECIFIED], + ]; + + taxBehaviors.forEach(([stripeTaxBehavior, expectedTaxBehavior]) => { + const mockData = createMockPriceData({ + tax_behavior: stripeTaxBehavior, + }); + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result.taxBehavior).toBe(expectedTaxBehavior); + }); + }); + + it('should handle all price types correctly', () => { + const priceTypes = [ + ['one_time', BillingPriceType.ONE_TIME], + ['recurring', BillingPriceType.RECURRING], + ]; + + priceTypes.forEach(([stripeType, expectedType]) => { + const mockData = createMockPriceData({ type: stripeType }); + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result.type).toBe(expectedType); + }); + }); + + it('should handle all billing schemes correctly', () => { + const billingSchemes = [ + ['per_unit', BillingPriceBillingScheme.PER_UNIT], + ['tiered', BillingPriceBillingScheme.TIERED], + ]; + + billingSchemes.forEach(([stripeScheme, expectedScheme]) => { + const mockData = createMockPriceData({ billing_scheme: stripeScheme }); + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result.billingScheme).toBe(expectedScheme); + }); + }); + + it('should handle all usage types correctly', () => { + const usageTypes = [ + ['licensed', BillingUsageType.LICENSED], + ['metered', BillingUsageType.METERED], + ]; + + usageTypes.forEach(([stripeUsageType, expectedUsageType]) => { + const mockData = createMockPriceData({ + recurring: { usage_type: stripeUsageType, interval: 'month' }, + }); + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result.usageType).toBe(expectedUsageType); + }); + }); + + it('should handle all tiers modes correctly', () => { + const tiersModes = [ + ['graduated', BillingPriceTiersMode.GRADUATED], + ['volume', BillingPriceTiersMode.VOLUME], + ]; + + tiersModes.forEach(([stripeTiersMode, expectedTiersMode]) => { + const mockData = createMockPriceData({ tiers_mode: stripeTiersMode }); + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result.tiersMode).toBe(expectedTiersMode); + }); + }); + + it('should handle all intervals correctly', () => { + const intervals = [ + ['month', SubscriptionInterval.Month], + ['day', SubscriptionInterval.Day], + ['week', SubscriptionInterval.Week], + ['year', SubscriptionInterval.Year], + ]; + + intervals.forEach(([stripeInterval, expectedInterval]) => { + const mockData = createMockPriceData({ + recurring: { usage_type: 'licensed', interval: stripeInterval }, + }); + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result.interval).toBe(expectedInterval); + }); + }); + + it('should handle tiered pricing configuration', () => { + const mockTiers = [ + { up_to: 10, unit_amount: 1000 }, + { up_to: 20, unit_amount: 800 }, + ]; + + const mockData = createMockPriceData({ + billing_scheme: 'tiered', + tiers: mockTiers, + tiers_mode: 'graduated', + }); + + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result.billingScheme).toBe(BillingPriceBillingScheme.TIERED); + expect(result.tiers).toEqual(mockTiers); + expect(result.tiersMode).toBe(BillingPriceTiersMode.GRADUATED); + }); + + it('should handle metered pricing with transform quantity', () => { + const mockTransformQuantity = { + divide_by: 100, + round: 'up', + }; + + const mockData = createMockPriceData({ + recurring: { + usage_type: 'metered', + interval: 'month', + meter: 'meter_123', + }, + transform_quantity: mockTransformQuantity, + }); + + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result.stripeMeterId).toBe('meter_123'); + expect(result.usageType).toBe(BillingUsageType.METERED); + expect(result.transformQuantity).toEqual(mockTransformQuantity); + }); + + it('should handle currency options', () => { + const mockCurrencyOptions = { + eur: { + unit_amount: 850, + unit_amount_decimal: '850', + }, + }; + + const mockData = createMockPriceData({ + currency_options: mockCurrencyOptions, + }); + + const result = transformStripePriceEventToPriceRepositoryData( + mockData as any, + ); + + expect(result.currencyOptions).toEqual(mockCurrencyOptions); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-data-to-product-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-data-to-product-repository-data.util.spec.ts new file mode 100644 index 000000000000..875a19de032e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-data-to-product-repository-data.util.spec.ts @@ -0,0 +1,86 @@ +import Stripe from 'stripe'; + +import { transformStripeProductDataToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util'; +describe('transformStripeProductDataToProductRepositoryData', () => { + it('should return the correct data', () => { + const data: Stripe.Product = { + id: 'prod_123', + name: 'Product 1', + active: true, + description: 'Description 1', + images: ['image1.jpg', 'image2.jpg'], + marketing_features: [ + { + name: 'feature1', + }, + ], + created: 1719859200, + updated: 1719859200, + type: 'service', + livemode: false, + package_dimensions: null, + shippable: false, + object: 'product', + default_price: 'price_123', + unit_label: 'Unit', + url: 'https://example.com', + tax_code: 'tax_code_1', + metadata: { key: 'value' }, + }; + + const result = transformStripeProductDataToProductRepositoryData(data); + + expect(result).toEqual({ + stripeProductId: 'prod_123', + name: 'Product 1', + active: true, + description: 'Description 1', + images: ['image1.jpg', 'image2.jpg'], + marketingFeatures: [{ name: 'feature1' }], + defaultStripePriceId: 'price_123', + unitLabel: 'Unit', + url: 'https://example.com', + taxCode: 'tax_code_1', + metadata: { key: 'value' }, + }); + }); + + it('should return the correct data with null values', () => { + const data: Stripe.Product = { + id: 'prod_456', + name: 'Product 2', + active: false, + description: '', + images: [], + created: 1719859200, + updated: 1719859200, + type: 'service', + livemode: false, + package_dimensions: null, + shippable: false, + object: 'product', + marketing_features: [], + default_price: null, + unit_label: null, + url: null, + tax_code: null, + metadata: {}, + }; + + const result = transformStripeProductDataToProductRepositoryData(data); + + expect(result).toEqual({ + stripeProductId: 'prod_456', + name: 'Product 2', + active: false, + description: '', + images: [], + marketingFeatures: [], + defaultStripePriceId: undefined, + unitLabel: undefined, + url: undefined, + taxCode: undefined, + metadata: {}, + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-event-to-product-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-event-to-product-repository-data.util.spec.ts new file mode 100644 index 000000000000..2a858a781bdd --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-event-to-product-repository-data.util.spec.ts @@ -0,0 +1,89 @@ +import Stripe from 'stripe'; + +import { transformStripeProductEventToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util'; + +describe('transformStripeProductEventToProductRepositoryData', () => { + it('should return the correct data', () => { + const data: Stripe.ProductCreatedEvent.Data = { + object: { + id: 'prod_123', + name: 'Product 1', + active: true, + description: 'Description 1', + images: ['image1.jpg', 'image2.jpg'], + marketing_features: [ + { + name: 'feature1', + }, + ], + created: 1719859200, + updated: 1719859200, + type: 'service', + livemode: false, + package_dimensions: null, + shippable: false, + object: 'product', + default_price: 'price_123', + unit_label: 'Unit', + url: 'https://example.com', + tax_code: 'tax_code_1', + metadata: { key: 'value' }, + }, + }; + + const result = transformStripeProductEventToProductRepositoryData(data); + + expect(result).toEqual({ + stripeProductId: 'prod_123', + name: 'Product 1', + active: true, + description: 'Description 1', + images: ['image1.jpg', 'image2.jpg'], + marketingFeatures: [{ name: 'feature1' }], + defaultStripePriceId: 'price_123', + unitLabel: 'Unit', + url: 'https://example.com', + taxCode: 'tax_code_1', + }); + }); + + it('should return the correct data with null values', () => { + const data: Stripe.ProductUpdatedEvent.Data = { + object: { + id: 'prod_456', + name: 'Product 2', + object: 'product', + active: false, + description: '', + images: [], + created: 1719859200, + updated: 1719859200, + type: 'service', + livemode: false, + package_dimensions: null, + shippable: false, + marketing_features: [], + default_price: null, + unit_label: null, + url: null, + tax_code: null, + metadata: {}, + }, + }; + + const result = transformStripeProductEventToProductRepositoryData(data); + + expect(result).toEqual({ + stripeProductId: 'prod_456', + name: 'Product 2', + active: false, + description: '', + images: [], + marketingFeatures: [], + defaultStripePriceId: undefined, + unitLabel: undefined, + url: undefined, + taxCode: undefined, + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-customer-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-customer-repository-data.util.spec.ts new file mode 100644 index 000000000000..be841607b25a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-customer-repository-data.util.spec.ts @@ -0,0 +1,85 @@ +import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util'; + +describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => { + const mockWorkspaceId = 'workspace_123'; + const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC + + const createMockSubscriptionData = (overrides = {}) => ({ + object: { + id: 'sub_123', + customer: 'cus_123', + status: 'active', + items: { + data: [ + { + plan: { + interval: 'month', + }, + }, + ], + }, + cancel_at_period_end: false, + currency: 'usd', + current_period_end: mockTimestamp, + current_period_start: mockTimestamp - 2592000, // 30 days before end + metadata: {}, + collection_method: 'charge_automatically', + automatic_tax: null, + cancellation_details: null, + ended_at: null, + trial_start: null, + trial_end: null, + cancel_at: null, + canceled_at: null, + ...overrides, + }, + }); + + it('should transform basic customer data correctly', () => { + const mockData = createMockSubscriptionData('cus_123'); + + const result = transformStripeSubscriptionEventToCustomerRepositoryData( + mockWorkspaceId, + mockData as any, + ); + + expect(result).toEqual({ + workspaceId: 'workspace_123', + stripeCustomerId: 'cus_123', + }); + }); + + it('should work with different subscription event types', () => { + const mockData = createMockSubscriptionData('cus_123'); + + // Test with different event types (they should all transform the same way) + ['updated', 'created', 'deleted'].forEach(() => { + const result = transformStripeSubscriptionEventToCustomerRepositoryData( + mockWorkspaceId, + mockData as any, + ); + + expect(result).toEqual({ + workspaceId: 'workspace_123', + stripeCustomerId: 'cus_123', + }); + }); + }); + + it('should handle different workspace IDs', () => { + const mockData = createMockSubscriptionData('cus_123'); + const testWorkspaces = ['workspace_1', 'workspace_2', 'workspace_abc']; + + testWorkspaces.forEach((testWorkspaceId) => { + const result = transformStripeSubscriptionEventToCustomerRepositoryData( + testWorkspaceId, + mockData as any, + ); + + expect(result).toEqual({ + workspaceId: testWorkspaceId, + stripeCustomerId: 'cus_123', + }); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-subscription-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-subscription-repository-data.util.spec.ts new file mode 100644 index 000000000000..6c4a6cdb0eb1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-subscription-repository-data.util.spec.ts @@ -0,0 +1,197 @@ +import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; +import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util'; + +describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => { + const mockWorkspaceId = 'workspace-123'; + const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC + + const createMockSubscriptionData = (overrides = {}) => ({ + object: { + id: 'sub_123', + customer: 'cus_123', + status: 'active', + items: { + data: [ + { + plan: { + interval: 'month', + }, + }, + ], + }, + cancel_at_period_end: false, + currency: 'usd', + current_period_end: mockTimestamp, + current_period_start: mockTimestamp - 2592000, // 30 days before end + metadata: {}, + collection_method: 'charge_automatically', + automatic_tax: null, + cancellation_details: null, + ended_at: null, + trial_start: null, + trial_end: null, + cancel_at: null, + canceled_at: null, + ...overrides, + }, + }); + + it('should transform basic subscription data correctly', () => { + const mockData = createMockSubscriptionData(); + const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( + mockWorkspaceId, + mockData as any, + ); + + expect(result).toEqual({ + workspaceId: mockWorkspaceId, + stripeCustomerId: 'cus_123', + stripeSubscriptionId: 'sub_123', + status: SubscriptionStatus.Active, + interval: 'month', + cancelAtPeriodEnd: false, + currency: 'USD', + currentPeriodEnd: new Date(mockTimestamp * 1000), + currentPeriodStart: new Date((mockTimestamp - 2592000) * 1000), + metadata: {}, + collectionMethod: + BillingSubscriptionCollectionMethod.CHARGE_AUTOMATICALLY, + automaticTax: undefined, + cancellationDetails: undefined, + endedAt: undefined, + trialStart: undefined, + trialEnd: undefined, + cancelAt: undefined, + canceledAt: undefined, + }); + }); + + it('should handle all subscription statuses correctly', () => { + const statuses = [ + ['active', SubscriptionStatus.Active], + ['canceled', SubscriptionStatus.Canceled], + ['incomplete', SubscriptionStatus.Incomplete], + ['incomplete_expired', SubscriptionStatus.IncompleteExpired], + ['past_due', SubscriptionStatus.PastDue], + ['paused', SubscriptionStatus.Paused], + ['trialing', SubscriptionStatus.Trialing], + ['unpaid', SubscriptionStatus.Unpaid], + ]; + + statuses.forEach(([stripeStatus, expectedStatus]) => { + const mockData = createMockSubscriptionData({ + status: stripeStatus, + }); + const result = + transformStripeSubscriptionEventToSubscriptionRepositoryData( + mockWorkspaceId, + mockData as any, + ); + + expect(result.status).toBe(expectedStatus); + }); + }); + + it('should handle subscription with trial periods', () => { + const trialStart = mockTimestamp - 604800; // 7 days before + const trialEnd = mockTimestamp + 604800; // 7 days after + + const mockData = createMockSubscriptionData({ + trial_start: trialStart, + trial_end: trialEnd, + }); + + const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( + mockWorkspaceId, + mockData as any, + ); + + expect(result.trialStart).toEqual(new Date(trialStart * 1000)); + expect(result.trialEnd).toEqual(new Date(trialEnd * 1000)); + }); + + it('should handle subscription cancellation details', () => { + const cancelAt = mockTimestamp + 2592000; // 30 days after + const canceledAt = mockTimestamp; + const mockData = createMockSubscriptionData({ + cancel_at: cancelAt, + canceled_at: canceledAt, + cancel_at_period_end: true, + cancellation_details: { + comment: 'Customer requested cancellation', + feedback: 'too_expensive', + reason: 'customer_request', + }, + }); + + const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( + mockWorkspaceId, + mockData as any, + ); + + expect(result.cancelAt).toEqual(new Date(cancelAt * 1000)); + expect(result.canceledAt).toEqual(new Date(canceledAt * 1000)); + expect(result.cancelAtPeriodEnd).toBe(true); + expect(result.cancellationDetails).toEqual({ + comment: 'Customer requested cancellation', + feedback: 'too_expensive', + reason: 'customer_request', + }); + }); + + it('should handle automatic tax information', () => { + const mockData = createMockSubscriptionData({ + automatic_tax: { + enabled: true, + status: 'calculated', + }, + }); + + const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( + mockWorkspaceId, + mockData as any, + ); + + expect(result.automaticTax).toEqual({ + enabled: true, + status: 'calculated', + }); + }); + + it('should handle different collection methods', () => { + const methods = [ + [ + 'charge_automatically', + BillingSubscriptionCollectionMethod.CHARGE_AUTOMATICALLY, + ], + ['send_invoice', BillingSubscriptionCollectionMethod.SEND_INVOICE], + ]; + + methods.forEach(([stripeMethod, expectedMethod]) => { + const mockData = createMockSubscriptionData({ + collection_method: stripeMethod, + }); + const result = + transformStripeSubscriptionEventToSubscriptionRepositoryData( + mockWorkspaceId, + mockData as any, + ); + + expect(result.collectionMethod).toBe(expectedMethod); + }); + }); + + it('should handle different currencies', () => { + const mockData = createMockSubscriptionData({ + currency: 'eur', + }); + + const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( + mockWorkspaceId, + mockData as any, + ); + + expect(result.currency).toBe('EUR'); + }); +}); diff --git a/packages/twenty-server/test/integration/billing/suites/billing-controller.integration-spec.ts b/packages/twenty-server/test/integration/billing/suites/billing-controller.integration-spec.ts new file mode 100644 index 000000000000..b5b74fb90ea2 --- /dev/null +++ b/packages/twenty-server/test/integration/billing/suites/billing-controller.integration-spec.ts @@ -0,0 +1,112 @@ +import request from 'supertest'; +import { createMockStripeEntitlementUpdatedData } from 'test/integration/billing/utils/create-mock-stripe-entitlement-updated-data.util'; +import { createMockStripePriceCreatedData } from 'test/integration/billing/utils/create-mock-stripe-price-created-data.util'; +import { createMockStripeProductUpdatedData } from 'test/integration/billing/utils/create-mock-stripe-product-updated-data.util'; +import { createMockStripeSubscriptionCreatedData } from 'test/integration/billing/utils/create-mock-stripe-subscription-created-data.util'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('BillingController (integration)', () => { + it('should handle product.updated and price.created webhook events', async () => { + const productUpdatedPayload = { + type: 'product.updated', + data: createMockStripeProductUpdatedData(), + }; + const priceCreatedPayload = { + type: 'price.created', + data: createMockStripePriceCreatedData(), + }; + + await client + .post('/billing/webhooks') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .set('stripe-signature', 'correct-signature') + .set('Content-Type', 'application/json') + .send(JSON.stringify(productUpdatedPayload)) + .expect(200) + .then((res) => { + expect(res.body.stripeProductId).toBeDefined(); + }); + + await client + .post('/billing/webhooks') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .set('stripe-signature', 'correct-signature') + .set('Content-Type', 'application/json') + .send(JSON.stringify(priceCreatedPayload)) + .expect(200) + .then((res) => { + expect(res.body.stripePriceId).toBeDefined(); + expect(res.body.stripeMeterId).toBeDefined(); + }); + }); + it('should handle subscription.created webhook event', async () => { + const subscriptionCreatedPayload = { + type: 'customer.subscription.created', + data: createMockStripeSubscriptionCreatedData(), + }; + const entitlementUpdatedPayload = { + type: 'entitlements.active_entitlement_summary.updated', + data: createMockStripeEntitlementUpdatedData(), + }; + + await client + .post('/billing/webhooks') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .set('stripe-signature', 'correct-signature') + .set('Content-Type', 'application/json') + .send(JSON.stringify(subscriptionCreatedPayload)) + .expect(200) + .then((res) => { + expect(res.body.stripeSubscriptionId).toBeDefined(); + expect(res.body.stripeCustomerId).toBeDefined(); + }); + + await client + .post('/billing/webhooks') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .set('stripe-signature', 'correct-signature') + .set('Content-Type', 'application/json') + .send(JSON.stringify(entitlementUpdatedPayload)) + .expect(200) + .then((res) => { + expect(res.body.stripeEntitlementCustomerId).toBeDefined(); + }); + }); + + it('should handle entitlements.active_entitlement_summary.updated when the subscription is not found', async () => { + const entitlementUpdatedPayload = { + type: 'entitlements.active_entitlement_summary.updated', + data: createMockStripeEntitlementUpdatedData({ + customer: 'new_customer', + }), + }; + + await client + .post('/billing/webhooks') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .set('stripe-signature', 'correct-signature') + .set('Content-Type', 'application/json') + .send(JSON.stringify(entitlementUpdatedPayload)) + .expect(404); + }); + + it('should reject webhook with invalid signature', async () => { + const entitlementUpdatedPayload = { + type: 'customer.entitlement.created', + data: { + object: { + id: 'ent_test123', + }, + }, + }; + + await client + .post('/billing/webhooks') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .set('stripe-signature', 'invalid-signature') + .set('Content-Type', 'application/json') + .send(JSON.stringify(entitlementUpdatedPayload)) + .expect(500); + }); +}); diff --git a/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-entitlement-updated-data.util.ts b/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-entitlement-updated-data.util.ts new file mode 100644 index 000000000000..1a7e8b7f6f0b --- /dev/null +++ b/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-entitlement-updated-data.util.ts @@ -0,0 +1,25 @@ +import Stripe from 'stripe'; +export const createMockStripeEntitlementUpdatedData = ( + overrides = {}, +): Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data => ({ + object: { + object: 'entitlements.active_entitlement_summary', + customer: 'cus_default1', + livemode: false, + entitlements: { + object: 'list', + data: [ + { + id: 'ent_test_61', + object: 'entitlements.active_entitlement', + feature: 'feat_test_61', + livemode: false, + lookup_key: 'SSO', + }, + ], + has_more: false, + url: '/v1/customer/cus_Q/entitlements', + }, + ...overrides, + }, +}); diff --git a/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-price-created-data.util.ts b/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-price-created-data.util.ts new file mode 100644 index 000000000000..65890e8c8dd4 --- /dev/null +++ b/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-price-created-data.util.ts @@ -0,0 +1,34 @@ +import Stripe from 'stripe'; +export const createMockStripePriceCreatedData = ( + overrides = {}, +): Stripe.PriceCreatedEvent.Data => ({ + object: { + id: 'price_1Q', + object: 'price', + active: true, + billing_scheme: 'per_unit', + created: 1733734326, + currency: 'usd', + custom_unit_amount: null, + livemode: false, + lookup_key: null, + metadata: {}, + nickname: null, + product: 'prod_RLN', + recurring: { + aggregate_usage: null, + interval: 'month', + interval_count: 1, + meter: null, + trial_period_days: null, + usage_type: 'licensed', + }, + tax_behavior: 'unspecified', + tiers_mode: null, + transform_quantity: null, + type: 'recurring', + unit_amount: 0, + unit_amount_decimal: '0', + ...overrides, + }, +}); diff --git a/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-product-updated-data.util.ts b/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-product-updated-data.util.ts new file mode 100644 index 000000000000..b65f70da15b8 --- /dev/null +++ b/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-product-updated-data.util.ts @@ -0,0 +1,32 @@ +import Stripe from 'stripe'; + +export const createMockStripeProductUpdatedData = ( + overrides = {}, +): Stripe.ProductUpdatedEvent.Data => ({ + object: { + id: 'prod_RLN', + object: 'product', + active: true, + created: 1733410584, + default_price: null, + description: null, + images: [], + livemode: false, + marketing_features: [], + metadata: {}, + name: 'kjnnjkjknkjnjkn', + package_dimensions: null, + shippable: null, + statement_descriptor: null, + tax_code: 'txcd_10103001', + type: 'service', + unit_label: null, + updated: 1734694649, + url: null, + }, + previous_attributes: { + default_price: 'price_1Q', + updated: 1733410585, + }, + ...overrides, +}); diff --git a/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-subscription-created-data.util.ts b/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-subscription-created-data.util.ts new file mode 100644 index 000000000000..e95a0a542494 --- /dev/null +++ b/packages/twenty-server/test/integration/billing/utils/create-mock-stripe-subscription-created-data.util.ts @@ -0,0 +1,130 @@ +import Stripe from 'stripe'; + +export const createMockStripeSubscriptionCreatedData = ( + overrides = {}, +): Stripe.CustomerSubscriptionCreatedEvent.Data => ({ + object: { + object: 'subscription', + id: 'sub_default', + customer: 'cus_default1', + status: 'active', + items: { + data: [ + { + plan: { + id: 'plan_default', + object: 'plan', + active: true, + aggregate_usage: null, + amount_decimal: '0', + billing_scheme: 'per_unit', + interval_count: 1, + livemode: false, + nickname: null, + tiers_mode: null, + transform_usage: null, + trial_period_days: null, + interval: 'month', + currency: 'usd', + amount: 0, + created: 1672531200, + product: 'prod_default', + usage_type: 'licensed', + metadata: {}, + meter: null, + }, + id: '', + object: 'subscription_item', + billing_thresholds: null, + created: 0, + discounts: [], + metadata: {}, + price: { + id: 'price_default', + object: 'price', + active: true, + billing_scheme: 'per_unit', + created: 1672531200, + currency: 'usd', + custom_unit_amount: null, + livemode: false, + lookup_key: null, + metadata: {}, + nickname: null, + product: 'prod_default', + recurring: { + aggregate_usage: null, + interval: 'month', + interval_count: 1, + meter: null, + trial_period_days: null, + usage_type: 'licensed', + }, + tax_behavior: null, + tiers_mode: null, + transform_quantity: null, + type: 'recurring', + unit_amount: 1000, + unit_amount_decimal: '1000', + }, + subscription: '', + tax_rates: null, + }, + ], + object: 'list', + has_more: false, + url: '', + }, + cancel_at_period_end: false, + currency: 'usd', + current_period_end: 1672531200, + current_period_start: 1672531200, + metadata: { workspaceId: '3b8e6458-5fc1-4e63-8563-008ccddaa6db' }, + trial_end: null, + trial_start: null, + canceled_at: null, + ...overrides, + application: null, + application_fee_percent: null, + automatic_tax: { + enabled: true, + liability: { + type: 'self', + }, + }, + billing_cycle_anchor: 0, + billing_cycle_anchor_config: null, + billing_thresholds: null, + cancel_at: null, + cancellation_details: null, + collection_method: 'charge_automatically', + created: 0, + days_until_due: null, + default_payment_method: null, + default_source: null, + description: null, + discount: null, + discounts: [], + ended_at: null, + invoice_settings: { + account_tax_ids: null, + issuer: { + type: 'self', + }, + }, + latest_invoice: null, + livemode: false, + next_pending_invoice_item_invoice: null, + on_behalf_of: null, + pause_collection: null, + payment_settings: null, + pending_invoice_item_interval: null, + pending_setup_intent: null, + pending_update: null, + schedule: null, + start_date: 0, + test_clock: null, + transfer_data: null, + trial_settings: null, + }, +}); diff --git a/packages/twenty-server/test/integration/utils/create-app.ts b/packages/twenty-server/test/integration/utils/create-app.ts index dfa136e4d4b7..b5eeabf5f3df 100644 --- a/packages/twenty-server/test/integration/utils/create-app.ts +++ b/packages/twenty-server/test/integration/utils/create-app.ts @@ -2,6 +2,8 @@ import { NestExpressApplication } from '@nestjs/platform-express'; import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing'; import { AppModule } from 'src/app.module'; +import { StripeSDKMockService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk-mock.service'; +import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; interface TestingModuleCreatePreHook { (moduleBuilder: TestingModuleBuilder): TestingModuleBuilder; @@ -23,9 +25,12 @@ export const createApp = async ( appInitHook?: TestingAppCreatePreHook; } = {}, ): Promise => { + const stripeSDKMockService = new StripeSDKMockService(); let moduleBuilder: TestingModuleBuilder = Test.createTestingModule({ imports: [AppModule], - }); + }) + .overrideProvider(StripeSDKService) + .useValue(stripeSDKMockService); if (config.moduleBuilderHook) { moduleBuilder = config.moduleBuilderHook(moduleBuilder); @@ -33,7 +38,10 @@ export const createApp = async ( const moduleFixture: TestingModule = await moduleBuilder.compile(); - const app = moduleFixture.createNestApplication(); + const app = moduleFixture.createNestApplication({ + rawBody: true, + cors: true, + }); if (config.appInitHook) { await config.appInitHook(app); From 71a4593ba432e0fb65a1b1931b070c36321fd9bb Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:43:30 +0100 Subject: [PATCH 05/22] Move FieldMetadataType to twenty-shared (#9482) Co-authored-by: Charles Bochet --- .eslintrc.cjs | 4 +++ .../0-32-simplify-search-vector-expression.ts | 6 ++-- ...date-rich-text-search-vector-expression.ts | 6 ++-- ...hone-calling-code-create-column.command.ts | 6 ++-- ...phone-calling-code-migrate-data.command.ts | 6 ++-- .../typeorm-seeds/metadata/fieldsMetadata.ts | 3 +- .../__mocks__/object-metadata-item.mock.ts | 3 +- ...ct-records-to-graphql-connection.helper.ts | 3 +- .../compute-cursor-arg-filter.spec.ts | 3 +- .../utils/compute-cursor-arg-filter.ts | 3 +- .../query-runner-args.factory.spec.ts | 3 +- .../factories/query-runner-args.factory.ts | 3 +- .../record-position-backfill-service.spec.ts | 3 +- .../record-position-backfill-service.ts | 2 +- .../composite-enum-type-definition.factory.ts | 8 ++--- ...composite-input-type-definition.factory.ts | 6 ++-- ...omposite-object-type-definition.factory.ts | 8 ++--- .../factories/input-type.factory.ts | 2 +- .../factories/output-type.factory.ts | 2 +- .../services/type-mapper.service.ts | 2 +- .../storages/type-definitions.storage.ts | 2 +- .../__tests__/get-field-metadata-type.spec.ts | 3 +- .../compute-composite-property-target.util.ts | 4 +-- .../utils/generate-fields.utils.ts | 2 +- ...le-aggregations-from-object-fields.util.ts | 7 ++-- .../utils/get-field-metadata-type.util.ts | 2 +- .../__tests__/get-field-type.utils.spec.ts | 3 +- ...ld-metadata-to-graphql-query.utils.spec.ts | 3 +- .../check-filter-enum-values.spec.ts | 5 +-- .../format-field-values.utils.spec.ts | 3 +- .../filter-utils/check-filter-enum-values.ts | 4 +-- .../filter-utils/format-field-values.utils.ts | 3 +- .../utils/get-field-type.utils.ts | 4 +-- ...p-field-metadata-to-graphql-query.utils.ts | 3 +- .../utils/object-record-changed-values.ts | 3 +- .../open-api/utils/components.utils.ts | 7 ++-- .../composite-types/actor.composite-type.ts | 4 +-- .../composite-types/address.composite-type.ts | 4 +-- .../currency.composite-type.ts | 4 +-- .../composite-types/emails.composite-type.ts | 4 +-- .../full-name.composite-type.ts | 4 +-- .../field-metadata/composite-types/index.ts | 3 +- .../composite-types/links.composite-type.ts | 4 +-- .../composite-types/phones.composite-type.ts | 4 +-- .../field-metadata/dtos/field-metadata.dto.ts | 2 +- .../field-metadata-validation.service.ts | 2 +- .../field-metadata/field-metadata.entity.ts | 32 +++--------------- .../field-metadata/field-metadata.resolver.ts | 3 +- .../field-metadata/field-metadata.service.ts | 6 ++-- .../interfaces/composite-type.interface.ts | 4 +-- .../field-metadata-default-value.interface.ts | 3 +- .../field-metadata-options.interface.ts | 3 +- .../field-metadata-settings.interface.ts | 2 +- .../interfaces/field-metadata.interface.ts | 3 +- .../field-metadata-validation.service.spec.ts | 3 +- .../utils/__tests__/generate-nullable.spec.ts | 3 +- ...lidate-default-value-based-on-type.spec.ts | 3 +- .../utils/compute-column-name.util.ts | 3 +- .../utils/generate-default-value.ts | 4 +-- .../field-metadata/utils/generate-nullable.ts | 2 +- .../is-composite-field-metadata-type.util.ts | 2 +- .../utils/is-enum-field-metadata-type.util.ts | 2 +- .../is-select-field-metadata-type.util.ts | 2 +- .../validate-default-value-for-type.util.ts | 2 +- .../utils/validate-options-for-type.util.ts | 2 +- ...-field-metadata-default-value.validator.ts | 6 ++-- .../is-field-metadata-options.validator.ts | 6 ++-- .../dtos/create-object.input.ts | 2 +- .../object-metadata-migration.service.ts | 6 ++-- .../object-metadata-relation.service.ts | 7 ++-- ...d-default-fields-for-custom-object.util.ts | 7 ++-- .../relation-metadata.service.ts | 6 ++-- .../remote-table-relations.service.ts | 6 ++-- .../utils/udt-name-mapper.util.ts | 4 +-- .../metadata-modules/search/search.service.ts | 6 ++-- .../fieldMetadataTypesToTextColumnType.ts | 2 +- .../factories/basic-column-action.factory.ts | 3 +- .../column-action-abstract.factory.ts | 9 ++--- .../composite-column-action.factory.ts | 3 +- .../factories/enum-column-action.factory.ts | 3 +- .../ts-vector-column-action.factory.ts | 3 +- ...rkspace-column-action-factory.interface.ts | 7 ++-- ...field-metadata-type-to-column-type.util.ts | 3 +- .../utils/is-text-column-type.util.ts | 2 +- .../workspace-migration.factory.ts | 3 +- .../metadata-seeds/pets-metadata-seeds.ts | 2 +- .../survey-results-metadata-seeds.ts | 2 +- .../src/engine/seeder/seeder.service.ts | 3 +- .../twenty-orm/base.workspace-entity.ts | 3 +- .../twenty-orm/custom.workspace-entity.ts | 3 +- .../decorators/workspace-field.decorator.ts | 3 +- .../factories/entity-schema-column.factory.ts | 2 +- ...workspace-field-metadata-args.interface.ts | 3 +- .../twenty-orm/utils/format-data.util.ts | 3 +- .../twenty-orm/utils/format-result.util.ts | 3 +- ...-subfields-for-aggregate-operation.util.ts | 3 +- .../deduce-relation-direction.spec.ts | 3 +- .../src/engine/utils/generate-fake-value.ts | 2 +- .../is-relation-field-metadata-type.util.ts | 2 +- .../services/database-structure.service.ts | 6 ++-- .../services/field-metadata-health.service.ts | 7 ++-- .../workspace-migration-field.factory.ts | 6 ++-- .../workspace-migration-object.factory.ts | 3 +- .../comparators/workspace-field.comparator.ts | 6 ++-- .../factories/standard-field.factory.ts | 3 +- .../partial-field-metadata.interface.ts | 3 +- .../workspace-metadata-updater.service.ts | 7 ++-- ...ync-object-metadata-identifiers.service.ts | 6 ++-- ...ts-vectors-column-expression.utils.spec.ts | 3 +- .../utils/compute-standard-fields.util.ts | 3 +- .../get-ts-vector-column-expression.util.ts | 3 +- .../utils/is-searchable-field.util.ts | 2 +- .../utils/sync-metadata.util.spec.ts | 3 +- .../api-key.workspace-entity.ts | 3 +- .../attachment.workspace-entity.ts | 3 +- .../blocklist.workspace-entity.ts | 3 +- ...nnel-event-association.workspace-entity.ts | 3 +- .../calendar-channel.workspace-entity.ts | 3 +- ...ndar-event-participant.workspace-entity.ts | 3 +- .../calendar-event.workspace-entity.ts | 3 +- .../company.workspace-entity.ts | 3 +- .../connected-account.workspace-entity.ts | 3 +- .../favorite-folder.workspace-entity.ts | 3 +- .../services/favorite-deletion.service.ts | 6 ++-- .../favorite.workspace-entity.ts | 3 +- ...el-message-association.workspace-entity.ts | 3 +- .../message-channel.workspace-entity.ts | 3 +- .../message-participant.workspace-entity.ts | 3 +- .../message.workspace-entity.ts | 3 +- .../standard-objects/note.workspace-entity.ts | 3 +- .../opportunity.workspace-entity.ts | 3 +- .../person.workspace-entity.ts | 3 +- .../standard-objects/task.workspace-entity.ts | 3 +- .../audit-log.workspace-entity.ts | 3 +- .../behavioral-event.workspace-entity.ts | 3 +- .../timeline-activity.workspace-entity.ts | 3 +- .../view-field.workspace-entity.ts | 2 +- .../view-filter-group.workspace-entity.ts | 2 +- .../view-filter.workspace-entity.ts | 3 +- .../view-group.workspace-entity.ts | 3 +- .../view-sort.workspace-entity.ts | 3 +- .../standard-objects/view.workspace-entity.ts | 3 +- .../webhook.workspace-entity.ts | 3 +- ...orkflow-event-listener.workspace-entity.ts | 3 +- .../workflow-run.workspace-entity.ts | 3 +- .../workflow-version.workspace-entity.ts | 3 +- .../workflow.workspace-entity.ts | 5 +-- .../types/input-schema.type.ts | 2 +- .../utils/should-generate-field-fake-value.ts | 7 ++-- .../workspace-member.workspace-entity.ts | 3 +- ...ate-one-field-metadata.integration-spec.ts | 3 +- .../utils/create-test-field-metadata.util.ts | 3 +- .../rename-custom-object.integration-spec.ts | 6 ++-- packages/twenty-shared/src/index.ts | 1 + packages/twenty-zapier/package.json | 8 ++--- packages/twenty-zapier/project.json | 32 ++++++++++++++++++ .../twenty-zapier/src/creates/crud_record.ts | 2 +- .../src/test/utils/capitalize.test.ts | 8 ----- .../src/test/utils/computeInputFields.test.ts | 25 ++------------ .../twenty-zapier/src/utils/capitalize.ts | 3 -- .../src/utils/computeInputFields.ts | 33 ++----------------- .../twenty-zapier/src/utils/data.types.ts | 33 ++----------------- packages/twenty-zapier/tsconfig.json | 2 +- 163 files changed, 340 insertions(+), 387 deletions(-) create mode 100644 packages/twenty-zapier/project.json delete mode 100644 packages/twenty-zapier/src/test/utils/capitalize.test.ts delete mode 100644 packages/twenty-zapier/src/utils/capitalize.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 632eac421525..dc431c2ebd9b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -29,6 +29,10 @@ module.exports = { sourceTag: 'scope:frontend', onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'], }, + { + sourceTag: 'scope:zapier', + onlyDependOnLibsWithTags: ['scope:shared'], + }, ], }, ], diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression.ts index 9ed6bb0f6ad0..7383c5b7e741 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-simplify-search-vector-expression.ts @@ -2,6 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import chalk from 'chalk'; import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared'; import { Repository } from 'typeorm'; import { @@ -9,10 +10,7 @@ import { ActiveWorkspacesCommandRunner, } from 'src/database/commands/active-workspaces.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { SearchService } from 'src/engine/metadata-modules/search/search.service'; import { SEARCH_FIELDS_FOR_CUSTOM_OBJECT } from 'src/engine/twenty-orm/custom.workspace-entity'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression.ts index 0b95e6a67ca5..1bcd25fe75de 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-33/0-33-update-rich-text-search-vector-expression.ts @@ -2,6 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import chalk from 'chalk'; import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared'; import { Repository } from 'typeorm'; import { @@ -9,10 +10,7 @@ import { ActiveWorkspacesCommandRunner, } from 'src/database/commands/active-workspaces.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { SearchService } from 'src/engine/metadata-modules/search/search.service'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-create-column.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-create-column.command.ts index 21fe77a6de86..433edddfb682 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-create-column.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-create-column.command.ts @@ -2,6 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import chalk from 'chalk'; import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; @@ -11,10 +12,7 @@ import { } from 'src/database/commands/active-workspaces.command'; import { isCommandLogger } from 'src/database/commands/logger'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-migrate-data.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-migrate-data.command.ts index 67bf658918d0..b257565656d2 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-migrate-data.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-35/0-35-phone-calling-code-migrate-data.command.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import chalk from 'chalk'; import { getCountries, getCountryCallingCode } from 'libphonenumber-js'; import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; @@ -12,10 +13,7 @@ import { } from 'src/database/commands/active-workspaces.command'; import { isCommandLogger } from 'src/database/commands/logger'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; diff --git a/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts b/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts index fd0bfe2c1279..7e930faa2c73 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts @@ -1,5 +1,6 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; export const getDevSeedCompanyCustomFields = ( objectMetadataId: string, diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index 093b4a0efbee..3b3951645968 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -1,5 +1,6 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; export const FIELD_LINKS_MOCK_NAME = 'fieldLinks'; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts index 777919d2f51c..3915f4efc1e6 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { ObjectRecord, ObjectRecordOrderBy, @@ -14,7 +16,6 @@ import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/ import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/compute-cursor-arg-filter.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/compute-cursor-arg-filter.spec.ts index 8aaaeb003b50..cdf056007256 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/compute-cursor-arg-filter.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/compute-cursor-arg-filter.spec.ts @@ -1,8 +1,9 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; describe('computeCursorArgFilter', () => { const mockFieldMetadataMap = { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts index 030c515251f4..222c10e67934 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { ObjectRecordFilter, ObjectRecordOrderBy, @@ -9,7 +11,6 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts index 04e7b650d454..fe6e8a44b329 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/__tests__/query-runner-args.factory.spec.ts @@ -1,11 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { FieldMetadataType } from 'twenty-shared'; + import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; describe('QueryRunnerArgsFactory', () => { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts index 6fae9cb18e37..25cab922556f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { ObjectRecord, ObjectRecordFilter, @@ -16,7 +18,6 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { RecordPositionFactory } from './record-position.factory'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/__tests__/record-position-backfill-service.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/__tests__/record-position-backfill-service.spec.ts index 6a05c92837a8..f40616f7963b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/__tests__/record-position-backfill-service.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/__tests__/record-position-backfill-service.spec.ts @@ -1,10 +1,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { FieldMetadataType } from 'twenty-shared'; + import { RecordPositionQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory'; import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory'; import { RecordPositionBackfillService } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service.ts index cf5827568c21..fbb183e456b1 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-service.ts @@ -3,13 +3,13 @@ import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'class-validator'; import { Repository } from 'typeorm'; +import { FieldMetadataType } from 'twenty-shared'; import { RecordPositionQueryFactory, RecordPositionQueryType, } from 'src/engine/api/graphql/workspace-query-builder/factories/record-position-query.factory'; import { RecordPositionFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/record-position.factory'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory.ts index a400f29050f5..0de3bad9b887 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLEnumType } from 'graphql'; +import { FieldMetadataType } from 'twenty-shared'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { @@ -8,15 +9,14 @@ import { CompositeType, } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; -import { pascalCase } from 'src/utils/pascal-case'; +import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; +import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util'; import { FieldMetadataComplexOption, FieldMetadataDefaultOption, } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util'; +import { pascalCase } from 'src/utils/pascal-case'; export interface EnumTypeDefinition { target: string; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory.ts index 39ed36c2a6e8..249828b9ef87 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory.ts @@ -1,18 +1,18 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; +import { FieldMetadataType } from 'twenty-shared'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; -import { pascalCase } from 'src/utils/pascal-case'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { InputTypeDefinition, InputTypeDefinitionKind, } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { pascalCase } from 'src/utils/pascal-case'; import { InputTypeFactory } from './input-type.factory'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory.ts index 1a3934505387..702ff89f379b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory.ts @@ -1,19 +1,19 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; +import { FieldMetadataType } from 'twenty-shared'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; -import { pascalCase } from 'src/utils/pascal-case'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { OutputTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory'; import { ObjectTypeDefinition, ObjectTypeDefinitionKind, } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { OutputTypeFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory'; import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { pascalCase } from 'src/utils/pascal-case'; @Injectable() export class CompositeObjectTypeDefinitionFactory { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts index 97eb3c6b1932..e9c94e97edcd 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts @@ -6,6 +6,7 @@ import { GraphQLInputType, GraphQLList, } from 'graphql'; +import { FieldMetadataType } from 'twenty-shared'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; @@ -15,7 +16,6 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; import { InputTypeDefinitionKind } from './input-type-definition.factory'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory.ts index ea3bdda4c21c..e8db91075d2b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/output-type.factory.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLOutputType } from 'graphql'; +import { FieldMetadataType } from 'twenty-shared'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; @@ -9,7 +10,6 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectTypeDefinitionKind } from './object-type-definition.factory'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts index aa3259aea9b8..4970dc3f543b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts @@ -13,6 +13,7 @@ import { GraphQLString, GraphQLType, } from 'graphql'; +import { FieldMetadataType } from 'twenty-shared'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; @@ -37,7 +38,6 @@ import { PositionScalarType } from 'src/engine/api/graphql/workspace-schema-buil import { RawJSONScalar } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar'; import { getNumberFilterType } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-number-filter-type.util'; import { getNumberScalarType } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-number-scalar-type.util'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; export interface TypeOptions { nullable?: boolean; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage.ts index 585e9ad99df5..0c98e17c0762 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage.ts @@ -5,8 +5,8 @@ import { GraphQLInputObjectType, GraphQLObjectType, } from 'graphql'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { EnumTypeDefinition } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; import { InputTypeDefinition, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-field-metadata-type.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-field-metadata-type.spec.ts index 022eab2af3dd..21ad78287e1b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-field-metadata-type.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/__tests__/get-field-metadata-type.spec.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { getFieldMetadataType } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-field-metadata-type.util'; describe('getFieldMetadataType', () => { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util.ts index 607790ef0f95..1ed8adcdcc69 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util.ts @@ -1,6 +1,6 @@ -import { CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; export const computeCompositePropertyTarget = ( type: FieldMetadataType, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts index 4797b9dbbaef..0b6460c55866 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts @@ -4,13 +4,13 @@ import { GraphQLInputType, GraphQLOutputType, } from 'graphql'; +import { FieldMetadataType } from 'twenty-shared'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts index 0445e4ab55b6..5f80f9266fb7 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts @@ -1,12 +1,15 @@ import { GraphQLISODateTime } from '@nestjs/graphql'; import { GraphQLFloat, GraphQLInt, GraphQLScalarType } from 'graphql'; -import { capitalize, isFieldMetadataDateKind } from 'twenty-shared'; +import { + capitalize, + FieldMetadataType, + isFieldMetadataDateKind, +} from 'twenty-shared'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { getSubfieldsForAggregateOperation } from 'src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util'; export type AggregationField = { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-field-metadata-type.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-field-metadata-type.util.ts index 8c93756899dd..515fbe97c4ab 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-field-metadata-type.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-field-metadata-type.util.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; const typeOrmTypeMapping = new Map([ ['uuid', FieldMetadataType.UUID], diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts index ec6854bc482f..8a23abca6c77 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts @@ -1,5 +1,6 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils'; describe('getFieldType', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts index 4fec6c7ab443..55743d1a397f 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { fieldCurrencyMock, fieldNumberMock, @@ -5,7 +7,6 @@ import { objectMetadataItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; describe('mapFieldMetadataToGraphqlQuery', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts index 8c24bce85846..ee0a8c6e047f 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts @@ -1,9 +1,10 @@ -import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { fieldSelectMock, objectMetadataItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; +import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values'; describe('checkFilterEnumValues', () => { it('should check properly', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/format-field-values.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/format-field-values.utils.spec.ts index 6787ccc3eec8..f8d0344313e1 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/format-field-values.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/format-field-values.utils.spec.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { formatFieldValue } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils'; describe('formatFieldValue', () => { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts index 3a57186c15cb..9565a087fcdf 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values.ts @@ -1,8 +1,8 @@ import { BadRequestException } from '@nestjs/common'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; export const checkFilterEnumValues = ( fieldType: FieldMetadataType | undefined, diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts index e884ebf4b6ac..fb9faf67179b 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/format-field-values.utils.ts @@ -1,6 +1,7 @@ import { BadRequestException } from '@nestjs/common'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldValue } from 'src/engine/api/rest/core/types/field-value.type'; export const formatFieldValue = ( diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts index 9cae4ad8e3a3..b118344e2be2 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/get-field-type.utils.ts @@ -1,6 +1,6 @@ -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; export const getFieldType = ( objectMetadata: ObjectMetadataInterface, diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts index fb229a1f8eb5..96bacd1629a6 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; const DEFAULT_DEPTH_VALUE = 1; diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts index c8439f7c0b28..476dd8160be9 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts @@ -1,10 +1,9 @@ import deepEqual from 'deep-equal'; +import { FieldMetadataType } from 'twenty-shared'; import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; - export const objectRecordChangedValues = ( oldRecord: Partial, newRecord: Partial, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 99e369965d17..e01c55257ad2 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -1,5 +1,5 @@ import { OpenAPIV3_1 } from 'openapi-types'; -import { capitalize } from 'twenty-shared'; +import { capitalize, FieldMetadataType } from 'twenty-shared'; import { computeDepthParameters, @@ -10,10 +10,7 @@ import { computeOrderByParameters, computeStartingAfterParameters, } from 'src/engine/core-modules/open-api/utils/parameters.utils'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts index 80f4689aef74..f517c8b74ca5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts @@ -1,10 +1,10 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { CompositeProperty, CompositeType, } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; - export enum FieldActorSource { EMAIL = 'EMAIL', CALENDAR = 'CALENDAR', diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/address.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/address.composite-type.ts index a8871b037bb8..3101877ef8df 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/address.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/address.composite-type.ts @@ -1,6 +1,6 @@ -import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; export const addressCompositeType: CompositeType = { type: FieldMetadataType.ADDRESS, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type.ts index c7ce2147b60c..a948529d5d2e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type.ts @@ -1,6 +1,6 @@ -import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; export const currencyCompositeType: CompositeType = { type: FieldMetadataType.CURRENCY, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts index 5cda75160530..919cf98d1081 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts @@ -1,6 +1,6 @@ -import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; export const emailsCompositeType: CompositeType = { type: FieldMetadataType.EMAILS, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts index 937a9f6aae3a..2aca0513a9fb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type.ts @@ -1,6 +1,6 @@ -import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; export const fullNameCompositeType: CompositeType = { type: FieldMetadataType.FULL_NAME, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts index fbc999d49e70..f7f943b825ca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; import { actorCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; @@ -7,7 +9,6 @@ import { emailsCompositeType } from 'src/engine/metadata-modules/field-metadata/ import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; import { linksCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; import { phonesCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; export const compositeTypeDefinitions = new Map< FieldMetadataType, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/links.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/links.composite-type.ts index b2d5cfd6ba4c..aebd470b1d55 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/links.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/links.composite-type.ts @@ -1,6 +1,6 @@ -import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; export const linksCompositeType: CompositeType = { type: FieldMetadataType.LINKS, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type.ts index 6f635f63bf21..6802a2854340 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type.ts @@ -1,6 +1,6 @@ -import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; export const phonesCompositeType: CompositeType = { type: FieldMetadataType.PHONES, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts index fb51feaf042f..612646bdc15a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts @@ -24,6 +24,7 @@ import { Validate, } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; +import { FieldMetadataType } from 'twenty-shared'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; @@ -32,7 +33,6 @@ import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadat import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator'; import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator'; import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator'; import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts index e1c29d5fc0d3..caf2ef32968a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts @@ -9,10 +9,10 @@ import { Min, validateOrReject, } from 'class-validator'; +import { FieldMetadataType } from 'twenty-shared'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataException, FieldMetadataExceptionCode, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index 4f8e5219abb0..400e6643b551 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -1,3 +1,4 @@ +import { FieldMetadataType } from 'twenty-shared'; import { Column, CreateDateColumn, @@ -21,32 +22,6 @@ import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-meta import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -export enum FieldMetadataType { - UUID = 'UUID', - TEXT = 'TEXT', - PHONES = 'PHONES', - EMAILS = 'EMAILS', - DATE_TIME = 'DATE_TIME', - DATE = 'DATE', - BOOLEAN = 'BOOLEAN', - NUMBER = 'NUMBER', - NUMERIC = 'NUMERIC', - LINKS = 'LINKS', - CURRENCY = 'CURRENCY', - FULL_NAME = 'FULL_NAME', - RATING = 'RATING', - SELECT = 'SELECT', - MULTI_SELECT = 'MULTI_SELECT', - RELATION = 'RELATION', - POSITION = 'POSITION', - ADDRESS = 'ADDRESS', - RAW_JSON = 'RAW_JSON', - RICH_TEXT = 'RICH_TEXT', - ACTOR = 'ACTOR', - ARRAY = 'ARRAY', - TS_VECTOR = 'TS_VECTOR', -} - @Entity('fieldMetadata') @Unique('IndexOnNameObjectMetadataIdAndWorkspaceIdUnique', [ 'name', @@ -72,7 +47,10 @@ export class FieldMetadataEntity< @JoinColumn({ name: 'objectMetadataId' }) object: Relation; - @Column({ nullable: false }) + @Column({ + nullable: false, + type: 'varchar', + }) type: FieldMetadataType; @Column({ nullable: false }) diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts index 2d9e4796132c..e2fd5576d7d7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts @@ -12,6 +12,8 @@ import { Resolver, } from '@nestjs/graphql'; +import { FieldMetadataType } from 'twenty-shared'; + import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; @@ -21,7 +23,6 @@ import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/ import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { RelationDefinitionDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation-definition.dto'; import { UpdateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util'; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index 2c539e4b2f04..c114b9113ccc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -3,6 +3,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import isEmpty from 'lodash.isempty'; +import { FieldMetadataType } from 'twenty-shared'; import { DataSource, FindOneOptions, Repository } from 'typeorm'; import { v4 as uuidV4, v4 } from 'uuid'; @@ -60,10 +61,7 @@ import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view import { isDefined } from 'src/utils/is-defined'; import { FieldMetadataValidationService } from './field-metadata-validation.service'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from './field-metadata.entity'; +import { FieldMetadataEntity } from './field-metadata.entity'; import { generateDefaultValue } from './utils/generate-default-value'; import { generateRatingOptions } from './utils/generate-rating-optionts.util'; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface.ts index 321cd3c7f917..d85b3751a54b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface.ts @@ -1,6 +1,6 @@ -import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; export interface CompositeProperty< Type extends FieldMetadataType = FieldMetadataType, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts index 8acf362373e7..5196dac246ef 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataDefaultActor, FieldMetadataDefaultArray, @@ -16,7 +18,6 @@ import { FieldMetadataDefaultValueString, FieldMetadataDefaultValueUuidFunction, } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; type ExtractValueType = T extends { value: infer V } ? V : T; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface.ts index c6139048b511..b801612f6b93 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface.ts @@ -1,8 +1,9 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataComplexOption, FieldMetadataDefaultOption, } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; type FieldMetadataOptionsMapping = { [FieldMetadataType.RATING]: FieldMetadataDefaultOption[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts index 5471a21b3c1c..150c451afede 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export enum NumberDataType { FLOAT = 'float', diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts index a33fe7392cdd..f5ba634d3e7e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts @@ -1,8 +1,9 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; export interface FieldMetadataInterface< diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/field-metadata-validation.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/field-metadata-validation.service.spec.ts index 1e1b0bf63f07..6c37e8ee7c14 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/field-metadata-validation.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/field-metadata-validation.service.spec.ts @@ -1,7 +1,8 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/field-metadata-validation.service'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataException } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; describe('FieldMetadataValidationService', () => { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/generate-nullable.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/generate-nullable.spec.ts index 7d98519bd66d..7e383d0ae206 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/generate-nullable.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/generate-nullable.spec.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { generateNullable } from 'src/engine/metadata-modules/field-metadata/utils/generate-nullable'; describe('generateNullable', () => { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts index 60e4b04261c3..79dc8a2fba03 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util'; describe('validateDefaultValueForType', () => { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts index bc24729a7277..53958fa1ca63 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts @@ -1,7 +1,8 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataException, FieldMetadataExceptionCode, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts index 17b55a4b6622..bbb307ebf2cb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts @@ -1,6 +1,6 @@ -import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; export function generateDefaultValue( type: FieldMetadataType, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-nullable.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-nullable.ts index 759012070ffc..6d1e400beeea 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-nullable.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-nullable.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export function generateNullable( type: FieldMetadataType, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts index 911162c9ae39..0fb1c6968ebf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export const isCompositeFieldMetadataType = ( type: FieldMetadataType, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util.ts index 2e179b45f277..45c0b707c044 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export type EnumFieldMetadataUnionType = | FieldMetadataType.RATING diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util.ts index 56a918911e2f..2c892f1f347e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export const isSelectFieldMetadataType = ( type: FieldMetadataType, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts index a5cea6d00482..efa979312b3a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts @@ -1,5 +1,6 @@ import { plainToInstance } from 'class-transformer'; import { ValidationError, validateSync } from 'class-validator'; +import { FieldMetadataType } from 'twenty-shared'; import { FieldMetadataClassValidation, @@ -24,7 +25,6 @@ import { FieldMetadataDefaultValueStringArray, FieldMetadataDefaultValueUuidFunction, } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; export const defaultValueValidatorsMap = { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util.ts index aecb4eefe9fd..d9e09b7db11b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util.ts @@ -1,9 +1,9 @@ import { plainToInstance } from 'class-transformer'; import { validateSync } from 'class-validator'; +import { FieldMetadataType } from 'twenty-shared'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataComplexOption, FieldMetadataDefaultOption, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts index cdb17c52e827..254af376042a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator.ts @@ -6,15 +6,13 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; +import { FieldMetadataType } from 'twenty-shared'; import { Repository } from 'typeorm'; import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { LoggerService } from 'src/engine/core-modules/logger/logger.service'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util'; @Injectable() diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator.ts index bbfad46aa818..866b96870e1e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator.ts @@ -2,14 +2,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ValidationArguments, ValidatorConstraint } from 'class-validator'; +import { FieldMetadataType } from 'twenty-shared'; import { Repository } from 'typeorm'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { validateOptionsForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util'; @Injectable() diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts index e6b953c7caf8..ef365e647b8d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts @@ -3,11 +3,11 @@ import { Field, HideField, InputType } from '@nestjs/graphql'; import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql'; import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import GraphQLJSON from 'graphql-type-json'; +import { FieldMetadataType } from 'twenty-shared'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook'; @InputType() diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts index ece0209c3c45..5fa9c9323105 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts @@ -1,12 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { FieldMetadataType } from 'twenty-shared'; import { In, Repository } from 'typeorm'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util'; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts index de88295e1f84..fbec4faf764e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts @@ -1,15 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { capitalize } from 'twenty-shared'; +import { capitalize, FieldMetadataType } from 'twenty-shared'; import { In, Repository } from 'typeorm'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util'; import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util'; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts index 19eb5b2f7c05..d6edd1bde1fc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util.ts @@ -1,7 +1,6 @@ -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { BASE_OBJECT_STANDARD_FIELD_IDS, CUSTOM_OBJECT_STANDARD_FIELD_IDS, diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts index e5fb7e56f4b4..f90559614e2d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts @@ -3,15 +3,13 @@ import { InjectRepository } from '@nestjs/typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import camelCase from 'lodash.camelcase'; +import { FieldMetadataType } from 'twenty-shared'; import { FindOneOptions, In, Repository } from 'typeorm'; import { v4 as uuidV4 } from 'uuid'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts index 614cbb891c2d..0ae9bb96fb9c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service.ts @@ -1,14 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { FieldMetadataType } from 'twenty-shared'; import { In, Repository } from 'typeorm'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { createRelationForeignKeyFieldMetadataName } from 'src/engine/metadata-modules/relation-metadata/utils/create-relation-foreign-key-field-metadata-name.util'; import { buildMigrationsToCreateRemoteTableRelations } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/utils/build-migrations-to-create-remote-table-relations.util'; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util.ts index 5678f2ac5a5b..2799da011c26 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util.ts @@ -1,10 +1,10 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataSettings, NumberDataType, } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; - export const mapUdtNameToFieldType = (udtName: string): FieldMetadataType => { switch (udtName) { case 'uuid': diff --git a/packages/twenty-server/src/engine/metadata-modules/search/search.service.ts b/packages/twenty-server/src/engine/metadata-modules/search/search.service.ts index 4bad6f902b4d..9f745bef39f8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/search/search.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/search/search.service.ts @@ -1,15 +1,13 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { FieldMetadataType } from 'twenty-shared'; import { Repository } from 'typeorm'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/constants/fieldMetadataTypesToTextColumnType.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/constants/fieldMetadataTypesToTextColumnType.ts index 889cd3035f67..7a447a9b8b7d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/constants/fieldMetadataTypesToTextColumnType.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/constants/fieldMetadataTypesToTextColumnType.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export const FIELD_METADATA_TYPES_TO_TEXT_COLUMN_TYPE = [ FieldMetadataType.TEXT, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts index 26925227d2b1..998191ec556b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts @@ -1,9 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory.ts index 0e600aa6f618..1572fd9a2b66 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory.ts @@ -1,17 +1,18 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { Logger } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; import { WorkspaceColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface'; +import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; import { - WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnAction, - WorkspaceMigrationColumnCreate, + WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnAlter, + WorkspaceMigrationColumnCreate, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { WorkspaceMigrationException, WorkspaceMigrationExceptionCode, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts index 0051d7293a65..941403074b21 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts @@ -1,9 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory.ts index 182bc0a7c9f4..2d7e011f384d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory.ts @@ -1,9 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory.ts index 71f1265b7baa..8872e23d9e70 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory.ts @@ -1,8 +1,9 @@ import { Injectable, Logger } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface.ts index b1114c181488..f8a2f034f678 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface.ts @@ -1,10 +1,11 @@ -import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { - WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnAction, + WorkspaceMigrationColumnActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; export interface WorkspaceColumnActionFactory< diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts index 48961fe8530d..c9777c2f5c7b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { isTextColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/is-text-column-type.util'; import { WorkspaceMigrationException, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/is-text-column-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/is-text-column-type.util.ts index d0a1d70a298e..6dc0b0ef88a6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/is-text-column-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/is-text-column-type.util.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export const isTextColumnType = (type: FieldMetadataType) => { return ( diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts index 5df5da052f2b..77d7a8351ae8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts @@ -1,10 +1,11 @@ import { Injectable, Logger } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { WorkspaceColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-factory.interface'; import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; diff --git a/packages/twenty-server/src/engine/seeder/metadata-seeds/pets-metadata-seeds.ts b/packages/twenty-server/src/engine/seeder/metadata-seeds/pets-metadata-seeds.ts index e3acc9daed49..13728594dea2 100644 --- a/packages/twenty-server/src/engine/seeder/metadata-seeds/pets-metadata-seeds.ts +++ b/packages/twenty-server/src/engine/seeder/metadata-seeds/pets-metadata-seeds.ts @@ -1,6 +1,6 @@ import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export const PETS_METADATA_SEEDS: ObjectMetadataSeed = { labelPlural: 'Pets', diff --git a/packages/twenty-server/src/engine/seeder/metadata-seeds/survey-results-metadata-seeds.ts b/packages/twenty-server/src/engine/seeder/metadata-seeds/survey-results-metadata-seeds.ts index af6d6305fc3e..14a76da5f7e1 100644 --- a/packages/twenty-server/src/engine/seeder/metadata-seeds/survey-results-metadata-seeds.ts +++ b/packages/twenty-server/src/engine/seeder/metadata-seeds/survey-results-metadata-seeds.ts @@ -5,7 +5,7 @@ import { } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export const SURVEY_RESULTS_METADATA_SEEDS: ObjectMetadataSeed = { labelPlural: 'Survey results', diff --git a/packages/twenty-server/src/engine/seeder/seeder.service.ts b/packages/twenty-server/src/engine/seeder/seeder.service.ts index f796791a34ab..0f06c7d55808 100644 --- a/packages/twenty-server/src/engine/seeder/seeder.service.ts +++ b/packages/twenty-server/src/engine/seeder/seeder.service.ts @@ -1,13 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { capitalize } from 'twenty-shared'; +import { capitalize, FieldMetadataType } from 'twenty-shared'; import { ObjectMetadataSeed } from 'src/engine/seeder/interfaces/object-metadata-seed'; import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; diff --git a/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts index 53355304c346..80e9a673b261 100644 --- a/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts +++ b/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; import { WorkspaceIsPrimaryField } from 'src/engine/twenty-orm/decorators/workspace-is-primary-field.decorator'; diff --git a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts index 168dc1b7a7d8..eb26fbb5cbff 100644 --- a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts +++ b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts @@ -1,9 +1,10 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; import { diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts index 35068e823005..2ea31fa1ee63 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts @@ -1,8 +1,9 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts index 323a02befdb3..4d6839a37aa2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; import { ColumnType, EntitySchemaColumnOptions } from 'typeorm'; +import { FieldMetadataType } from 'twenty-shared'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts index 03862b3adf61..4d65fc557bcb 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts @@ -1,9 +1,10 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; export interface WorkspaceFieldMetadataArgs { diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts index f1f1e61e8022..66e1838d25cb 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts @@ -1,9 +1,8 @@ -import { capitalize } from 'twenty-shared'; +import { capitalize, FieldMetadataType } from 'twenty-shared'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index ceb2f427c793..954712fdfaed 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -1,9 +1,10 @@ import { isPlainObject } from '@nestjs/common/utils/shared.utils'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util.ts index bbe0c48f0567..a33a96afdd05 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; export const getSubfieldsForAggregateOperation = ( diff --git a/packages/twenty-server/src/engine/utils/__tests__/deduce-relation-direction.spec.ts b/packages/twenty-server/src/engine/utils/__tests__/deduce-relation-direction.spec.ts index 71114d678a59..a6a9ad177c6b 100644 --- a/packages/twenty-server/src/engine/utils/__tests__/deduce-relation-direction.spec.ts +++ b/packages/twenty-server/src/engine/utils/__tests__/deduce-relation-direction.spec.ts @@ -1,7 +1,8 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { RelationMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-metadata.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { deduceRelationDirection, diff --git a/packages/twenty-server/src/engine/utils/generate-fake-value.ts b/packages/twenty-server/src/engine/utils/generate-fake-value.ts index e29c0dc9a189..bfc87d313c4e 100644 --- a/packages/twenty-server/src/engine/utils/generate-fake-value.ts +++ b/packages/twenty-server/src/engine/utils/generate-fake-value.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; type FakeValueTypes = | string diff --git a/packages/twenty-server/src/engine/utils/is-relation-field-metadata-type.util.ts b/packages/twenty-server/src/engine/utils/is-relation-field-metadata-type.util.ts index a5ed9d644eba..f12bc852d86a 100644 --- a/packages/twenty-server/src/engine/utils/is-relation-field-metadata-type.util.ts +++ b/packages/twenty-server/src/engine/utils/is-relation-field-metadata-type.util.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export const isRelationFieldMetadataType = ( type: FieldMetadataType, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts index f7d6d1e36f3d..40cfab79a7f1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; import { ColumnType } from 'typeorm'; import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'; @@ -15,10 +16,7 @@ import { import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { FieldMetadataDefaultValueFunctionNames } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util'; import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts index 994b91ec70ba..b309cde585a1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { WorkspaceHealthIssue, @@ -9,10 +11,7 @@ import { WorkspaceHealthOptions } from 'src/engine/workspace-manager/workspace-h import { WorkspaceTableStructure } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeColumnName, computeCompositeColumnName, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts index c4852a5841e5..5f83d8b3e633 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts @@ -1,13 +1,11 @@ import { Injectable } from '@nestjs/common'; import diff from 'microdiff'; +import { FieldMetadataType } from 'twenty-shared'; import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts index 6085de1a0c83..4afc2829e8cc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory.ts @@ -1,8 +1,9 @@ import { Injectable } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts index 42c481c81371..2733fc4e8b48 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import diff from 'microdiff'; +import { FieldMetadataType } from 'twenty-shared'; import { ComparatorAction, @@ -8,10 +9,7 @@ import { } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; import { ComputedPartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; const commonFieldPropertiesToIgnore = [ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts index 1aae07797ea7..29de3fe17f82 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; + import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { WorkspaceDynamicRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface'; import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface'; @@ -11,7 +13,6 @@ import { } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts index 4f17d0ee3dca..fbcc484063a2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts @@ -1,7 +1,8 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; export type PartialFieldMetadata = Omit< diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts index 35636fb042cd..31f0f5e26186 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { capitalize } from 'twenty-shared'; +import { capitalize, FieldMetadataType } from 'twenty-shared'; import { EntityManager, EntityTarget, @@ -17,10 +17,7 @@ import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-syn import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service.ts index d59f35c1f9fc..15052c36a72e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service.ts @@ -1,14 +1,12 @@ import { Injectable } from '@nestjs/common'; +import { FieldMetadataType } from 'twenty-shared'; import { EntityManager, Repository } from 'typeorm'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { StandardObjectFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts index a7a4f73bd3b7..1263cc9959a7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { FieldTypeAndNameMetadata, getTsVectorColumnExpressionFromFields, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts index 7f1a1247bdce..e2aa820c43b4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { ComputedPartialFieldMetadata, PartialComputedFieldMetadata, @@ -5,7 +7,6 @@ import { } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { createForeignKeyDeterministicUuid, createRelationDeterministicUuid, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts index 3b804ecd4dac..d10224b62a69 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts @@ -1,5 +1,6 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeColumnName, computeCompositeColumnName, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts index 78b9c62e75cf..c1dd3a3cd567 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; const SEARCHABLE_FIELD_TYPES = [ FieldMetadataType.TEXT, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util.spec.ts index 3ab01cd8e3da..801ea23620bd 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util.spec.ts @@ -1,5 +1,6 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { mapObjectMetadataByUniqueIdentifier } from './sync-metadata.util'; diff --git a/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts b/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts index 7fd493c1c272..376db42bbd35 100644 --- a/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts +++ b/packages/twenty-server/src/modules/api-key/standard-objects/api-key.workspace-entity.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; diff --git a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts index b47716028f65..50aa00e32214 100644 --- a/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts +++ b/packages/twenty-server/src/modules/attachment/standard-objects/attachment.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; diff --git a/packages/twenty-server/src/modules/blocklist/standard-objects/blocklist.workspace-entity.ts b/packages/twenty-server/src/modules/blocklist/standard-objects/blocklist.workspace-entity.ts index 3a45baa99ed1..da8adfddf3af 100644 --- a/packages/twenty-server/src/modules/blocklist/standard-objects/blocklist.workspace-entity.ts +++ b/packages/twenty-server/src/modules/blocklist/standard-objects/blocklist.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts index 99cad3e5a1b0..099f237653a9 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts index ec4eb9cb7e76..0fe75f820f10 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts @@ -1,8 +1,9 @@ import { registerEnumType } from '@nestjs/graphql'; +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity.ts index 1e2d05b168b1..3c65c9c9de1f 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts index 94967baae433..22f83677273b 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts @@ -1,7 +1,8 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index d36e4f473391..d93ea689dbd1 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; @@ -8,7 +10,6 @@ import { import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type'; import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, diff --git a/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts b/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts index b1c0c97c1612..0f8e2769152c 100644 --- a/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts +++ b/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, diff --git a/packages/twenty-server/src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity.ts b/packages/twenty-server/src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity.ts index c58dab4027ef..e96fa3fd99cd 100644 --- a/packages/twenty-server/src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity.ts +++ b/packages/twenty-server/src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, diff --git a/packages/twenty-server/src/modules/favorite/services/favorite-deletion.service.ts b/packages/twenty-server/src/modules/favorite/services/favorite-deletion.service.ts index 32f62a34b01a..d3f02484c8ae 100644 --- a/packages/twenty-server/src/modules/favorite/services/favorite-deletion.service.ts +++ b/packages/twenty-server/src/modules/favorite/services/favorite-deletion.service.ts @@ -1,12 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { FieldMetadataType } from 'twenty-shared'; import { In, Repository } from 'typeorm'; -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { FAVORITE_DELETION_BATCH_SIZE } from 'src/modules/favorite/constants/favorite-deletion-batch-size'; diff --git a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts index e9fef0ae7ac3..edc7cbb9c957 100644 --- a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts +++ b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts @@ -1,7 +1,8 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts index b14cb8499fe2..25523625b327 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts index 1e0b80b18fec..2f951cefa4f2 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts @@ -1,8 +1,9 @@ import { registerEnumType } from '@nestjs/graphql'; +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts index 40989d0ae76f..90623a12f006 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts index 9780aeb1844f..c7606936731d 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, diff --git a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts index 2b51ae509fef..bff384c2d757 100644 --- a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts +++ b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; @@ -5,7 +7,6 @@ import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, diff --git a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts index 791c1af4a2c7..a3038d2bae2a 100644 --- a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; @@ -6,7 +8,6 @@ import { FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index 68122a1a649d..9ba4c3139f89 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; @@ -9,7 +11,6 @@ import { EmailsMetadata } from 'src/engine/metadata-modules/field-metadata/compo import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; import { PhonesMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, diff --git a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts index ff3b83e6c07b..73557f8067fb 100644 --- a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts +++ b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; @@ -5,7 +7,6 @@ import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts index 8de2edb58317..b826e675ec86 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/audit-log.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.workspace-entity.ts index d5ed28db764e..496c9ab9804a 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/behavioral-event.workspace-entity.ts @@ -1,5 +1,6 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts index 1a3c95e5f41f..de2fea0b8327 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts @@ -1,7 +1,8 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts index 9f34ac24321b..60b8ed12e11c 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-field.workspace-entity.ts @@ -1,9 +1,9 @@ import { registerEnumType } from '@nestjs/graphql'; import { Relation } from 'typeorm'; +import { FieldMetadataType } from 'twenty-shared'; import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-filter-group.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-filter-group.workspace-entity.ts index b501478a318b..af9d9bab0069 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-filter-group.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-filter-group.workspace-entity.ts @@ -1,6 +1,6 @@ import { Relation } from 'typeorm'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts index 1d2f33546e27..266b07280364 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts index 6f7780da4cb9..1f0d25ec1c01 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-group.workspace-entity.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts index 09eb870e9453..32bb1c857985 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-sort.workspace-entity.ts @@ -1,6 +1,7 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts index 06fd46286343..cfab0c8657f4 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts @@ -1,7 +1,8 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, diff --git a/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts b/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts index ee55e07da21d..47183c3a0cff 100644 --- a/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts +++ b/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts @@ -1,4 +1,5 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts index 32aa8aeb5a6a..d54f1219dc8d 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts @@ -1,7 +1,8 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts index f14716ee9509..d3eaaef13555 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @@ -5,7 +7,6 @@ import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts index 46cd739a7b59..de6d47d43e1c 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts @@ -1,8 +1,9 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts index 2f8b20361904..3c4fd1359fad 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @@ -5,7 +7,7 @@ import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { RelationMetadataType, RelationOnDeleteAction, @@ -25,7 +27,6 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity'; import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; -import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; export enum WorkflowStatus { DRAFT = 'DRAFT', diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/types/input-schema.type.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/types/input-schema.type.ts index cb826df524ab..26d54a485ff8 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/types/input-schema.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/types/input-schema.type.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; export type InputSchemaPropertyType = | 'string' diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/should-generate-field-fake-value.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/should-generate-field-fake-value.ts index 37ce8c022cdb..fe36a9f363cf 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/should-generate-field-fake-value.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/should-generate-field-fake-value.ts @@ -1,7 +1,6 @@ -import { - FieldMetadataEntity, - FieldMetadataType, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; export const shouldGenerateFieldFakeValue = (field: FieldMetadataEntity) => { return ( diff --git a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts index 5e6e87069b3d..4e39f9ff8a73 100644 --- a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts @@ -1,10 +1,11 @@ import { registerEnumType } from '@nestjs/graphql'; +import { FieldMetadataType } from 'twenty-shared'; + import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/create-one-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/create-one-field-metadata.integration-spec.ts index ee0fbe2e7fed..e4a29aef152a 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/create-one-field-metadata.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/create-one-field-metadata.integration-spec.ts @@ -2,8 +2,7 @@ import { createOneFieldMetadataFactory } from 'test/integration/metadata/suites/ import { createListingCustomObject } from 'test/integration/metadata/suites/object-metadata/utils/create-test-object-metadata.util'; import { deleteOneObjectMetadataItem } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; - -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; describe('createOne', () => { describe('FieldMetadataService name/label sync', () => { diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-test-field-metadata.util.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-test-field-metadata.util.ts index 7d5af01334cf..a1d932ee0a96 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-test-field-metadata.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-test-field-metadata.util.ts @@ -1,7 +1,6 @@ import { createOneFieldMetadataFactory } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-factory.util'; import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; - -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataType } from 'twenty-shared'; const FIELD_NAME = 'testName'; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts index ac4d836b4981..8b96d3baa45d 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts @@ -1,14 +1,14 @@ import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { createOneObjectMetadataFactory } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-factory.util'; import { deleteOneObjectMetadataItemFactory } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-factory.util'; +import { objectsMetadataFactory } from 'test/integration/metadata/suites/object-metadata/utils/objects-metadata-factory.util'; +import { updateOneObjectMetadataItemFactory } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata-factory.util'; import { createOneRelationMetadataFactory } from 'test/integration/metadata/suites/utils/create-one-relation-metadata-factory.util'; import { deleteOneRelationMetadataItemFactory } from 'test/integration/metadata/suites/utils/delete-one-relation-metadata-factory.util'; import { fieldsMetadataFactory } from 'test/integration/metadata/suites/utils/fields-metadata-factory.util'; import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; -import { updateOneObjectMetadataItemFactory } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata-factory.util'; -import { objectsMetadataFactory } from 'test/integration/metadata/suites/object-metadata/utils/objects-metadata-factory.util'; +import { FieldMetadataType } from 'twenty-shared'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; const LISTING_NAME_SINGULAR = 'listing'; diff --git a/packages/twenty-shared/src/index.ts b/packages/twenty-shared/src/index.ts index 91931ceceb4d..6bd4638f2a23 100644 --- a/packages/twenty-shared/src/index.ts +++ b/packages/twenty-shared/src/index.ts @@ -1,5 +1,6 @@ export * from './constants/TwentyCompaniesBaseUrl'; export * from './constants/TwentyIconsBaseUrl'; +export * from './types/FieldMetadataType'; export * from './utils/fieldMetadata/isFieldMetadataDateKind'; export * from './utils/image/getImageAbsoluteURI'; export * from './utils/strings'; diff --git a/packages/twenty-zapier/package.json b/packages/twenty-zapier/package.json index 4c57386e990a..aaa1e8898770 100644 --- a/packages/twenty-zapier/package.json +++ b/packages/twenty-zapier/package.json @@ -1,19 +1,15 @@ { "name": "twenty-zapier", - "version": "2.0.0", + "version": "2.0.1", "description": "Effortlessly sync Twenty with 3000+ apps. Automate tasks, boost productivity, and supercharge your customer relationships!", "main": "src/index.ts", "scripts": { "nx": "NX_DEFAULT_PROJECT=twenty-zapier node ../../node_modules/nx/bin/nx.js", "format": "prettier . --write \"!build\"", "test": "yarn build && jest --testTimeout 10000 --rootDir ./lib/test", - "build": "yarn clean && tsc", - "deploy": "yarn build && zapier push", "validate": "yarn build && zapier validate", "versions": "yarn build && zapier versions", - "clean": "rimraf ./lib ./build", - "watch": "yarn clean && tsc --watch", - "_zapier-build": "yarn build" + "watch": "yarn clean && npx tsc --watch" }, "engines": { "node": "^18.17.1", diff --git a/packages/twenty-zapier/project.json b/packages/twenty-zapier/project.json new file mode 100644 index 000000000000..b8e858dc8267 --- /dev/null +++ b/packages/twenty-zapier/project.json @@ -0,0 +1,32 @@ +{ + "name": "twenty-zapier", + "projectType": "application", + "tags": ["scope:zapier"], + "targets": { + "build": { + "outputs": ["{projectRoot}/lib"], + "executor": "nx:run-commands", + "options": { + "cwd": "{projectRoot}", + "commands": ["nx run twenty-zapier:clean && tsc"] + }, + "dependsOn": ["^build"] + }, + "clean": { + "executor": "nx:run-commands", + "options": { + "cwd": "{projectRoot}", + "commands": ["rimraf ./lib ./build"] + } + }, + "deploy": { + "executor": "nx:run-commands", + "options": { + "cwd": "{projectRoot}", + "commands": [ + "nx run twenty-zapier:build && cp -r ../twenty-shared/ node_modules/twenty-shared && zapier push --skip-npm-install" + ] + } + } + } +} diff --git a/packages/twenty-zapier/src/creates/crud_record.ts b/packages/twenty-zapier/src/creates/crud_record.ts index eaa4c2a3dd8a..0d6eb4740c8f 100644 --- a/packages/twenty-zapier/src/creates/crud_record.ts +++ b/packages/twenty-zapier/src/creates/crud_record.ts @@ -1,8 +1,8 @@ import { Bundle, ZObject } from 'zapier-platform-core'; +import { capitalize } from 'twenty-shared'; import { findObjectNamesSingularKey } from '../triggers/find_object_names_singular'; import { listRecordIdsKey } from '../triggers/list_record_ids'; -import { capitalize } from '../utils/capitalize'; import { computeInputFields } from '../utils/computeInputFields'; import { InputData } from '../utils/data.types'; import handleQueryParams from '../utils/handleQueryParams'; diff --git a/packages/twenty-zapier/src/test/utils/capitalize.test.ts b/packages/twenty-zapier/src/test/utils/capitalize.test.ts deleted file mode 100644 index 96e7bdf34a36..000000000000 --- a/packages/twenty-zapier/src/test/utils/capitalize.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { capitalize } from '../../utils/capitalize'; - -describe('capitalize', () => { - test('should capitalize properly', () => { - expect(capitalize('word')).toEqual('Word'); - expect(capitalize('word word')).toEqual('Word word'); - }); -}); diff --git a/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts b/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts index fe76d7bed70d..44ac0afc3166 100644 --- a/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts +++ b/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts @@ -1,5 +1,6 @@ +import { FieldMetadataType } from 'twenty-shared'; import { computeInputFields } from '../../utils/computeInputFields'; -import { FieldMetadataType, InputField } from '../../utils/data.types'; +import { InputField } from '../../utils/data.types'; describe('computeInputFields', () => { test('should create Person input fields properly', () => { @@ -89,16 +90,6 @@ describe('computeInputFields', () => { defaultValue: null, }, }, - { - node: { - type: FieldMetadataType.LINK, - name: 'xLink', - label: 'X', - description: 'Contact’s X/Twitter account', - isNullable: true, - defaultValue: null, - }, - }, { node: { type: FieldMetadataType.LINKS, @@ -109,18 +100,6 @@ describe('computeInputFields', () => { defaultValue: null, }, }, - { - node: { - type: FieldMetadataType.EMAIL, - name: 'email', - label: 'Email', - description: 'Contact’s Email', - isNullable: false, - defaultValue: { - value: '', - }, - }, - }, { node: { type: FieldMetadataType.UUID, diff --git a/packages/twenty-zapier/src/utils/capitalize.ts b/packages/twenty-zapier/src/utils/capitalize.ts deleted file mode 100644 index 046620ddf38c..000000000000 --- a/packages/twenty-zapier/src/utils/capitalize.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const capitalize = (word: string): string => { - return word.charAt(0).toUpperCase() + word.slice(1); -}; diff --git a/packages/twenty-zapier/src/utils/computeInputFields.ts b/packages/twenty-zapier/src/utils/computeInputFields.ts index 0802361469f1..570c0b9323c0 100644 --- a/packages/twenty-zapier/src/utils/computeInputFields.ts +++ b/packages/twenty-zapier/src/utils/computeInputFields.ts @@ -1,9 +1,5 @@ -import { - FieldMetadataType, - InputField, - Node, - NodeField, -} from '../utils/data.types'; +import { FieldMetadataType } from 'twenty-shared'; +import { InputField, Node, NodeField } from '../utils/data.types'; const getListFromFieldMetadataType = (fieldMetadataType: FieldMetadataType) => { return fieldMetadataType === FieldMetadataType.ARRAY; @@ -16,9 +12,6 @@ const getTypeFromFieldMetadataType = ( case FieldMetadataType.UUID: case FieldMetadataType.TEXT: case FieldMetadataType.RICH_TEXT: - case FieldMetadataType.PHONE: - case FieldMetadataType.EMAIL: - case FieldMetadataType.LINK: case FieldMetadataType.ARRAY: case FieldMetadataType.RATING: return 'string'; @@ -59,25 +52,6 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { }; return [firstName, lastName]; } - case FieldMetadataType.LINK: { - const url: NodeField = { - type: FieldMetadataType.TEXT, - name: 'url', - label: 'Url', - description: 'Link Url', - isNullable: true, - defaultValue: null, - }; - const label: NodeField = { - type: FieldMetadataType.TEXT, - name: 'label', - label: 'Label', - description: 'Link Label', - isNullable: true, - defaultValue: null, - }; - return [url, label]; - } case FieldMetadataType.CURRENCY: { const amountMicros: NodeField = { type: FieldMetadataType.NUMBER, @@ -244,7 +218,6 @@ export const computeInputFields = ( const nodeField = field.node; switch (nodeField.type) { case FieldMetadataType.FULL_NAME: - case FieldMetadataType.LINK: case FieldMetadataType.CURRENCY: case FieldMetadataType.PHONES: case FieldMetadataType.EMAILS: @@ -266,8 +239,6 @@ export const computeInputFields = ( case FieldMetadataType.UUID: case FieldMetadataType.TEXT: case FieldMetadataType.RICH_TEXT: - case FieldMetadataType.PHONE: - case FieldMetadataType.EMAIL: case FieldMetadataType.DATE_TIME: case FieldMetadataType.DATE: case FieldMetadataType.BOOLEAN: diff --git a/packages/twenty-zapier/src/utils/data.types.ts b/packages/twenty-zapier/src/utils/data.types.ts index 776b8c8cc3e4..13e3b6ecef77 100644 --- a/packages/twenty-zapier/src/utils/data.types.ts +++ b/packages/twenty-zapier/src/utils/data.types.ts @@ -1,3 +1,5 @@ +import { FieldMetadataType } from 'twenty-shared'; + export type InputData = { [x: string]: any }; export type NodeField = { @@ -32,37 +34,6 @@ export type InputField = { placeholder?: string; }; -export enum FieldMetadataType { - UUID = 'UUID', - TEXT = 'TEXT', - PHONE = 'PHONE', - PHONES = 'PHONES', - EMAIL = 'EMAIL', - EMAILS = 'EMAILS', - DATE_TIME = 'DATE_TIME', - DATE = 'DATE', - BOOLEAN = 'BOOLEAN', - NUMBER = 'NUMBER', - NUMERIC = 'NUMERIC', - LINK = 'LINK', - LINKS = 'LINKS', - CURRENCY = 'CURRENCY', - FULL_NAME = 'FULL_NAME', - RATING = 'RATING', - SELECT = 'SELECT', - MULTI_SELECT = 'MULTI_SELECT', - POSITION = 'POSITION', - ADDRESS = 'ADDRESS', - RICH_TEXT = 'RICH_TEXT', - ARRAY = 'ARRAY', - - // Ignored fieldTypes - RELATION = 'RELATION', - RAW_JSON = 'RAW_JSON', - ACTOR = 'ACTOR', - TS_VECTOR = 'TS_VECTOR', -} - export type Schema = { data: { objects: { diff --git a/packages/twenty-zapier/tsconfig.json b/packages/twenty-zapier/tsconfig.json index 3e011dd8a419..72ac9b8071df 100644 --- a/packages/twenty-zapier/tsconfig.json +++ b/packages/twenty-zapier/tsconfig.json @@ -8,7 +8,7 @@ "rootDir": "./src", "strict": true, "esModuleInterop": true, - "skipLibCheck": true + "skipLibCheck": true, }, "exclude": [ "jest.config.ts" From 17e2e38812021a67300b08321e384ec3a938010a Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 9 Jan 2025 18:54:34 +0100 Subject: [PATCH 06/22] Refactored select filter and select filter definition logic (#9519) This PR extracts the logic to manage filter and filter definition setting. Previously it was ambiguous, we had the same "selectFilter" naming used for setting filter definition in filter dropdown and for setting the actual filter value and saving to view filter states. This is another incremental refactor, which will allow to remove useFilterDropdown hook. --- .../ObjectFilterDropdownBooleanSelect.tsx | 6 ++- .../ObjectFilterDropdownDateInput.tsx | 8 ++-- .../ObjectFilterDropdownFilterSelect.tsx | 11 +++-- ...pdownFilterSelectCompositeFieldSubMenu.tsx | 7 ++- ...jectFilterDropdownFilterSelectMenuItem.tsx | 26 +++++----- .../ObjectFilterDropdownNumberInput.tsx | 9 +++- .../ObjectFilterDropdownOperandSelect.tsx | 8 ++-- .../ObjectFilterDropdownOptionSelect.tsx | 6 ++- .../ObjectFilterDropdownRatingInput.tsx | 6 ++- .../ObjectFilterDropdownRecordSelect.tsx | 6 ++- .../ObjectFilterDropdownSourceSelect.tsx | 6 ++- .../ObjectFilterDropdownTextSearchInput.tsx | 6 ++- .../__tests__/useFilterDropdown.test.tsx | 21 +++++--- .../hooks/useApplyRecordFilter.ts | 48 +++++++++++++++++++ .../hooks/useFilterDropdown.ts | 22 --------- ...seSelectFilterDefinitionUsedInDropdown.ts} | 14 ++++-- 16 files changed, 138 insertions(+), 72 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyRecordFilter.ts rename packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/{useSelectFilter.ts => useSelectFilterDefinitionUsedInDropdown.ts} (85%) diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx index 9a550ea9d556..e8fa38cbedfc 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay'; @@ -40,9 +41,10 @@ export const ObjectFilterDropdownBooleanSelect = () => { filterDefinitionUsedInDropdownState, selectedOperandInDropdownState, selectedFilterState, - selectFilter, } = useFilterDropdown(); + const { applyRecordFilter } = useApplyRecordFilter(); + const { closeDropdown } = useDropdown(); const filterDefinitionUsedInDropdown = useRecoilValue( @@ -69,7 +71,7 @@ export const ObjectFilterDropdownBooleanSelect = () => { return; } - selectFilter({ + applyRecordFilter({ id: selectedFilter?.id ?? v4(), definition: filterDefinitionUsedInDropdown, operand: selectedOperandInDropdown, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx index 6a90b16f0ce3..a7a061bdfb56 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx @@ -1,6 +1,7 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue'; @@ -21,9 +22,10 @@ export const ObjectFilterDropdownDateInput = () => { filterDefinitionUsedInDropdownState, selectedOperandInDropdownState, selectedFilterState, - selectFilter, } = useFilterDropdown(); + const { applyRecordFilter } = useApplyRecordFilter(); + const filterDefinitionUsedInDropdown = useRecoilValue( filterDefinitionUsedInDropdownState, ); @@ -51,7 +53,7 @@ export const ObjectFilterDropdownDateInput = () => { if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return; - selectFilter?.({ + applyRecordFilter({ id: selectedFilter?.id ? selectedFilter.id : v4(), fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, value: newDate?.toISOString() ?? '', @@ -83,7 +85,7 @@ export const ObjectFilterDropdownDateInput = () => { ) : ''; - selectFilter?.({ + applyRecordFilter({ id: selectedFilter?.id ? selectedFilter.id : v4(), fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, value, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx index 0e465e57220e..649032fe0b2e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx @@ -7,7 +7,7 @@ import { AdvancedFilterButton } from '@/object-record/object-filter-dropdown/com import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; +import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown'; import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; @@ -121,7 +121,8 @@ export const ObjectFilterDropdownFilterSelect = ({ (item) => item.fieldMetadataId, ); - const { selectFilter } = useSelectFilter(); + const { selectFilterDefinitionUsedInDropdown } = + useSelectFilterDefinitionUsedInDropdown(); const { resetSelectedItem } = useSelectableList(OBJECT_FILTER_DROPDOWN_ID); @@ -135,7 +136,11 @@ export const ObjectFilterDropdownFilterSelect = ({ } resetSelectedItem(); - selectFilter({ filterDefinition: selectedFilterDefinition }); + + selectFilterDefinitionUsedInDropdown({ + filterDefinition: selectedFilterDefinition, + }); + closeAdvancedFilterDropdown(); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx index c364bb90eb6a..9dbd2d110db4 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx @@ -1,4 +1,5 @@ import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState'; @@ -55,11 +56,12 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, setObjectFilterDropdownSearchInput, - selectFilter, advancedFilterViewFilterIdState, advancedFilterViewFilterGroupIdState, } = useFilterDropdown(); + const { applyRecordFilter } = useApplyRecordFilter(); + const advancedFilterViewFilterId = useRecoilValue( advancedFilterViewFilterIdState, ); @@ -84,7 +86,8 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { definition.type, operand, ); - selectFilter({ + + applyRecordFilter({ id: advancedFilterViewFilterId, fieldMetadataId: definition.fieldMetadataId, value, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx index 84f9addb447f..d933f2f2730e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx @@ -1,7 +1,7 @@ import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; +import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; @@ -25,7 +25,8 @@ export type ObjectFilterDropdownFilterSelectMenuItemProps = { export const ObjectFilterDropdownFilterSelectMenuItem = ({ filterDefinition, }: ObjectFilterDropdownFilterSelectMenuItemProps) => { - const { selectFilter } = useSelectFilter(); + const { selectFilterDefinitionUsedInDropdown } = + useSelectFilterDefinitionUsedInDropdown(); const [, setObjectFilterDropdownFirstLevelFilterDefinition] = useRecoilComponentStateV2( @@ -55,12 +56,8 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ const isACompositeField = isCompositeField(filterDefinition.type); - const { - setFilterDefinitionUsedInDropdown, - setSelectedOperandInDropdown, - setObjectFilterDropdownSearchInput, - advancedFilterViewFilterIdState, - } = useFilterDropdown(); + const { setSelectedOperandInDropdown, advancedFilterViewFilterIdState } = + useFilterDropdown(); const setHotkeyScope = useSetHotkeyScope(); @@ -72,11 +69,14 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ advancedFilterViewFilterId, ); - const handleSelectFilter = (availableFilterDefinition: FilterDefinition) => { + const handleSelectFilterDefinition = ( + availableFilterDefinition: FilterDefinition, + ) => { closeAdvancedFilterDropdown(); - selectFilter({ filterDefinition: availableFilterDefinition }); - setFilterDefinitionUsedInDropdown(availableFilterDefinition); + selectFilterDefinitionUsedInDropdown({ + filterDefinition: availableFilterDefinition, + }); if ( availableFilterDefinition.type === 'RELATION' || @@ -89,8 +89,6 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ getOperandsForFilterDefinition(availableFilterDefinition)[0], ); - setObjectFilterDropdownSearchInput(''); - setObjectFilterDropdownFilterIsSelected(true); }; @@ -107,7 +105,7 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ setObjectFilterDropdownFirstLevelFilterDefinition(filterDefinition); setObjectFilterDropdownIsSelectingCompositeField(true); } else { - handleSelectFilter(filterDefinition); + handleSelectFilterDefinition(filterDefinition); } }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx index 38246cf53df9..5222e6156996 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx @@ -2,6 +2,7 @@ import { ChangeEvent, useCallback, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; @@ -10,8 +11,10 @@ export const ObjectFilterDropdownNumberInput = () => { selectedOperandInDropdownState, filterDefinitionUsedInDropdownState, selectedFilterState, - selectFilter, } = useFilterDropdown(); + + const { applyRecordFilter } = useApplyRecordFilter(); + const [hasFocused, setHasFocused] = useState(false); const filterDefinitionUsedInDropdown = useRecoilValue( @@ -48,8 +51,10 @@ export const ObjectFilterDropdownNumberInput = () => { placeholder={filterDefinitionUsedInDropdown.label} onChange={(event: ChangeEvent) => { const newValue = event.target.value; + setInputValue(newValue); - selectFilter?.({ + + applyRecordFilter({ id: selectedFilter?.id ? selectedFilter.id : v4(), fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, value: newValue, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx index 3dfd6d6203ee..ba59dc97ea29 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx @@ -1,6 +1,7 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -22,9 +23,10 @@ export const ObjectFilterDropdownOperandSelect = () => { filterDefinitionUsedInDropdownState, setSelectedOperandInDropdown, selectedFilterState, - selectFilter, } = useFilterDropdown(); + const { applyRecordFilter } = useApplyRecordFilter(); + const { closeDropdown } = useDropdown(); const filterDefinitionUsedInDropdown = useRecoilValue( @@ -49,7 +51,7 @@ export const ObjectFilterDropdownOperandSelect = () => { setSelectedOperandInDropdown(newOperand); if (isValuelessOperand && isDefined(filterDefinitionUsedInDropdown)) { - selectFilter?.({ + applyRecordFilter({ id: v4(), fieldMetadataId: filterDefinitionUsedInDropdown?.fieldMetadataId ?? '', displayValue: '', @@ -71,7 +73,7 @@ export const ObjectFilterDropdownOperandSelect = () => { selectedFilter.displayValue, ); - selectFilter?.({ + applyRecordFilter({ id: selectedFilter.id ? selectedFilter.id : v4(), fieldMetadataId: selectedFilter.fieldMetadataId, displayValue, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx index c409cdf00636..0eb8035254ca 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx @@ -14,6 +14,7 @@ import { SelectableList } from '@/ui/layout/selectable-list/components/Selectabl import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/internal/useSelectableListStates'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { MenuItem, MenuItemMultiSelect } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; @@ -32,9 +33,10 @@ export const ObjectFilterDropdownOptionSelect = () => { selectedOperandInDropdownState, objectFilterDropdownSelectedOptionValuesState, selectedFilterState, - selectFilter, } = useFilterDropdown(); + const { applyRecordFilter } = useApplyRecordFilter(); + const { closeDropdown } = useDropdown(); const { selectedItemIdState } = useSelectableListStates({ @@ -128,7 +130,7 @@ export const ObjectFilterDropdownOptionSelect = () => { ? JSON.stringify(selectedOptions.map((option) => option.value)) : EMPTY_FILTER_VALUE; - selectFilter({ + applyRecordFilter({ id: selectedFilter?.id ? selectedFilter.id : v4(), definition: filterDefinitionUsedInDropdown, operand: selectedOperandInDropdown, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx index 95af2de52e4b..5982797053cd 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx @@ -1,6 +1,7 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues'; import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata'; @@ -34,7 +35,6 @@ export const ObjectFilterDropdownRatingInput = () => { selectedOperandInDropdownState, filterDefinitionUsedInDropdownState, selectedFilterState, - selectFilter, } = useFilterDropdown(); const filterDefinitionUsedInDropdown = useRecoilValue( @@ -46,6 +46,8 @@ export const ObjectFilterDropdownRatingInput = () => { const selectedFilter = useRecoilValue(selectedFilterState); + const { applyRecordFilter } = useApplyRecordFilter(); + return ( filterDefinitionUsedInDropdown && selectedOperandInDropdown && ( @@ -57,7 +59,7 @@ export const ObjectFilterDropdownRatingInput = () => { return; } - selectFilter?.({ + applyRecordFilter?.({ id: selectedFilter?.id ? selectedFilter.id : v4(), fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, value: convertFieldRatingValueToNumber(newValue), diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index 1bfb234f5935..1c5046a30e9b 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -5,6 +5,7 @@ import { v4 } from 'uuid'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectFilterDropdownRecordPinnedItems } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems'; import { CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID } from '@/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; @@ -37,9 +38,10 @@ export const ObjectFilterDropdownRecordSelect = ({ selectedOperandInDropdownState, selectedFilterState, objectFilterDropdownSelectedRecordIdsState, - selectFilter, } = useFilterDropdown(); + const { applyRecordFilter } = useApplyRecordFilter(viewComponentId); + const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(viewComponentId); @@ -187,7 +189,7 @@ export const ObjectFilterDropdownRecordSelect = ({ const filterId = viewFilter?.id ?? fieldId; - selectFilter({ + applyRecordFilter({ id: selectedFilter?.id ? selectedFilter.id : filterId, definition: filterDefinitionUsedInDropdown, operand: selectedOperandInDropdown, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx index cb46def5c76e..b0fac7b09497 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; @@ -29,10 +30,11 @@ export const ObjectFilterDropdownSourceSelect = ({ selectedFilterState, setObjectFilterDropdownSelectedRecordIds, objectFilterDropdownSelectedRecordIdsState, - selectFilter, emptyFilterButKeepDefinition, } = useFilterDropdown(); + const { applyRecordFilter } = useApplyRecordFilter(viewComponentId); + const { deleteCombinedViewFilter } = useDeleteCombinedViewFilters(viewComponentId); @@ -108,7 +110,7 @@ export const ObjectFilterDropdownSourceSelect = ({ const filterId = viewFilter?.id ?? fieldId; - selectFilter({ + applyRecordFilter({ id: selectedFilter?.id ? selectedFilter.id : filterId, definition: filterDefinitionUsedInDropdown, operand: selectedOperandInDropdown || ViewFilterOperand.Is, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx index b6508323bbe0..9f7c13af76e6 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx @@ -2,6 +2,7 @@ import { ChangeEvent, useCallback, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; @@ -12,7 +13,6 @@ export const ObjectFilterDropdownTextSearchInput = () => { objectFilterDropdownSearchInputState, setObjectFilterDropdownSearchInput, selectedFilterState, - selectFilter, } = useFilterDropdown(); const [filterId] = useState(v4()); @@ -29,6 +29,8 @@ export const ObjectFilterDropdownTextSearchInput = () => { ); const selectedFilter = useRecoilValue(selectedFilterState); + const { applyRecordFilter } = useApplyRecordFilter(); + const handleInputRef = useCallback( (node: HTMLInputElement | null) => { if (Boolean(node) && !hasFocused) { @@ -51,7 +53,7 @@ export const ObjectFilterDropdownTextSearchInput = () => { onChange={(event: ChangeEvent) => { setObjectFilterDropdownSearchInput(event.target.value); - selectFilter?.({ + applyRecordFilter({ id: selectedFilter?.id ?? filterId, fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, value: event.target.value, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx index a3007acdccc6..1433c976311f 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx @@ -2,6 +2,7 @@ import { expect } from '@storybook/test'; import { act, renderHook, waitFor } from '@testing-library/react'; import { RecoilRoot, useRecoilState } from 'recoil'; +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useFilterDropdownStates } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownStates'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; @@ -261,19 +262,26 @@ describe('useFilterDropdown', () => { it('should reset filter', async () => { const { result } = renderHook(() => { - const { resetFilter, selectFilter } = useFilterDropdown({ + const { resetFilter } = useFilterDropdown({ filterDropdownId, }); + const { applyRecordFilter } = useApplyRecordFilter(filterDropdownId); + const { selectedFilterState } = useFilterDropdownStates(filterDropdownId); const [selectedFilter, setSelectedFilter] = useRecoilState(selectedFilterState); - return { selectedFilter, setSelectedFilter, selectFilter, resetFilter }; + return { + selectedFilter, + setSelectedFilter, + applyRecordFilter, + resetFilter, + }; }, renderHookConfig); act(() => { - result.current.selectFilter(mockFilter); + result.current.applyRecordFilter(mockFilter); }); await waitFor(() => { @@ -291,12 +299,13 @@ describe('useFilterDropdown', () => { it('should call onFilterSelect when a filter option is set', async () => { const { result } = renderHook(() => { - const { selectFilter } = useFilterDropdown({ filterDropdownId }); + const { applyRecordFilter } = useApplyRecordFilter(filterDropdownId); + const { onFilterSelectState } = useFilterDropdownStates(filterDropdownId); const [onFilterSelect, setOnFilterSelect] = useRecoilState(onFilterSelectState); - return { onFilterSelect, setOnFilterSelect, selectFilter }; + return { onFilterSelect, setOnFilterSelect, applyRecordFilter }; }, renderHookConfig); const onFilterSelectMock = jest.fn(); @@ -304,7 +313,7 @@ describe('useFilterDropdown', () => { act(() => { result.current.setOnFilterSelect(onFilterSelectMock); - result.current.selectFilter(mockFilter); + result.current.applyRecordFilter(mockFilter); }); await waitFor(() => { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyRecordFilter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyRecordFilter.ts new file mode 100644 index 000000000000..cc19f836a5c3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useApplyRecordFilter.ts @@ -0,0 +1,48 @@ +import { onFilterSelectComponentState } from '@/object-record/object-filter-dropdown/states/onFilterSelectComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const useApplyRecordFilter = (componentInstanceId?: string) => { + const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters(); + const selectedFilterCallbackState = useRecoilComponentCallbackStateV2( + selectedFilterComponentState, + componentInstanceId, + ); + + const onFilterSelectCallbackState = useRecoilComponentCallbackStateV2( + onFilterSelectComponentState, + componentInstanceId, + ); + + const applyRecordFilter = useRecoilCallback( + ({ set, snapshot }) => + (filter: Filter | null) => { + set(selectedFilterCallbackState, filter); + + const onFilterSelect = getSnapshotValue( + snapshot, + onFilterSelectCallbackState, + ); + + if (isDefined(filter)) { + upsertCombinedViewFilter(filter); + } + + onFilterSelect?.(filter); + }, + [ + selectedFilterCallbackState, + onFilterSelectCallbackState, + upsertCombinedViewFilter, + ], + ); + + return { + applyRecordFilter, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts index df5acd6765a6..fe36bb1191b6 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts @@ -1,16 +1,12 @@ import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useFilterDropdownStates } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownStates'; -import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; -import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; -import { isDefined } from 'twenty-ui'; -import { Filter } from '../types/Filter'; type UseFilterDropdownProps = { filterDropdownId?: string; @@ -34,23 +30,6 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { advancedFilterViewFilterIdState, } = useFilterDropdownStates(componentInstanceId); - const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters(); - - const selectFilter = useRecoilCallback( - ({ set, snapshot }) => - (filter: Filter | null) => { - set(selectedFilterState, filter); - const onFilterSelect = getSnapshotValue(snapshot, onFilterSelectState); - - if (isDefined(filter)) { - upsertCombinedViewFilter(filter); - } - - onFilterSelect?.(filter); - }, - [selectedFilterState, onFilterSelectState, upsertCombinedViewFilter], - ); - const emptyFilterButKeepDefinition = useRecoilCallback( ({ set }) => () => { @@ -129,7 +108,6 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { return { componentInstanceId, - selectFilter, resetFilter, setSelectedFilter, setSelectedOperandInDropdown, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts similarity index 85% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts index af287141ea32..d919c7b20364 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts @@ -1,3 +1,4 @@ +import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; @@ -12,12 +13,11 @@ type SelectFilterParams = { filterDefinition: FilterDefinition; }; -export const useSelectFilter = () => { +export const useSelectFilterDefinitionUsedInDropdown = () => { const { setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, setObjectFilterDropdownSearchInput, - selectFilter: filterDropdownSelectFilter, advancedFilterViewFilterGroupIdState, advancedFilterViewFilterIdState, } = useFilterDropdown(); @@ -31,7 +31,11 @@ export const useSelectFilter = () => { const setHotkeyScope = useSetHotkeyScope(); - const selectFilter = ({ filterDefinition }: SelectFilterParams) => { + const { applyRecordFilter } = useApplyRecordFilter(); + + const selectFilterDefinitionUsedInDropdown = ({ + filterDefinition, + }: SelectFilterParams) => { setFilterDefinitionUsedInDropdown(filterDefinition); if ( @@ -53,7 +57,7 @@ export const useSelectFilter = () => { const isAdvancedFilter = isDefined(advancedFilterViewFilterId); if (isAdvancedFilter || value !== '') { - filterDropdownSelectFilter({ + applyRecordFilter({ id: advancedFilterViewFilterId ?? v4(), fieldMetadataId: filterDefinition.fieldMetadataId, displayValue, @@ -68,6 +72,6 @@ export const useSelectFilter = () => { }; return { - selectFilter, + selectFilterDefinitionUsedInDropdown, }; }; From 21774c60c7d1f82ed16e6940a5accd69efd2cd6b Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 9 Jan 2025 18:56:21 +0100 Subject: [PATCH 07/22] Refactored reset filter dropdown (#9523) This PR extracts reset filter logic from useFilterDropdown hook. The goal is to remove useFilterDropdown hook by incrementally removing each sub hook one by one. --------- Co-authored-by: Charles Bochet --- .../AddObjectFilterFromDetailsButton.tsx | 8 +- .../components/MultipleFiltersButton.tsx | 6 +- .../MultipleFiltersDropdownButton.tsx | 8 +- .../__tests__/useFilterDropdown.test.tsx | 10 +-- .../hooks/useFilterDropdown.ts | 41 ---------- .../hooks/useResetFilterDropdown.ts | 78 +++++++++++++++++++ 6 files changed, 93 insertions(+), 58 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useResetFilterDropdown.ts diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx index 21d407471462..739ab783d1cb 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx @@ -1,9 +1,9 @@ import { IconPlus, LightButton } from 'twenty-ui'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useResetFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useResetFilterDropdown'; type AddObjectFilterFromDetailsButtonProps = { filterDropdownId?: string; }; @@ -13,12 +13,10 @@ export const AddObjectFilterFromDetailsButton = ({ }: AddObjectFilterFromDetailsButtonProps) => { const { toggleDropdown } = useDropdown(OBJECT_FILTER_DROPDOWN_ID); - const { resetFilter } = useFilterDropdown({ - filterDropdownId: filterDropdownId, - }); + const { resetFilterDropdown } = useResetFilterDropdown(filterDropdownId); const handleClick = () => { - resetFilter(); + resetFilterDropdown(); toggleDropdown(); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersButton.tsx index 066cc9fb343f..5306d36811ca 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersButton.tsx @@ -1,10 +1,10 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { useResetFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useResetFilterDropdown'; import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; export const MultipleFiltersButton = () => { - const { resetFilter } = useFilterDropdown(); + const { resetFilterDropdown } = useResetFilterDropdown(); const { isDropdownOpen, toggleDropdown } = useDropdown( OBJECT_FILTER_DROPDOWN_ID, @@ -12,7 +12,7 @@ export const MultipleFiltersButton = () => { const handleClick = () => { toggleDropdown(); - resetFilter(); + resetFilterDropdown(); }; return ( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx index 0ba1d9146069..2ed3ece59c6d 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx @@ -1,5 +1,5 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { useResetFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useResetFilterDropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useCallback } from 'react'; @@ -14,11 +14,11 @@ type MultipleFiltersDropdownButtonProps = { export const MultipleFiltersDropdownButton = ({ hotkeyScope, }: MultipleFiltersDropdownButtonProps) => { - const { resetFilter } = useFilterDropdown(); + const { resetFilterDropdown } = useResetFilterDropdown(); const handleDropdownClose = useCallback(() => { - resetFilter(); - }, [resetFilter]); + resetFilterDropdown(); + }, [resetFilterDropdown]); return ( { it('should reset filter', async () => { const { result } = renderHook(() => { - const { resetFilter } = useFilterDropdown({ - filterDropdownId, - }); + const { resetFilterDropdown } = useResetFilterDropdown(filterDropdownId); const { applyRecordFilter } = useApplyRecordFilter(filterDropdownId); @@ -272,11 +271,12 @@ describe('useFilterDropdown', () => { const [selectedFilter, setSelectedFilter] = useRecoilState(selectedFilterState); + return { selectedFilter, setSelectedFilter, applyRecordFilter, - resetFilter, + resetFilterDropdown, }; }, renderHookConfig); @@ -289,7 +289,7 @@ describe('useFilterDropdown', () => { }); act(() => { - result.current.resetFilter(); + result.current.resetFilterDropdown(); }); await waitFor(() => { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts index fe36bb1191b6..42421e0dc2c5 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts @@ -3,10 +3,7 @@ import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useFilterDropdownStates } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownStates'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; -import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; -import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; -import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; type UseFilterDropdownProps = { filterDropdownId?: string; @@ -44,43 +41,6 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { ], ); - const setObjectFilterDropdownFilterIsSelectedCallbackState = - useRecoilComponentCallbackStateV2( - objectFilterDropdownFilterIsSelectedComponentState, - props?.filterDropdownId, - ); - - const setObjectFilterDropdownIsSelectingCompositeFieldCallbackState = - useRecoilComponentCallbackStateV2( - objectFilterDropdownIsSelectingCompositeFieldComponentState, - props?.filterDropdownId, - ); - - const resetFilter = useRecoilCallback( - ({ set }) => - () => { - set(objectFilterDropdownSearchInputState, ''); - set(objectFilterDropdownSelectedRecordIdsState, []); - set(selectedFilterState, undefined); - set(filterDefinitionUsedInDropdownState, null); - set(selectedOperandInDropdownState, null); - set(setObjectFilterDropdownFilterIsSelectedCallbackState, false); - set( - setObjectFilterDropdownIsSelectingCompositeFieldCallbackState, - false, - ); - }, - [ - filterDefinitionUsedInDropdownState, - objectFilterDropdownSearchInputState, - objectFilterDropdownSelectedRecordIdsState, - selectedFilterState, - selectedOperandInDropdownState, - setObjectFilterDropdownFilterIsSelectedCallbackState, - setObjectFilterDropdownIsSelectingCompositeFieldCallbackState, - ], - ); - const setSelectedFilter = useSetRecoilState(selectedFilterState); const setSelectedOperandInDropdown = useSetRecoilState( selectedOperandInDropdownState, @@ -108,7 +68,6 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { return { componentInstanceId, - resetFilter, setSelectedFilter, setSelectedOperandInDropdown, setFilterDefinitionUsedInDropdown, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useResetFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useResetFilterDropdown.ts new file mode 100644 index 000000000000..2f7b6cdf5477 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useResetFilterDropdown.ts @@ -0,0 +1,78 @@ +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; +import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; + +export const useResetFilterDropdown = (componentInstanceId?: string) => { + const objectFilterDropdownSearchInputCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownSearchInputComponentState, + componentInstanceId, + ); + + const objectFilterDropdownSelectedRecordIdsCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownSelectedRecordIdsComponentState, + componentInstanceId, + ); + + const selectedFilterCallbackState = useRecoilComponentCallbackStateV2( + selectedFilterComponentState, + componentInstanceId, + ); + + const filterDefinitionUsedInDropdownCallbackState = + useRecoilComponentCallbackStateV2( + filterDefinitionUsedInDropdownComponentState, + componentInstanceId, + ); + + const selectedOperandInDropdownCallbackState = + useRecoilComponentCallbackStateV2( + selectedOperandInDropdownComponentState, + componentInstanceId, + ); + + const objectFilterDropdownFilterIsSelectedCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownFilterIsSelectedComponentState, + componentInstanceId, + ); + + const objectFilterDropdownIsSelectingCompositeFieldCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownIsSelectingCompositeFieldComponentState, + componentInstanceId, + ); + + const resetFilterDropdown = useRecoilCallback( + ({ set }) => + () => { + set(objectFilterDropdownSearchInputCallbackState, ''); + set(objectFilterDropdownSelectedRecordIdsCallbackState, []); + set(selectedFilterCallbackState, undefined); + set(filterDefinitionUsedInDropdownCallbackState, null); + set(selectedOperandInDropdownCallbackState, null); + set(objectFilterDropdownFilterIsSelectedCallbackState, false); + set(objectFilterDropdownIsSelectingCompositeFieldCallbackState, false); + }, + [ + filterDefinitionUsedInDropdownCallbackState, + objectFilterDropdownSearchInputCallbackState, + objectFilterDropdownSelectedRecordIdsCallbackState, + selectedFilterCallbackState, + selectedOperandInDropdownCallbackState, + objectFilterDropdownFilterIsSelectedCallbackState, + objectFilterDropdownIsSelectingCompositeFieldCallbackState, + ], + ); + + return { + resetFilterDropdown, + }; +}; From 4c8c33831632c26064e5b323fef1f993596554c8 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 9 Jan 2025 18:58:36 +0100 Subject: [PATCH 08/22] Update ci runner for sb build --- .github/workflows/ci-front.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index 95f18fc990fc..a11c132e0115 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -13,7 +13,7 @@ concurrency: jobs: front-sb-build: timeout-minutes: 30 - runs-on: ubuntu-latest + runs-on: shipfox-8vcpu-ubuntu-2204 env: REACT_APP_SERVER_BASE_URL: http://localhost:3000 NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 From 1f1cac3b00f8e17bdd6b3aa06b0be5673c83ec6c Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Thu, 9 Jan 2025 19:12:36 +0100 Subject: [PATCH 09/22] chore: update returned attachement fullPath (#9516) ### Solution fullPath prop on attachement (when returned by backend) is updated to 'domain + path' (formerly 'path'). Consequently, getFileAbsoluteURI util in front is removed. closes #8763 --------- Co-authored-by: etiennejouan --- .../activities/components/ActivityRichTextEditor.tsx | 4 ++-- .../activities/files/components/AttachmentRow.tsx | 6 +++--- .../activities/files/hooks/useUploadAttachmentFile.tsx | 7 ++----- .../files/utils/__tests__/downloadFile.test.ts | 6 ++---- .../src/modules/activities/files/utils/downloadFile.ts | 3 +-- .../utils/file/__tests__/getFileAbsoluteURI.test.ts | 10 ---------- .../twenty-front/src/utils/file/getFileAbsoluteURI.ts | 5 ----- .../handlers/attachment-query-result-getter.handler.ts | 2 +- 8 files changed, 11 insertions(+), 32 deletions(-) delete mode 100644 packages/twenty-front/src/utils/file/__tests__/getFileAbsoluteURI.test.ts delete mode 100644 packages/twenty-front/src/utils/file/getFileAbsoluteURI.ts diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index 24b6045905dd..a39d230d8a2b 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -173,9 +173,9 @@ export const ActivityRichTextEditor = ({ }, [activity]); const handleEditorBuiltInUploadFile = async (file: File) => { - const { attachementAbsoluteURL } = await handleUploadAttachment(file); + const { attachmentAbsoluteURL } = await handleUploadAttachment(file); - return attachementAbsoluteURL; + return attachmentAbsoluteURL; }; const editor = useCreateBlockNote({ diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx index f0eca529d762..18f59b8ce000 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx @@ -17,7 +17,6 @@ import { useMemo, useState } from 'react'; import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui'; import { formatToHumanReadableDate } from '~/utils/date-utils'; -import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI'; import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension'; const StyledLeftContent = styled.div` @@ -139,8 +138,9 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { ) : ( diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx index bbd8d6152af4..46c71f194610 100644 --- a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx +++ b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx @@ -9,7 +9,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { isNonEmptyString } from '@sniptt/guards'; import { FileFolder, useUploadFileMutation } from '~/generated/graphql'; -import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI'; // Note: This is probably not the right way to do this. export const computePathWithoutToken = (attachmentPath: string): string => { @@ -56,11 +55,9 @@ export const useUploadAttachmentFile = () => { updatedAt: new Date().toISOString(), } as Partial; - await createOneAttachment(attachmentToCreate); + const createdAttachment = await createOneAttachment(attachmentToCreate); - const attachementAbsoluteURL = getFileAbsoluteURI(attachmentPath); - - return { attachementAbsoluteURL }; + return { attachmentAbsoluteURL: createdAttachment.fullPath }; }; return { uploadAttachmentFile }; 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 b25fae0e7c37..2261b584e36f 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 @@ -17,12 +17,10 @@ window.URL.revokeObjectURL = jest.fn(); describe.skip('downloadFile', () => { it('should download a file', () => { // Call downloadFile - downloadFile('path/to/file.pdf', 'file.pdf'); + downloadFile('url/to/file.pdf', 'file.pdf'); // Assert on fetch - expect(fetch).toHaveBeenCalledWith( - process.env.REACT_APP_SERVER_BASE_URL + '/files/path/to/file.pdf', - ); + expect(fetch).toHaveBeenCalledWith('url/to/file.pdf'); // Assert on element creation const link = document.querySelector( diff --git a/packages/twenty-front/src/modules/activities/files/utils/downloadFile.ts b/packages/twenty-front/src/modules/activities/files/utils/downloadFile.ts index af64fed4e226..bdc5ed0fab97 100644 --- a/packages/twenty-front/src/modules/activities/files/utils/downloadFile.ts +++ b/packages/twenty-front/src/modules/activities/files/utils/downloadFile.ts @@ -1,8 +1,7 @@ import { saveAs } from 'file-saver'; -import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI'; export const downloadFile = (fullPath: string, fileName: string) => { - fetch(getFileAbsoluteURI(fullPath)) + fetch(fullPath) .then((resp) => resp.status === 200 ? resp.blob() diff --git a/packages/twenty-front/src/utils/file/__tests__/getFileAbsoluteURI.test.ts b/packages/twenty-front/src/utils/file/__tests__/getFileAbsoluteURI.test.ts deleted file mode 100644 index 01a75cecaa94..000000000000 --- a/packages/twenty-front/src/utils/file/__tests__/getFileAbsoluteURI.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getFileAbsoluteURI } from '../getFileAbsoluteURI'; -import { REACT_APP_SERVER_BASE_URL } from '~/config'; - -describe('getFileAbsoluteURI', () => { - test('should return absolute uri', () => { - expect(getFileAbsoluteURI('foo')).toEqual( - `${REACT_APP_SERVER_BASE_URL}/files/foo`, - ); - }); -}); diff --git a/packages/twenty-front/src/utils/file/getFileAbsoluteURI.ts b/packages/twenty-front/src/utils/file/getFileAbsoluteURI.ts deleted file mode 100644 index f3777e38a92c..000000000000 --- a/packages/twenty-front/src/utils/file/getFileAbsoluteURI.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { REACT_APP_SERVER_BASE_URL } from '~/config'; - -export const getFileAbsoluteURI = (fileUrl?: string) => { - return `${REACT_APP_SERVER_BASE_URL}/files/${fileUrl}`; -}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts index 794657c7d269..87f9b413532e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts @@ -23,7 +23,7 @@ export class AttachmentQueryResultGetterHandler return { ...attachment, - fullPath: `${attachment.fullPath}?token=${signedPayload}`, + fullPath: `${process.env.SERVER_URL}/files/${attachment.fullPath}?token=${signedPayload}`, }; } } From 0a798a6671b96f00b4aa4caeb79b3378e3c855af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Thu, 9 Jan 2025 19:12:57 +0100 Subject: [PATCH 10/22] Feat/view groups fast follow (#9513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #9512 - 🟠 [Icon should be lighter](https://discord.com/channels/1130383047699738754/1326487470895923222) The current weight is the same as in Figma, waiting for confirmation - 🟠 [None has an unwanted margin left](https://discord.com/channels/1130383047699738754/1326493647796961323) This component is used in lot of places, removing the padding left can brake other places - 🟢 [All cells should have a the same right design](https://discord.com/channels/1130383047699738754/1326489001926066176) - 🔴 [Group Sorting should not "freeze" when mouse is release](https://discord.com/channels/1130383047699738754/1326494381795966996) Can't find a good way to fix it, seems more related to the fact it's running in debug mode. - 🟢 [Alignment issue](https://discord.com/channels/1130383047699738754/1326486523822084140) - 🟢 [View record count error](https://discord.com/channels/1130383047699738754/1326491489466978365) - 🟢 [Vertically align tags and numbers/count](https://discord.com/channels/1130383047699738754/1326490661800902728) - 🟢 [Display "Calculate" only on hover in view groups](https://discord.com/channels/1130383047699738754/1326490411929436191) - 🟢 [Aggregates height in view groups is 28px instead of 32px](https://discord.com/channels/1130383047699738754/1326489587127943188) - 🟠 [Picker under the aggregate](https://discord.com/channels/1130383047699738754/1326487940557439039) Can't reproduce the issue - 🟢 [Icon should not be hoverable](https://discord.com/channels/1130383047699738754/1326477402360123425) - 🟢 [Crop long view titles](https://discord.com/channels/1130383047699738754/1326477009576136755) - 🟢 [Removing the group by on opportunities (group by none) give an white screen](https://discord.com/channels/1130383047699738754/1324651927962910750) --- ...ptionsDropdownRecordGroupFieldsContent.tsx | 14 +- .../components/RecordIndexContainer.tsx | 2 +- .../RecordIndexTableContainerEffect.tsx | 10 +- .../hooks/useLoadRecordIndexBoard.ts | 123 ------------------ .../hooks/useSetRecordIndexEntityCount.ts | 49 +++++++ ...xEntityCountByGroupComponentFamilyState.ts | 12 ++ ...xEntityCountNoGroupComponentFamilyState.ts | 9 ++ ...recordIndexEntityCountComponentSelector.ts | 39 ++++++ .../record-table/components/RecordTable.tsx | 4 +- .../components/RecordTableRecordGroupRows.tsx | 13 ++ .../hooks/internal/useSetRecordTableData.ts | 7 +- .../record-table/hooks/useRecordTable.ts | 4 +- .../components/RecordTableBody.tsx | 2 +- .../RecordTableRecordGroupsBody.tsx | 12 -- .../components/RecordTableAggregateFooter.tsx | 119 +++-------------- .../RecordTableAggregateFooterCell.tsx | 1 + ...ordTableColumnAggregateFooterValueCell.tsx | 8 +- .../components/RecordTableActionRow.tsx | 7 +- .../RecordTableRecordGroupSection.tsx | 4 +- .../onEntityCountChangeComponentState.ts | 2 +- .../SignInBackgroundMockContainerEffect.tsx | 9 +- .../components/DropdownMenuHeader.tsx | 45 +++++-- .../hooks/useSetRecordCountInCurrentView.ts | 13 -- .../entityCountInCurrentViewComponentState.ts | 10 -- .../components/ViewPickerDropdown.tsx | 10 +- 25 files changed, 225 insertions(+), 303 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/hooks/useSetRecordIndexEntityCount.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/states/recordIndexEntityCountByGroupComponentFamilyState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/states/recordIndexEntityCountNoGroupComponentFamilyState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/states/selectors/recordIndexEntityCountComponentSelector.ts delete mode 100644 packages/twenty-front/src/modules/views/hooks/useSetRecordCountInCurrentView.ts delete mode 100644 packages/twenty-front/src/modules/views/states/entityCountInCurrentViewComponentState.ts diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx index 66eff4c465d5..8cb7beb4cfc0 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx @@ -24,6 +24,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { ViewType } from '@/views/types/ViewType'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -33,6 +34,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { const { getIcon } = useIcons(); const { + viewType, currentContentId, recordIndexId, objectMetadataItem, @@ -121,11 +123,13 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { onChange={(event) => setRecordGroupFieldSearchInput(event.target.value)} /> - + {viewType === ViewType.Table && ( + + )} {filteredRecordGroupFieldMetadataItems.map((fieldMetadataItem) => ( { const { columnDefinitions } = useColumnDefinitionsFromFieldMetadata(objectMetadataItem); - const { setRecordCountInCurrentView } = - useSetRecordCountInCurrentView(viewBarId); + const { setRecordIndexEntityCount } = useSetRecordIndexEntityCount(viewBarId); useEffect(() => { setAvailableTableColumns(columnDefinitions); @@ -68,9 +67,10 @@ export const RecordIndexTableContainerEffect = () => { useEffect(() => { setOnEntityCountChange( - () => (entityCount: number) => setRecordCountInCurrentView(entityCount), + () => (entityCount: number, currentRecordGroupId?: string) => + setRecordIndexEntityCount(entityCount, currentRecordGroupId), ); - }, [setRecordCountInCurrentView, setOnEntityCountChange]); + }, [setRecordIndexEntityCount, setOnEntityCountChange]); const setViewFieldAggregateOperation = useRecoilCallback( ({ set, snapshot }) => diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts deleted file mode 100644 index 9df7f7a2919f..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; - -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/useSetRecordBoardRecordIds'; -import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; -import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; -import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; -import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; -import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; -import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; -import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; -import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState'; -import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; -import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; -import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; - -type UseLoadRecordIndexBoardProps = { - objectNameSingular: string; - viewBarId: string; - recordBoardId: string; -}; - -export const useLoadRecordIndexBoard = ({ - objectNameSingular, - viewBarId, - recordBoardId, -}: UseLoadRecordIndexBoardProps) => { - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - - const setRecordBoardFieldDefinitions = useSetRecoilComponentStateV2( - recordBoardFieldDefinitionsComponentState, - recordBoardId, - ); - - const { setRecordIds: setRecordIdsInBoard } = - useSetRecordBoardRecordIds(recordBoardId); - - const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore(); - - const recordIndexFieldDefinitions = useRecoilValue( - recordIndexFieldDefinitionsState, - ); - useEffect(() => { - setRecordBoardFieldDefinitions(recordIndexFieldDefinitions); - }, [recordIndexFieldDefinitions, setRecordBoardFieldDefinitions]); - - const recordIndexViewFilterGroups = useRecoilValue( - recordIndexViewFilterGroupsState, - ); - - const recordIndexFilters = useRecoilValue(recordIndexFiltersState); - const recordIndexSorts = useRecoilValue(recordIndexSortsState); - - const { filterValueDependencies } = useFilterValueDependencies(); - - const requestFilters = computeViewRecordGqlOperationFilter( - filterValueDependencies, - recordIndexFilters, - objectMetadataItem?.fields ?? [], - recordIndexViewFilterGroups, - ); - const orderBy = turnSortsIntoOrderBy(objectMetadataItem, recordIndexSorts); - - const recordIndexIsCompactModeActive = useRecoilValue( - recordIndexIsCompactModeActiveState, - ); - - const recordGqlFields = useRecordBoardRecordGqlFields({ - objectMetadataItem, - recordBoardId, - }); - - const { - records, - totalCount, - loading, - fetchMoreRecords, - queryStateIdentifier, - } = useFindManyRecords({ - objectNameSingular, - filter: requestFilters, - orderBy, - recordGqlFields, - }); - - const { setRecordCountInCurrentView } = - useSetRecordCountInCurrentView(viewBarId); - - const setIsCompactModeActive = useSetRecoilComponentStateV2( - isRecordBoardCompactModeActiveComponentState, - recordBoardId, - ); - - useEffect(() => { - setRecordIdsInBoard(records); - }, [records, setRecordIdsInBoard]); - - useEffect(() => { - upsertRecordsInStore(records); - }, [records, upsertRecordsInStore]); - - useEffect(() => { - setRecordCountInCurrentView(totalCount); - }, [totalCount, setRecordCountInCurrentView]); - - useEffect(() => { - setIsCompactModeActive(recordIndexIsCompactModeActive); - }, [recordIndexIsCompactModeActive, setIsCompactModeActive]); - - return { - records, - loading, - fetchMoreRecords, - queryStateIdentifier, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useSetRecordIndexEntityCount.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useSetRecordIndexEntityCount.ts new file mode 100644 index 000000000000..69100ac7eef7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useSetRecordIndexEntityCount.ts @@ -0,0 +1,49 @@ +import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; +import { recordIndexEntityCountByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexEntityCountByGroupComponentFamilyState'; +import { recordIndexEntityCountNoGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexEntityCountNoGroupComponentFamilyState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from '~/utils/isDefined'; + +export const useSetRecordIndexEntityCount = (viewBarComponentId?: string) => { + const hasRecordGroup = useRecoilComponentValueV2( + hasRecordGroupsComponentSelector, + ); + + const recordIndexEntityCountNoGroupFamilyState = + useRecoilComponentCallbackStateV2( + recordIndexEntityCountNoGroupComponentFamilyState, + viewBarComponentId, + ); + + const recordIndexEntityCountByGroupFamilyState = + useRecoilComponentCallbackStateV2( + recordIndexEntityCountByGroupComponentFamilyState, + viewBarComponentId, + ); + + const setRecordIndexEntityCount = useRecoilCallback( + ({ set }) => + (count: number, recordGroupId?: string) => { + if (hasRecordGroup) { + if (!isDefined(recordGroupId)) { + throw new Error('Record group ID is required'); + } + + set(recordIndexEntityCountByGroupFamilyState(recordGroupId), count); + } else { + set(recordIndexEntityCountNoGroupFamilyState, count); + } + }, + [ + hasRecordGroup, + recordIndexEntityCountByGroupFamilyState, + recordIndexEntityCountNoGroupFamilyState, + ], + ); + + return { + setRecordIndexEntityCount, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexEntityCountByGroupComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexEntityCountByGroupComponentFamilyState.ts new file mode 100644 index 000000000000..ebcb7079086f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexEntityCountByGroupComponentFamilyState.ts @@ -0,0 +1,12 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const recordIndexEntityCountByGroupComponentFamilyState = + createComponentFamilyStateV2( + { + key: 'recordIndexEntityCountByGroupComponentFamilyState', + defaultValue: undefined, + componentInstanceContext: ViewComponentInstanceContext, + }, + ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexEntityCountNoGroupComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexEntityCountNoGroupComponentFamilyState.ts new file mode 100644 index 000000000000..911f7ae248d3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexEntityCountNoGroupComponentFamilyState.ts @@ -0,0 +1,9 @@ +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const recordIndexEntityCountNoGroupComponentFamilyState = + createComponentStateV2({ + key: 'recordIndexEntityCountNoGroupComponentFamilyState', + defaultValue: undefined, + componentInstanceContext: ViewComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/selectors/recordIndexEntityCountComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-index/states/selectors/recordIndexEntityCountComponentSelector.ts new file mode 100644 index 000000000000..1c364c7ea9b5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/selectors/recordIndexEntityCountComponentSelector.ts @@ -0,0 +1,39 @@ +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { recordIndexEntityCountByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexEntityCountByGroupComponentFamilyState'; +import { recordIndexEntityCountNoGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexEntityCountNoGroupComponentFamilyState'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const recordIndexEntityCountComponentSelector = + createComponentSelectorV2({ + key: 'recordIndexEntityCountComponentSelector', + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + if (recordGroupIds.length === 0) { + return get( + recordIndexEntityCountNoGroupComponentFamilyState.atomFamily({ + instanceId, + }), + ); + } + + return recordGroupIds.reduce((acc, recordGroupId) => { + const count = get( + recordIndexEntityCountByGroupComponentFamilyState.atomFamily({ + instanceId, + familyKey: recordGroupId, + }), + ); + + return acc + (count ?? 0); + }, 0); + }, + componentInstanceContext: ViewComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 571510fcb2b6..b23c08ca398e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -100,9 +100,7 @@ export const RecordTable = () => { {isAggregateQueryEnabled && !hasRecordGroups && !isRecordTableInitialLoading && - allRecordIds.length > 0 && ( - - )} + allRecordIds.length > 0 && } { + const isAggregateQueryEnabled = useIsFeatureEnabled( + FeatureFlagKey.IsAggregateQueryEnabled, + ); + const currentRecordGroupId = useCurrentRecordGroupId(); const allRecordIds = useRecoilComponentValueV2( @@ -59,6 +66,12 @@ export const RecordTableRecordGroupRows = () => { + {isAggregateQueryEnabled && ( + + )} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts index dc09fd20aa1c..3cac0bca295c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts @@ -13,7 +13,10 @@ import { isDefined } from '~/utils/isDefined'; type useSetRecordTableDataProps = { recordTableId?: string; - onEntityCountChange: (entityCount?: number) => void; + onEntityCountChange: ( + entityCount?: number, + currentRecordGroupId?: string, + ) => void; }; export const useSetRecordTableData = ({ @@ -93,7 +96,7 @@ export const useSetRecordTableData = ({ set(recordIndexAllRecordIdsSelector, recordIds); } - onEntityCountChange(totalCount); + onEntityCountChange(totalCount, currentRecordGroupId); } }, [ diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 9cd173f5d432..cd54668fd1d9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -156,13 +156,13 @@ export const useRecordTable = (props?: useRecordTableProps) => { const onEntityCountChange = useRecoilCallback( ({ snapshot }) => - (count?: number) => { + (count?: number, currentRecordGroupId?: string) => { const onEntityCountChange = getSnapshotValue( snapshot, onEntityCountChangeState, ); - onEntityCountChange?.(count); + onEntityCountChange?.(count, currentRecordGroupId); }, [onEntityCountChangeState], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx index 7a6dbe2975e6..d121d7b76ef8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBody.tsx @@ -33,7 +33,7 @@ const StyledTbody = styled.tbody` } } - &::after { + &:not(.disable-shadow)::after { content: ''; position: absolute; top: -1px; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx index 2d7703d99c02..087019240b0c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx @@ -6,19 +6,13 @@ import { RecordTableRecordGroupRows } from '@/object-record/record-table/compone import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable'; import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading'; import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider'; -import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter'; import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewType } from '@/views/types/ViewType'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated/graphql'; export const RecordTableRecordGroupsBody = () => { - const isAggregateQueryEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsAggregateQueryEnabled, - ); const allRecordIds = useRecoilComponentValueV2( recordIndexAllRecordIdsComponentSelector, ); @@ -49,12 +43,6 @@ export const RecordTableRecordGroupsBody = () => { - {isAggregateQueryEnabled && ( - - )} ))} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx index 709643e013ed..1ddda73884b1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter.tsx @@ -1,124 +1,41 @@ import styled from '@emotion/styled'; -import { MOBILE_VIEWPORT } from 'twenty-ui'; import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell'; import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; -import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -const StyledTableFoot = styled.thead<{ endOfTableSticky?: boolean }>` - cursor: pointer; - - th:nth-of-type(1) { - width: ${FIRST_TH_WIDTH}; - left: 0; - border-right-color: ${({ theme }) => theme.background.primary}; - } - - th:nth-of-type(2) { - border-right-color: ${({ theme }) => theme.background.primary}; - } - - &.first-columns-sticky { - th:nth-of-type(1) { - position: sticky; - left: 0; - z-index: 5; - transition: 0.3s ease; - } - - th:nth-of-type(2) { - position: sticky; - left: 11px; - z-index: 5; - transition: 0.3s ease; - } - - th:nth-of-type(3) { - position: sticky; - left: 43px; - z-index: 5; - transition: 0.3s ease; - - &::after { - content: ''; - position: absolute; - top: -1px; - height: calc(100% + 2px); - width: 4px; - right: 0px; - box-shadow: ${({ theme }) => theme.boxShadow.light}; - clip-path: inset(0px -4px 0px 0px); - } - - @media (max-width: ${MOBILE_VIEWPORT}px) { - width: 34px; - max-width: 34px; - } - } - } - - tr { - position: sticky; - z-index: 5; - background: ${({ theme }) => theme.background.primary}; - ${({ endOfTableSticky }) => - endOfTableSticky && - ` - bottom: 10px; - &::after { - content: ''; - position: absolute; - bottom: -10px; - left: 0; - right: 0; - height: 10px; - background: inherit; - } - `} - } -`; - const StyledTh = styled.th` background-color: ${({ theme }) => theme.background.primary}; `; export const RecordTableAggregateFooter = ({ currentRecordGroupId, - endOfTableSticky, }: { currentRecordGroupId?: string; - endOfTableSticky?: boolean; }) => { const visibleTableColumns = useRecoilComponentValueV2( visibleTableColumnsComponentSelector, ); return ( - - - - - {visibleTableColumns.map((column, index) => ( - - - - ))} - - + + + + {visibleTableColumns.map((column, index) => ( + + + + ))} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx index f6f66b4dce39..febc995d6e39 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell.tsx @@ -34,6 +34,7 @@ const StyledColumnFooterCell = styled.th<{ }; `; }}; + height: 32px; user-select: none; overflow: auto; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx index b6f91db7c606..421ee8245a5c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValueCell.tsx @@ -1,7 +1,9 @@ +import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext'; import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue'; import { hasAggregateOperationForViewFieldFamilySelector } from '@/object-record/record-table/record-table-footer/states/hasAggregateOperationForViewFieldFamilySelector'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useContext, useState } from 'react'; @@ -53,6 +55,10 @@ export const RecordTableColumnAggregateFooterValueCell = ({ }), ); + const hasRecordGroups = useRecoilComponentValueV2( + hasRecordGroupsComponentSelector, + ); + return (
{ @@ -64,7 +70,7 @@ export const RecordTableColumnAggregateFooterValueCell = ({ {isHovered || isDropdownOpen || hasAggregateOperationForViewField || - isFirstCell ? ( + (isFirstCell && !hasRecordGroups) ? ( <> theme.font.color.secondary}; - text-align: center; - vertical-align: middle; - padding-top: 3px; + display: flex; + height: 32px; + align-items: center; + justify-content: center; `; const StyledRecordTableTdTextContainer = styled(RecordTableTd)` diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx index 8c38a68be747..ea1da6afd026 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx @@ -38,6 +38,8 @@ const StyledTotalRow = styled.span` const StyledRecordGroupSection = styled(RecordTableTd)` border-right: none; height: 32px; + display: flex; + align-items: center; `; const StyledEmptyTd = styled.td` @@ -92,7 +94,7 @@ export const RecordTableRecordGroupSection = () => { - + void) | undefined + ((entityCount?: number, currentRecordGroupId?: string) => void) | undefined >({ key: 'onEntityCountChangeComponentState', defaultValue: undefined, diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx index 2bad3fa95ffc..04b9d9205e48 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx @@ -2,13 +2,13 @@ import { useEffect } from 'react'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { useSetRecordIndexEntityCount } from '@/object-record/record-index/hooks/useSetRecordIndexEntityCount'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions'; import { SIGN_IN_BACKGROUND_MOCK_FILTER_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockFilterDefinitions'; import { SIGN_IN_BACKGROUND_MOCK_SORT_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockSortDefinitions'; import { SIGN_IN_BACKGROUND_MOCK_VIEW_FIELDS } from '@/sign-in-background-mock/constants/SignInBackgroundMockViewFields'; import { useInitViewBar } from '@/views/hooks/useInitViewBar'; -import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions'; type SignInBackgroundMockContainerEffectProps = { @@ -42,8 +42,7 @@ export const SignInBackgroundMockContainerEffect = ({ setViewObjectMetadataId, } = useInitViewBar(viewId); - const { setRecordCountInCurrentView } = - useSetRecordCountInCurrentView(viewId); + const { setRecordIndexEntityCount } = useSetRecordIndexEntityCount(viewId); useEffect(() => { setViewObjectMetadataId?.(objectMetadataItem.id); @@ -72,9 +71,9 @@ export const SignInBackgroundMockContainerEffect = ({ useEffect(() => { setOnEntityCountChange( - () => (entityCount: number) => setRecordCountInCurrentView(entityCount), + () => (entityCount: number) => setRecordIndexEntityCount(entityCount), ); - }, [setRecordCountInCurrentView, setOnEntityCountChange]); + }, [setRecordIndexEntityCount, setOnEntityCountChange]); return <>; }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx index 02ed2027b82b..901b47aad800 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx @@ -1,7 +1,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ComponentProps, MouseEvent } from 'react'; -import { IconComponent, LightIconButton } from 'twenty-ui'; +import { IconComponent, isDefined, LightIconButton } from 'twenty-ui'; const StyledHeader = styled.li` align-items: center; @@ -41,6 +41,26 @@ const StyledEndIcon = styled.div` const StyledChildrenWrapper = styled.span` overflow: hidden; padding: 0 ${({ theme }) => theme.spacing(1)}; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const StyledNonClickableStartIcon = styled.div` + align-items: center; + background: transparent; + border: none; + + display: flex; + flex-direction: row; + + font-family: ${({ theme }) => theme.font.family}; + font-weight: ${({ theme }) => theme.font.weight.regular}; + gap: ${({ theme }) => theme.spacing(1)}; + justify-content: center; + + white-space: nowrap; + height: 24px; + width: 24px; `; type DropdownMenuHeaderProps = ComponentProps<'li'> & { @@ -76,13 +96,22 @@ export const DropdownMenuHeader = ({ )} {StartIcon && ( - + {isDefined(onClick) ? ( + + ) : ( + + + + )} {children} )} diff --git a/packages/twenty-front/src/modules/views/hooks/useSetRecordCountInCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useSetRecordCountInCurrentView.ts deleted file mode 100644 index 778fa6906c6f..000000000000 --- a/packages/twenty-front/src/modules/views/hooks/useSetRecordCountInCurrentView.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { entityCountInCurrentViewComponentState } from '@/views/states/entityCountInCurrentViewComponentState'; - -export const useSetRecordCountInCurrentView = (viewBarComponentId?: string) => { - const setEntityCountInCurrentView = useSetRecoilComponentStateV2( - entityCountInCurrentViewComponentState, - viewBarComponentId, - ); - - return { - setRecordCountInCurrentView: setEntityCountInCurrentView, - }; -}; diff --git a/packages/twenty-front/src/modules/views/states/entityCountInCurrentViewComponentState.ts b/packages/twenty-front/src/modules/views/states/entityCountInCurrentViewComponentState.ts deleted file mode 100644 index f189084828ac..000000000000 --- a/packages/twenty-front/src/modules/views/states/entityCountInCurrentViewComponentState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; - -export const entityCountInCurrentViewComponentState = createComponentStateV2< - number | undefined ->({ - key: 'entityCountInCurrentViewComponentState', - defaultValue: undefined, - componentInstanceContext: ViewComponentInstanceContext, -}); diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx index 371a2c6aec52..59709a48f14d 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerDropdown.tsx @@ -7,12 +7,12 @@ import { useIcons, } from 'twenty-ui'; +import { recordIndexEntityCountComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexEntityCountComponentSelector'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; -import { entityCountInCurrentViewComponentState } from '@/views/states/entityCountInCurrentViewComponentState'; import { ViewsHotkeyScope } from '@/views/types/ViewsHotkeyScope'; import { ViewPickerContentCreateMode } from '@/views/view-picker/components/ViewPickerContentCreateMode'; import { ViewPickerContentEditMode } from '@/views/view-picker/components/ViewPickerContentEditMode'; @@ -55,8 +55,8 @@ export const ViewPickerDropdown = () => { const { updateViewFromCurrentState } = useUpdateViewFromCurrentState(); - const entityCountInCurrentView = useRecoilComponentValueV2( - entityCountInCurrentViewComponentState, + const entityCount = useRecoilComponentValueV2( + recordIndexEntityCountComponentSelector, ); const { isDropdownOpen: isViewsListDropdownOpen } = useDropdown( @@ -94,9 +94,7 @@ export const ViewPickerDropdown = () => { {currentViewWithCombinedFiltersAndSorts?.name ?? 'All'} - {isDefined(entityCountInCurrentView) && ( - <>· {entityCountInCurrentView} - )} + {isDefined(entityCount) && <>· {entityCount} } From 654a0bb79e90a619585f87987280aa471080f874 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 10 Jan 2025 12:29:22 +0100 Subject: [PATCH 11/22] Fix CI Server --- .github/workflows/ci-server.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index 806bfe725f75..03da78e9d8ea 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -187,12 +187,10 @@ jobs: - name: Update .env.test for billing if: steps.changed-files.outputs.any_changed == 'true' run: | - sed -i '$ a\ -IS_BILLING_ENABLED=true\ -BILLING_STRIPE_API_KEY=test-api-key\ -BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id\ -BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret' .env.test - + echo "IS_BILLING_ENABLED=true" >> .env.test + echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test + echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test + echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test - name: Server / Restore Task Cache if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache From c716a30d92e7c4237e976174866872585fb23ec1 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 10 Jan 2025 12:30:01 +0100 Subject: [PATCH 12/22] Refactored empty filter (#9532) Refactored empty filter function and removed it from useFilterDropdown hook --- ...lterDropdownRecordRemoveFilterMenuItem.tsx | 6 +-- .../ObjectFilterDropdownSourceSelect.tsx | 6 ++- .../hooks/useEmptyRecordFilter.ts | 42 +++++++++++++++++++ .../hooks/useFilterDropdown.ts | 17 +------- 4 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useEmptyRecordFilter.ts diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordRemoveFilterMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordRemoveFilterMenuItem.tsx index 7251fab80788..dafbabe7634e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordRemoveFilterMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordRemoveFilterMenuItem.tsx @@ -1,16 +1,16 @@ import { IconFilterOff, MenuItem } from 'twenty-ui'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { useEmptyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useEmptyRecordFilter'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; export const ObjectFilterDropdownRecordRemoveFilterMenuItem = () => { - const { emptyFilterButKeepDefinition } = useFilterDropdown(); + const { emptyRecordFilter } = useEmptyRecordFilter(); const { closeDropdown } = useDropdown(); const handleRemoveFilter = () => { - emptyFilterButKeepDefinition(); + emptyRecordFilter(); closeDropdown(); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx index b0fac7b09497..702706ba8b90 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; +import { useEmptyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useEmptyRecordFilter'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; @@ -30,7 +31,6 @@ export const ObjectFilterDropdownSourceSelect = ({ selectedFilterState, setObjectFilterDropdownSelectedRecordIds, objectFilterDropdownSelectedRecordIdsState, - emptyFilterButKeepDefinition, } = useFilterDropdown(); const { applyRecordFilter } = useApplyRecordFilter(viewComponentId); @@ -65,6 +65,8 @@ export const ObjectFilterDropdownSourceSelect = ({ objectFilterDropdownSelectedRecordIds.includes(option.id), ); + const { emptyRecordFilter } = useEmptyRecordFilter(); + const handleMultipleItemSelectChange = ( itemToSelect: SelectableItem, newSelectedValue: boolean, @@ -76,7 +78,7 @@ export const ObjectFilterDropdownSourceSelect = ({ ); if (newSelectedItemIds.length === 0) { - emptyFilterButKeepDefinition(); + emptyRecordFilter(); deleteCombinedViewFilter(fieldId); return; } diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useEmptyRecordFilter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useEmptyRecordFilter.ts new file mode 100644 index 000000000000..93e70009c1af --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useEmptyRecordFilter.ts @@ -0,0 +1,42 @@ +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilCallback } from 'recoil'; + +export const useEmptyRecordFilter = (componentInstanceId?: string) => { + const objectFilterDropdownSearchInputCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownSearchInputComponentState, + componentInstanceId, + ); + + const objectFilterDropdownSelectedRecordIdsCallbackState = + useRecoilComponentCallbackStateV2( + objectFilterDropdownSelectedRecordIdsComponentState, + componentInstanceId, + ); + + const selectedFilterCallbackState = useRecoilComponentCallbackStateV2( + selectedFilterComponentState, + componentInstanceId, + ); + + const emptyRecordFilter = useRecoilCallback( + ({ set }) => + () => { + set(objectFilterDropdownSearchInputCallbackState, ''); + set(objectFilterDropdownSelectedRecordIdsCallbackState, []); + set(selectedFilterCallbackState, undefined); + }, + [ + objectFilterDropdownSearchInputCallbackState, + objectFilterDropdownSelectedRecordIdsCallbackState, + selectedFilterCallbackState, + ], + ); + + return { + emptyRecordFilter, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts index 42421e0dc2c5..e75b2477d899 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts @@ -1,4 +1,4 @@ -import { useRecoilCallback, useSetRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; import { useFilterDropdownStates } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownStates'; @@ -27,20 +27,6 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { advancedFilterViewFilterIdState, } = useFilterDropdownStates(componentInstanceId); - const emptyFilterButKeepDefinition = useRecoilCallback( - ({ set }) => - () => { - set(objectFilterDropdownSearchInputState, ''); - set(objectFilterDropdownSelectedRecordIdsState, []); - set(selectedFilterState, undefined); - }, - [ - objectFilterDropdownSearchInputState, - objectFilterDropdownSelectedRecordIdsState, - selectedFilterState, - ], - ); - const setSelectedFilter = useSetRecoilState(selectedFilterState); const setSelectedOperandInDropdown = useSetRecoilState( selectedOperandInDropdownState, @@ -77,7 +63,6 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { setOnFilterSelect, setAdvancedFilterViewFilterGroupId, setAdvancedFilterViewFilterId, - emptyFilterButKeepDefinition, filterDefinitionUsedInDropdownState, objectFilterDropdownSearchInputState, objectFilterDropdownSelectedRecordIdsState, From 75bf9e3c69e1e1687416997ea23bc25ee70ad6fa Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Fri, 10 Jan 2025 12:30:42 +0100 Subject: [PATCH 13/22] fix(admin-panel): resolve feature flag key mismatch (#9530) Update feature flag handling by mapping input keys to enum values. This ensures compatibility and prevents potential runtime errors when updating workspace feature flags. --- .../admin-panel/admin-panel.resolver.ts | 3 +- .../admin-panel/admin-panel.service.ts | 20 +- .../admin-panel/admin-panel.spec.ts | 214 ++++++++++++++++++ .../feature-flag/feature-flag.entity.ts | 2 +- .../validates/feature-flag.validate.spec.ts | 24 ++ .../validates/feature-flag.validate.ts | 17 ++ .../engine/guards/impersonate-guard.spec.ts | 54 +++++ 7 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.ts create mode 100644 packages/twenty-server/src/engine/guards/impersonate-guard.spec.ts diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts index ea18ea80b801..16ac678aaccd 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -11,6 +11,7 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard'; import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @Resolver() @UseFilters(AuthGraphqlApiExceptionFilter) @@ -40,7 +41,7 @@ export class AdminPanelResolver { ): Promise { await this.adminService.updateWorkspaceFeatureFlags( updateFlagInput.workspaceId, - updateFlagInput.featureFlag, + FeatureFlagKey[updateFlagInput.featureFlag], updateFlagInput.value, ); diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index a50708e72e2a..0d34a6462479 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -15,6 +15,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; +import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate'; @Injectable() export class AdminPanelService { @@ -44,14 +45,9 @@ export class AdminPanelService { userValidator.assertIsDefinedOrThrow( user, - new AuthException('User not found', AuthExceptionCode.INVALID_INPUT), - ); - - workspaceValidator.assertIsDefinedOrThrow( - user.workspaces[0].workspace, new AuthException( - 'Impersonation not allowed', - AuthExceptionCode.FORBIDDEN_EXCEPTION, + 'User not found or impersonation not enable on workspace', + AuthExceptionCode.INVALID_INPUT, ), ); @@ -122,9 +118,17 @@ export class AdminPanelService { async updateWorkspaceFeatureFlags( workspaceId: string, - featureFlag: FeatureFlagKey, + featureFlag: string, value: boolean, ) { + featureFlagValidator.assertIsFeatureFlagKey( + featureFlag, + new AuthException( + 'Invalid feature flag key', + AuthExceptionCode.INVALID_INPUT, + ), + ); + const workspace = await this.workspaceRepository.findOne({ where: { id: workspaceId }, relations: ['featureFlags'], diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.spec.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.spec.ts new file mode 100644 index 000000000000..1868576df2d3 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.spec.ts @@ -0,0 +1,214 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { expect, jest } from '@jest/globals'; + +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; + +const UserFindOneMock = jest.fn(); +const WorkspaceFindOneMock = jest.fn(); +const FeatureFlagUpdateMock = jest.fn(); +const FeatureFlagSaveMock = jest.fn(); +const LoginTokenServiceGenerateLoginTokenMock = jest.fn(); + +jest.mock( + 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum', + () => { + return { + FeatureFlagKey: { + IsFlagEnabled: 'IS_FLAG_ENABLED', + }, + }; + }, +); + +describe('AdminPanelService', () => { + let service: AdminPanelService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AdminPanelService, + { + provide: getRepositoryToken(Workspace, 'core'), + useValue: { + findOne: WorkspaceFindOneMock, + }, + }, + { + provide: getRepositoryToken(User, 'core'), + useValue: { + findOne: UserFindOneMock, + }, + }, + { + provide: getRepositoryToken(FeatureFlagEntity, 'core'), + useValue: { + update: FeatureFlagUpdateMock, + save: FeatureFlagSaveMock, + }, + }, + { + provide: LoginTokenService, + useValue: { + generateLoginToken: LoginTokenServiceGenerateLoginTokenMock, + }, + }, + ], + }).compile(); + + service = module.get(AdminPanelService); + }); + + it('should be defined', async () => { + expect(service).toBeDefined(); + }); + + it('should update an existing feature flag if it exists', async () => { + const workspaceId = 'workspace-id'; + const featureFlag = 'IsFlagEnabled'; + const value = true; + const existingFlag = { id: 'flag-id', key: featureFlag, value: false }; + + WorkspaceFindOneMock.mockReturnValueOnce({ + id: workspaceId, + featureFlags: [existingFlag], + }); + + await service.updateWorkspaceFeatureFlags(workspaceId, featureFlag, value); + + expect(FeatureFlagUpdateMock).toHaveBeenCalledWith(existingFlag.id, { + value, + }); + expect(FeatureFlagSaveMock).not.toHaveBeenCalled(); + }); + + it('should create a new feature flag if it does not exist', async () => { + const workspaceId = 'workspace-id'; + const featureFlag = 'IsFlagEnabled'; + const value = true; + + WorkspaceFindOneMock.mockReturnValueOnce({ + id: workspaceId, + featureFlags: [], + }); + + await service.updateWorkspaceFeatureFlags(workspaceId, featureFlag, value); + + expect(FeatureFlagSaveMock).toHaveBeenCalledWith({ + key: featureFlag, + value, + workspaceId, + }); + expect(FeatureFlagUpdateMock).not.toHaveBeenCalled(); + }); + + it('should throw an exception if the workspace is not found', async () => { + const workspaceId = 'non-existent-workspace'; + const featureFlag = 'IsFlagEnabled'; + const value = true; + + WorkspaceFindOneMock.mockReturnValueOnce(null); + + await expect( + service.updateWorkspaceFeatureFlags(workspaceId, featureFlag, value), + ).rejects.toThrowError( + new AuthException('Workspace not found', AuthExceptionCode.INVALID_INPUT), + ); + }); + + it('should throw an exception if the flag is not found', async () => { + const workspaceId = 'non-existent-workspace'; + const featureFlag = 'IsUnknownFlagEnabled'; + const value = true; + + WorkspaceFindOneMock.mockReturnValueOnce(null); + + await expect( + service.updateWorkspaceFeatureFlags(workspaceId, featureFlag, value), + ).rejects.toThrowError( + new AuthException( + 'Invalid feature flag key', + AuthExceptionCode.INVALID_INPUT, + ), + ); + }); + + it('should impersonate a user and return workspace and loginToken on success', async () => { + const mockUser = { + id: 'user-id', + email: 'user@example.com', + workspaces: [ + { + workspace: { + id: 'workspace-id', + allowImpersonation: true, + subdomain: 'example-subdomain', + }, + }, + ], + }; + + UserFindOneMock.mockReturnValueOnce(mockUser); + LoginTokenServiceGenerateLoginTokenMock.mockReturnValueOnce({ + token: 'mock-login-token', + expiresAt: new Date(), + }); + + const result = await service.impersonate('user-id', 'workspace-id'); + + expect(UserFindOneMock).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + id: 'user-id', + workspaces: { + workspaceId: 'workspace-id', + workspace: { allowImpersonation: true }, + }, + }), + relations: ['workspaces', 'workspaces.workspace'], + }), + ); + + expect(LoginTokenServiceGenerateLoginTokenMock).toHaveBeenCalledWith( + 'user@example.com', + 'workspace-id', + ); + + expect(result).toEqual( + expect.objectContaining({ + workspace: { + id: 'workspace-id', + subdomain: 'example-subdomain', + }, + loginToken: expect.objectContaining({ + token: 'mock-login-token', + expiresAt: expect.any(Date), + }), + }), + ); + }); + + it('should throw an error when user is not found', async () => { + UserFindOneMock.mockReturnValueOnce(null); + + await expect( + service.impersonate('invalid-user-id', 'workspace-id'), + ).rejects.toThrow( + new AuthException( + 'User not found or impersonation not enable on workspace', + AuthExceptionCode.INVALID_INPUT, + ), + ); + + expect(UserFindOneMock).toHaveBeenCalled(); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts index 218dc047f5ab..5fc9422e8f0d 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.entity.ts @@ -26,7 +26,7 @@ export class FeatureFlagEntity { @Field(() => FeatureFlagKey) @Column({ nullable: false, type: 'text' }) - key: FeatureFlagKey; + key: `${FeatureFlagKey}`; @Field() @Column({ nullable: false, type: 'uuid' }) diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.spec.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.spec.ts new file mode 100644 index 000000000000..19286d646ddb --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.spec.ts @@ -0,0 +1,24 @@ +import { CustomException } from 'src/utils/custom-exception'; +import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate'; + +describe('featureFlagValidator', () => { + describe('assertIsFeatureFlagKey', () => { + it('should not throw error if featureFlagKey is valid', () => { + expect(() => + featureFlagValidator.assertIsFeatureFlagKey( + 'IsWorkflowEnabled', + new CustomException('Error', 'Error'), + ), + ).not.toThrow(); + }); + + it('should throw error if featureFlagKey is invalid', () => { + const invalidKey = 'InvalidKey'; + const exception = new CustomException('Error', 'Error'); + + expect(() => + featureFlagValidator.assertIsFeatureFlagKey(invalidKey, exception), + ).toThrow(exception); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.ts new file mode 100644 index 000000000000..40245a9fc01e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/validates/feature-flag.validate.ts @@ -0,0 +1,17 @@ +import { CustomException } from 'src/utils/custom-exception'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { isDefined } from 'src/utils/is-defined'; + +const assertIsFeatureFlagKey = ( + featureFlagKey: string, + exceptionToThrow: CustomException, +): asserts featureFlagKey is FeatureFlagKey => { + if (isDefined(FeatureFlagKey[featureFlagKey])) return; + throw exceptionToThrow; +}; + +export const featureFlagValidator: { + assertIsFeatureFlagKey: typeof assertIsFeatureFlagKey; +} = { + assertIsFeatureFlagKey: assertIsFeatureFlagKey, +}; diff --git a/packages/twenty-server/src/engine/guards/impersonate-guard.spec.ts b/packages/twenty-server/src/engine/guards/impersonate-guard.spec.ts new file mode 100644 index 000000000000..5135bd41790c --- /dev/null +++ b/packages/twenty-server/src/engine/guards/impersonate-guard.spec.ts @@ -0,0 +1,54 @@ +import { ExecutionContext } from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; + +import { expect, jest } from '@jest/globals'; + +import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard'; + +describe('ImpersonateGuard', () => { + const guard = new ImpersonateGuard(); + + it('should return true if user can impersonate', async () => { + const mockContext = { + getContext: jest.fn(() => ({ + req: { + user: { + canImpersonate: true, + }, + }, + })), + }; + + jest + .spyOn(GqlExecutionContext, 'create') + .mockReturnValue(mockContext as any); + + const mockExecutionContext = {} as ExecutionContext; + + const result = await guard.canActivate(mockExecutionContext); + + expect(result).toBe(true); + }); + + it('should return false if user cannot impersonate', async () => { + const mockContext = { + getContext: jest.fn(() => ({ + req: { + user: { + canImpersonate: false, + }, + }, + })), + }; + + jest + .spyOn(GqlExecutionContext, 'create') + .mockReturnValue(mockContext as any); + + const mockExecutionContext = {} as ExecutionContext; + + const result = await guard.canActivate(mockExecutionContext); + + expect(result).toBe(false); + }); +}); From ddcb3dfd283acfcbc42cb556ce1811543f78ea3c Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Fri, 10 Jan 2025 18:34:00 +0530 Subject: [PATCH 14/22] Feature flags env variable gating (#9481) closes #9032 --------- Co-authored-by: Antoine Moreaux --- .../twenty-front/src/generated/graphql.tsx | 4 +- .../modules/auth/components/VerifyEffect.tsx | 27 +++---- .../auth/hooks/__tests__/useAuth.test.tsx | 5 +- .../src/modules/auth/hooks/useAuth.ts | 37 ++-------- .../components/ClientConfigProviderEffect.tsx | 7 ++ .../graphql/queries/getClientConfig.ts | 1 + .../states/canManageFeatureFlagsState.ts | 6 ++ .../components/SettingsAdminContent.tsx | 74 +++++++++++-------- .../admin-panel/hooks/useImpersonate.ts | 20 ++++- .../src/testing/mock-data/config.ts | 1 + .../admin-panel/admin-panel.resolver.ts | 6 +- .../admin-panel/admin-panel.service.ts | 6 +- .../client-config/client-config.entity.ts | 3 + .../client-config/client-config.resolver.ts | 3 + 14 files changed, 112 insertions(+), 88 deletions(-) create mode 100644 packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index ecc79a5e5a35..953c6b9e6b3f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -171,6 +171,7 @@ export type ClientConfig = { api: ApiConfig; authProviders: AuthProviders; billing: Billing; + canManageFeatureFlags: Scalars['Boolean']; captcha: Captcha; chromeExtensionId?: Maybe; debugMode: Scalars['Boolean']; @@ -2081,7 +2082,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; @@ -3514,6 +3515,7 @@ export const GetClientConfigDocument = gql` mutationMaximumAffectedRecords } chromeExtensionId + canManageFeatureFlags } } `; diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx index 6530532fae8f..a6230e9ff206 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx @@ -27,22 +27,17 @@ export const VerifyEffect = () => { ); useEffect(() => { - const getTokens = async () => { - if (isDefined(errorMessage)) { - enqueueSnackBar(errorMessage, { - variant: SnackBarVariant.Error, - }); - } - if (!loginToken) { - navigate(AppPath.SignInUp); - } else { - setIsAppWaitingForFreshObjectMetadata(true); - await verify(loginToken); - } - }; - - if (!isLogged) { - getTokens(); + if (isDefined(errorMessage)) { + enqueueSnackBar(errorMessage, { + variant: SnackBarVariant.Error, + }); + } + + if (isDefined(loginToken)) { + setIsAppWaitingForFreshObjectMetadata(true); + verify(loginToken); + } else if (!isLogged) { + navigate(AppPath.SignInUp); } // Verify only needs to run once at mount // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx index 6430246e359f..99bea99254f8 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx @@ -141,10 +141,7 @@ describe('useAuth', () => { const { result } = renderHooks(); await act(async () => { - const res = await result.current.signUpWithCredentials(email, password); - expect(res).toHaveProperty('user'); - expect(res).toHaveProperty('workspaceMember'); - expect(res).toHaveProperty('workspace'); + await result.current.signUpWithCredentials(email, password); }); expect(mocks[2].result).toHaveBeenCalled(); diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index d28d49d59e06..d583e815d8eb 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -268,6 +268,8 @@ export const useAuth = () => { const handleVerify = useCallback( async (loginToken: string) => { + setIsVerifyPendingState(true); + const verifyResult = await verify({ variables: { loginToken }, }); @@ -282,16 +284,11 @@ export const useAuth = () => { setTokenPair(verifyResult.data?.verify.tokens); - const { user, workspaceMember, workspace } = await loadCurrentUser(); + await loadCurrentUser(); - return { - user, - workspaceMember, - workspace, - tokens: verifyResult.data?.verify.tokens, - }; + setIsVerifyPendingState(false); }, - [verify, setTokenPair, loadCurrentUser], + [setIsVerifyPendingState, verify, setTokenPair, loadCurrentUser], ); const handleCrendentialsSignIn = useCallback( @@ -301,21 +298,9 @@ export const useAuth = () => { password, captchaToken, ); - setIsVerifyPendingState(true); - - const { user, workspaceMember, workspace } = await handleVerify( - loginToken.token, - ); - - setIsVerifyPendingState(false); - - return { - user, - workspaceMember, - workspace, - }; + await handleVerify(loginToken.token); }, - [handleChallenge, handleVerify, setIsVerifyPendingState], + [handleChallenge, handleVerify], ); const handleSignOut = useCallback(async () => { @@ -360,13 +345,7 @@ export const useAuth = () => { ); } - const { user, workspace, workspaceMember } = await handleVerify( - signUpResult.data?.signUp.loginToken.token, - ); - - setIsVerifyPendingState(false); - - return { user, workspaceMember, workspace }; + await handleVerify(signUpResult.data?.signUp.loginToken.token); }, [ setIsVerifyPendingState, diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index ebbfa965ead7..224812287dd3 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -1,6 +1,7 @@ import { apiConfigState } from '@/client-config/states/apiConfigState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; +import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; @@ -45,6 +46,10 @@ export const ClientConfigProviderEffect = () => { const setApiConfig = useSetRecoilState(apiConfigState); + const setCanManageFeatureFlags = useSetRecoilState( + canManageFeatureFlagsState, + ); + const { data, loading, error } = useGetClientConfigQuery({ skip: clientConfigApiStatus.isLoaded, }); @@ -107,6 +112,7 @@ export const ClientConfigProviderEffect = () => { defaultSubdomain: data?.clientConfig?.defaultSubdomain, frontDomain: data?.clientConfig?.frontDomain, }); + setCanManageFeatureFlags(data?.clientConfig?.canManageFeatureFlags); }, [ data, setIsDebugMode, @@ -125,6 +131,7 @@ export const ClientConfigProviderEffect = () => { setDomainConfiguration, setIsSSOEnabledState, setAuthProviders, + setCanManageFeatureFlags, ]); return <>; diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index 88a696368988..57aeb22389c4 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -44,6 +44,7 @@ export const GET_CLIENT_CONFIG = gql` mutationMaximumAffectedRecords } chromeExtensionId + canManageFeatureFlags } } `; diff --git a/packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts b/packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts new file mode 100644 index 000000000000..1d0222a6ea9d --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const canManageFeatureFlagsState = createState({ + key: 'canManageFeatureFlagsState', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx index 7dcdb1cf18b8..484932e73bd4 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx @@ -1,5 +1,7 @@ +import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs'; import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement'; +import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; import { TextInput } from '@/ui/input/components/TextInput'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -11,6 +13,7 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { getImageAbsoluteURI } from 'twenty-shared'; import { Button, @@ -24,7 +27,6 @@ import { Toggle, } from 'twenty-ui'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; const StyledLinkContainer = styled.div` margin-right: ${({ theme }) => theme.spacing(2)}; @@ -47,7 +49,7 @@ const StyledUserInfo = styled.div` `; const StyledTable = styled(Table)` - margin-top: ${({ theme }) => theme.spacing(0.5)}; + margin-top: ${({ theme }) => theme.spacing(3)}; `; const StyledTabListContainer = styled.div` @@ -87,6 +89,8 @@ export const SettingsAdminContent = () => { error, } = useFeatureFlagsManagement(); + const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState); + const handleSearch = async () => { setActiveTabId(''); @@ -151,37 +155,39 @@ export const SettingsAdminContent = () => { /> )} - - - Feature Flag - Status - - - {activeWorkspace.featureFlags.map((flag) => ( + {canManageFeatureFlags && ( + - {flag.key} - - - handleFeatureFlagUpdate( - activeWorkspace.id, - flag.key, - newValue, - ) - } - /> - + Feature Flag + Status - ))} - + + {activeWorkspace.featureFlags.map((flag) => ( + + {flag.key} + + + handleFeatureFlagUpdate( + activeWorkspace.id, + flag.key, + newValue, + ) + } + /> + + + ))} + + )} ); }; @@ -190,8 +196,16 @@ export const SettingsAdminContent = () => { <>
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts index 8c08f657d9b3..326bcfeeccd9 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts @@ -1,15 +1,24 @@ import { currentUserState } from '@/auth/states/currentUserState'; import { AppPath } from '@/types/AppPath'; import { useState } from 'react'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useImpersonateMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; +import { useAuth } from '@/auth/hooks/useAuth'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; export const useImpersonate = () => { const [currentUser] = useRecoilState(currentUserState); - const [impersonate] = useImpersonateMutation(); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState( + isAppWaitingForFreshObjectMetadataState, + ); + + const { verify } = useAuth(); + const [impersonate] = useImpersonateMutation(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const [isLoading, setIsLoading] = useState(false); @@ -39,6 +48,13 @@ export const useImpersonate = () => { const { loginToken, workspace } = impersonateResult.data.impersonate; + if (workspace.id === currentWorkspace?.id) { + setIsAppWaitingForFreshObjectMetadata(true); + await verify(loginToken.token); + setIsAppWaitingForFreshObjectMetadata(false); + return; + } + return redirectToWorkspaceDomain(workspace.subdomain, AppPath.Verify, { loginToken: loginToken.token, }); diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index a80303d9a5e6..97a62869a11c 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -40,4 +40,5 @@ export const mockedClientConfig: ClientConfig = { __typename: 'Captcha', }, api: { mutationMaximumAffectedRecords: 100 }, + canManageFeatureFlags: true, }; diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts index 16ac678aaccd..6e90eef747c9 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -3,15 +3,15 @@ import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.input'; +import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output'; import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input'; import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity'; import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input'; import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard'; -import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @Resolver() @UseFilters(AuthGraphqlApiExceptionFilter) diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index 0d34a6462479..e45a5afda826 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -8,14 +8,14 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; +import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; -import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate'; @Injectable() export class AdminPanelService { diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index c94b2418e050..6e3842f5c820 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -94,4 +94,7 @@ export class ClientConfig { @Field(() => ApiConfig) api: ApiConfig; + + @Field(() => Boolean) + canManageFeatureFlags: boolean; } 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 cbd1fb7d0181..bc64032070aa 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 @@ -59,6 +59,9 @@ export class ClientConfigResolver { ), }, analyticsEnabled: this.environmentService.get('ANALYTICS_ENABLED'), + canManageFeatureFlags: + this.environmentService.get('DEBUG_MODE') || + this.environmentService.get('IS_BILLING_ENABLED'), }; return Promise.resolve(clientConfig); From 7b2debf6fbfb730884cba53215d7768c4d7a2cfd Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 10 Jan 2025 14:29:58 +0100 Subject: [PATCH 15/22] Add error marker when invalid main function (#9489) ## Before ![image](https://github.com/user-attachments/assets/f6af6721-0896-48b5-8556-9d2a9c19de06) ## After ![image](https://github.com/user-attachments/assets/c59407c8-8244-4906-9d05-713909a19c33) --- ...rkflowEditActionFormServerlessFunction.tsx | 4 +- .../getWrongExportedFunctionMarkers.test.ts | 30 ++++++++++ .../utils/getWrongExportedFunctionMarkers.ts | 60 +++++++++++++++++++ .../code-editor/components/CodeEditor.tsx | 33 +++++++++- 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/getWrongExportedFunctionMarkers.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx index fb76b0fc610b..41aee71975b7 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx @@ -8,6 +8,7 @@ import { WorkflowCodeAction } from '@/workflow/types/Workflow'; import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/utils/setNestedValue'; +import { Monaco } from '@monaco-editor/react'; import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton'; import { ServerlessFunctionExecutionResult } from '@/serverless-functions/components/ServerlessFunctionExecutionResult'; import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath'; @@ -26,13 +27,13 @@ import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/w import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { Monaco } from '@monaco-editor/react'; import { editor } from 'monaco-editor'; import { AutoTypings } from 'monaco-editor-auto-typings'; import { useEffect, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { CodeEditor, IconCode, IconPlayerPlay, isDefined } from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; +import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers'; const StyledContainer = styled.div` display: flex; @@ -299,6 +300,7 @@ export const WorkflowEditActionFormServerlessFunction = ({ language={'typescript'} onChange={handleCodeChange} onMount={handleEditorDidMount} + setMarkers={getWrongExportedFunctionMarkers} options={{ readOnly: actionOptions.readonly, domReadOnly: actionOptions.readonly, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/getWrongExportedFunctionMarkers.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/getWrongExportedFunctionMarkers.test.ts new file mode 100644 index 000000000000..a93a4b725438 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/getWrongExportedFunctionMarkers.test.ts @@ -0,0 +1,30 @@ +import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers'; + +describe('getWrongExportedFunctionMarkers', () => { + it('should return marker when no exported function', () => { + const value = 'const main = async () => {}'; + const result = getWrongExportedFunctionMarkers(value); + expect(result.length).toEqual(1); + expect(result[0].message).toEqual( + 'An exported "main" arrow function is required.', + ); + }); + + it('should return marker when no wrong exported function', () => { + const value = 'export const wrongMain = async () => {}'; + const result = getWrongExportedFunctionMarkers(value); + expect(result.length).toEqual(1); + }); + + it('should return no marker when valid exported function', () => { + const value = 'export const main = async () => {}'; + const result = getWrongExportedFunctionMarkers(value); + expect(result.length).toEqual(0); + }); + + it('should return handle multiple spaces', () => { + const value = 'export const main = async () => {}'; + const result = getWrongExportedFunctionMarkers(value); + expect(result.length).toEqual(0); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers.ts new file mode 100644 index 000000000000..525914104fe2 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers.ts @@ -0,0 +1,60 @@ +import { isDefined } from 'twenty-ui'; + +const getSubstringCoordinate = ( + text: string, + substring: string, +): { line: number; column: number } | null => { + const lines = text.split('\n'); + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const columnIndex = lines[lineIndex].indexOf(substring); + if (columnIndex !== -1) { + return { + line: lineIndex + 1, // 1-based line number + column: columnIndex + 1, // 1-based column number + }; + } + } + + return null; +}; + +export const getWrongExportedFunctionMarkers = (value: string) => { + const validRegex = /export\s+const\s+main\s*=/g; + const invalidRegex = /export\s+const\s+\S*/g; + const exportRegex = /export\s+const/g; + const validMatch = value.match(validRegex); + const invalidMatch = value.match(invalidRegex); + const exportMatch = value.match(exportRegex); + const markers = []; + + if (!validMatch && !!invalidMatch) { + const coordinates = getSubstringCoordinate(value, invalidMatch[0]); + if (isDefined(coordinates)) { + const endColumn = invalidMatch[0].length + coordinates.column; + markers.push({ + severity: 8, //MarkerSeverity.Error, + message: 'Exported arrow function should be named "main"', + code: 'export const main', + startLineNumber: coordinates.line, + startColumn: coordinates.column, + endLineNumber: 1, + endColumn, + }); + } + } + + if (!exportMatch) { + markers.push({ + severity: 8, //MarkerSeverity.Error, + message: 'An exported "main" arrow function is required.', + code: 'export const main', + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + }); + } + + return markers; +}; diff --git a/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx b/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx index ab1b8c99c858..a671a4434a29 100644 --- a/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx +++ b/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx @@ -1,11 +1,14 @@ import { useTheme, css } from '@emotion/react'; -import Editor, { EditorProps } from '@monaco-editor/react'; +import Editor, { EditorProps, Monaco } from '@monaco-editor/react'; import { codeEditorTheme } from '@ui/input'; import { isDefined } from '@ui/utilities'; import styled from '@emotion/styled'; +import { useState } from 'react'; +import { editor } from 'monaco-editor'; type CodeEditorProps = Omit & { onChange?: (value: string) => void; + setMarkers?: (value: string) => editor.IMarkerData[]; withHeader?: boolean; }; @@ -35,12 +38,31 @@ export const CodeEditor = ({ language, onMount, onChange, + setMarkers, onValidate, height = 450, withHeader = false, options, }: CodeEditorProps) => { const theme = useTheme(); + const [monaco, setMonaco] = useState(undefined); + const [editor, setEditor] = useState< + editor.IStandaloneCodeEditor | undefined + >(undefined); + + const setModelMarkers = ( + editor: editor.IStandaloneCodeEditor | undefined, + monaco: Monaco | undefined, + ) => { + const model = editor?.getModel(); + if (!isDefined(model)) { + return; + } + const customMarkers = setMarkers?.(model.getValue()); + if (isDefined(customMarkers)) { + monaco?.editor.setModelMarkers(model, 'customMarker', customMarkers); + } + }; return ( { + setMonaco(monaco); + setEditor(editor); monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme)); monaco.editor.setTheme('codeEditorTheme'); - onMount?.(editor, monaco); + setModelMarkers(editor, monaco); }} onChange={(value) => { if (isDefined(value)) { onChange?.(value); + setModelMarkers(editor, monaco); } }} - onValidate={onValidate} + onValidate={(markers) => { + onValidate?.(markers); + }} options={{ overviewRulerLanes: 0, scrollbar: { From e58edc3bc1bf226eeb6b0ba0bb3ed7e49c91f736 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 10 Jan 2025 14:45:17 +0100 Subject: [PATCH 16/22] Removed useFilterDropdown hook (#9537) Removed useFilterDropdown hook and its many calls which were only exporting states. The test has been removed because it was used to do the equivalent of testing Recoil states, so it wasn't useful anymore. --- .../AdvancedFilterViewFilterFieldSelect.tsx | 13 +- .../AdvancedFilterViewFilterValueInput.tsx | 21 +- .../MultipleFiltersDropdownContent.tsx | 14 +- .../ObjectFilterDropdownBooleanSelect.tsx | 30 +- .../ObjectFilterDropdownDateInput.tsx | 31 +- .../ObjectFilterDropdownFilterInput.tsx | 22 +- .../ObjectFilterDropdownFilterSelect.tsx | 25 +- ...pdownFilterSelectCompositeFieldSubMenu.tsx | 38 +- ...jectFilterDropdownFilterSelectMenuItem.tsx | 18 +- .../ObjectFilterDropdownNumberInput.tsx | 31 +- .../ObjectFilterDropdownOperandDropdown.tsx | 10 +- .../ObjectFilterDropdownOperandSelect.tsx | 29 +- .../ObjectFilterDropdownOptionSelect.tsx | 47 +-- .../ObjectFilterDropdownRatingInput.tsx | 25 +- .../ObjectFilterDropdownRecordSelect.tsx | 48 +-- .../ObjectFilterDropdownSearchInput.tsx | 33 +- .../ObjectFilterDropdownSourceSelect.tsx | 54 +-- .../ObjectFilterDropdownTextSearchInput.tsx | 39 +- ...SingleEntityObjectFilterDropdownButton.tsx | 23 +- .../__tests__/useFilterDropdown.test.tsx | 346 ------------------ .../hooks/useFilterDropdown.ts | 76 ---- ...useSelectFilterDefinitionUsedInDropdown.ts | 38 +- .../EditableFilterDropdownButton.tsx | 26 +- .../views/components/ViewBarFilterEffect.tsx | 32 +- 24 files changed, 360 insertions(+), 709 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterFieldSelect.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterFieldSelect.tsx index ad679c293517..290aa037d4b1 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterFieldSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterFieldSelect.tsx @@ -2,11 +2,13 @@ import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter'; import { ObjectFilterDropdownFilterSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect'; import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { advancedFilterViewFilterGroupIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState'; +import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { SelectControl } from '@/ui/input/components/SelectControl'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; import styled from '@emotion/styled'; @@ -27,8 +29,13 @@ export const AdvancedFilterViewFilterFieldSelect = ({ const selectedFieldLabel = filter?.definition.label ?? ''; - const { setAdvancedFilterViewFilterGroupId, setAdvancedFilterViewFilterId } = - useFilterDropdown(); + const setAdvancedFilterViewFilterId = useSetRecoilComponentStateV2( + advancedFilterViewFilterIdComponentState, + ); + + const setAdvancedFilterViewFilterGroupId = useSetRecoilComponentStateV2( + advancedFilterViewFilterGroupIdComponentState, + ); const [objectFilterDropdownIsSelectingCompositeField] = useRecoilComponentStateV2( diff --git a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput.tsx b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput.tsx index 7ad454afa55c..e401914186b1 100644 --- a/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput.tsx +++ b/packages/twenty-front/src/modules/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput.tsx @@ -1,9 +1,12 @@ import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter'; import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { SelectControl } from '@/ui/input/components/SelectControl'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId'; type AdvancedFilterViewFilterValueInputProps = { @@ -19,11 +22,17 @@ export const AdvancedFilterViewFilterValueInput = ({ const isDisabled = !filter?.fieldMetadataId || !filter.operand; - const { - setFilterDefinitionUsedInDropdown, - setSelectedOperandInDropdown, - setSelectedFilter, - } = useFilterDropdown(); + const setFilterDefinitionUsedInDropdown = useSetRecoilComponentStateV2( + filterDefinitionUsedInDropdownComponentState, + ); + + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( + selectedOperandInDropdownComponentState, + ); + + const setSelectedFilter = useSetRecoilComponentStateV2( + selectedFilterComponentState, + ); if (isDisabled) { return ( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index 43f9e4faf4f3..08c3d8b06f01 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -1,11 +1,10 @@ -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; - import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu'; import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; -import { useRecoilValue } from 'recoil'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect'; @@ -16,9 +15,10 @@ type MultipleFiltersDropdownContentProps = { export const MultipleFiltersDropdownContent = ({ filterDropdownId, }: MultipleFiltersDropdownContentProps) => { - const { filterDefinitionUsedInDropdownState } = useFilterDropdown({ + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, filterDropdownId, - }); + ); const [objectFilterDropdownIsSelectingCompositeField] = useRecoilComponentStateV2( @@ -31,10 +31,6 @@ export const MultipleFiltersDropdownContent = ({ filterDropdownId, ); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, - ); - const shouldShowCompositeSelectionSubMenu = objectFilterDropdownIsSelectingCompositeField; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx index e8fa38cbedfc..1fb194eb2625 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownBooleanSelect.tsx @@ -1,16 +1,18 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { BooleanDisplay } from '@/ui/field/display/components/BooleanDisplay'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { IconCheck } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; @@ -37,24 +39,22 @@ export const ObjectFilterDropdownBooleanSelect = () => { const theme = useTheme(); const options = [true, false]; - const { - filterDefinitionUsedInDropdownState, - selectedOperandInDropdownState, - selectedFilterState, - } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + ); + + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); const { applyRecordFilter } = useApplyRecordFilter(); const { closeDropdown } = useDropdown(); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, - ); - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, - ); - const selectedFilter = useRecoilValue(selectedFilterState); - const [selectedValue, setSelectedValue] = useState( selectedFilter?.value === 'true', ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx index a7a061bdfb56..3a443812c378 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx @@ -1,11 +1,12 @@ -import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue'; import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { computeVariableDateViewFilterValue } from '@/views/view-filter-value/utils/computeVariableDateViewFilterValue'; import { @@ -18,25 +19,19 @@ import { isDefined } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const ObjectFilterDropdownDateInput = () => { - const { - filterDefinitionUsedInDropdownState, - selectedOperandInDropdownState, - selectedFilterState, - } = useFilterDropdown(); - - const { applyRecordFilter } = useApplyRecordFilter(); + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + ); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, ); - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, ); - const selectedFilter = useRecoilValue(selectedFilterState) as - | (Filter & { definition: { type: 'DATE' | 'DATE_TIME' } }) - | null - | undefined; + const { applyRecordFilter } = useApplyRecordFilter(); const initialFilterValue = selectedFilter ? resolveDateViewFilterValue(selectedFilter) diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx index 7664504bba44..6d792baa8ebc 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput.tsx @@ -1,5 +1,3 @@ -import { useRecoilValue } from 'recoil'; - import { ObjectFilterDropdownDateInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput'; import { ObjectFilterDropdownNumberInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput'; import { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect'; @@ -8,7 +6,6 @@ import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter- import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput'; import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect'; import { ObjectFilterDropdownTextSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; @@ -18,6 +15,9 @@ import { ObjectFilterDropdownBooleanSelect } from '@/object-record/object-filter import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes'; import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes'; import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; type ObjectFilterDropdownFilterInputProps = { filterDropdownId?: string; @@ -26,17 +26,13 @@ type ObjectFilterDropdownFilterInputProps = { export const ObjectFilterDropdownFilterInput = ({ filterDropdownId, }: ObjectFilterDropdownFilterInputProps) => { - const { - filterDefinitionUsedInDropdownState, - selectedOperandInDropdownState, - } = useFilterDropdown({ filterDropdownId }); - - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + filterDropdownId, ); - - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + filterDropdownId, ); const isConfigurable = diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx index 649032fe0b2e..cedf0da9c554 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx @@ -6,8 +6,8 @@ import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks import { AdvancedFilterButton } from '@/object-record/object-filter-dropdown/components/AdvancedFilterButton'; import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; @@ -17,13 +17,14 @@ import { SelectableItem } from '@/ui/layout/selectable-list/components/Selectabl import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; import { FeatureFlagKey } from '~/generated/graphql'; +import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState'; export const StyledInput = styled.input` background: transparent; border: none; @@ -59,22 +60,20 @@ export const ObjectFilterDropdownFilterSelect = ({ }: ObjectFilterDropdownFilterSelectProps) => { const { recordIndexId } = useRecordIndexContextOrThrow(); - const { - setObjectFilterDropdownSearchInput, - objectFilterDropdownSearchInputState, - advancedFilterViewFilterIdState, - } = useFilterDropdown(); + const setObjectFilterDropdownSearchInput = useSetRecoilComponentStateV2( + objectFilterDropdownSearchInputComponentState, + ); - const advancedFilterViewFilterId = useRecoilValue( - advancedFilterViewFilterIdState, + const advancedFilterViewFilterId = useRecoilComponentValueV2( + advancedFilterViewFilterIdComponentState, ); - const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown( - advancedFilterViewFilterId, + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, ); - const objectFilterDropdownSearchInput = useRecoilValue( - objectFilterDropdownSearchInputState, + const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown( + advancedFilterViewFilterId, ); const availableFilterDefinitions = useRecoilComponentValueV2( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx index 9dbd2d110db4..e3c938f56148 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx @@ -1,10 +1,14 @@ import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { advancedFilterViewFilterGroupIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState'; +import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel'; @@ -14,8 +18,9 @@ import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/con import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { IconApps, IconChevronLeft, @@ -52,23 +57,28 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { objectFilterDropdownSubMenuFieldTypeComponentState, ); - const { - setFilterDefinitionUsedInDropdown, - setSelectedOperandInDropdown, - setObjectFilterDropdownSearchInput, - advancedFilterViewFilterIdState, - advancedFilterViewFilterGroupIdState, - } = useFilterDropdown(); + const setFilterDefinitionUsedInDropdown = useSetRecoilComponentStateV2( + filterDefinitionUsedInDropdownComponentState, + ); - const { applyRecordFilter } = useApplyRecordFilter(); + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( + selectedOperandInDropdownComponentState, + ); + + const setObjectFilterDropdownSearchInput = useSetRecoilComponentStateV2( + objectFilterDropdownSearchInputComponentState, + ); - const advancedFilterViewFilterId = useRecoilValue( - advancedFilterViewFilterIdState, + const advancedFilterViewFilterId = useRecoilComponentValueV2( + advancedFilterViewFilterIdComponentState, ); - const advancedFilterViewFilterGroupId = useRecoilValue( - advancedFilterViewFilterGroupIdState, + + const advancedFilterViewFilterGroupId = useRecoilComponentValueV2( + advancedFilterViewFilterGroupIdComponentState, ); + const { applyRecordFilter } = useApplyRecordFilter(); + const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown( advancedFilterViewFilterId, ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx index d933f2f2730e..c65b23011246 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx @@ -1,11 +1,12 @@ import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useSelectFilterDefinitionUsedInDropdown } from '@/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown'; +import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { CompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/types/CompositeFilterableFieldType'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; @@ -15,6 +16,8 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useRecoilValue } from 'recoil'; import { MenuItemSelect, useIcons } from 'twenty-ui'; @@ -56,15 +59,16 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ const isACompositeField = isCompositeField(filterDefinition.type); - const { setSelectedOperandInDropdown, advancedFilterViewFilterIdState } = - useFilterDropdown(); - - const setHotkeyScope = useSetHotkeyScope(); + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( + selectedOperandInDropdownComponentState, + ); - const advancedFilterViewFilterId = useRecoilValue( - advancedFilterViewFilterIdState, + const advancedFilterViewFilterId = useRecoilComponentValueV2( + advancedFilterViewFilterIdComponentState, ); + const setHotkeyScope = useSetHotkeyScope(); + const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown( advancedFilterViewFilterId, ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx index 5222e6156996..6baf9b1c783e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx @@ -1,30 +1,29 @@ import { ChangeEvent, useCallback, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const ObjectFilterDropdownNumberInput = () => { - const { - selectedOperandInDropdownState, - filterDefinitionUsedInDropdownState, - selectedFilterState, - } = useFilterDropdown(); - - const { applyRecordFilter } = useApplyRecordFilter(); - - const [hasFocused, setHasFocused] = useState(false); + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, ); - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, ); - const selectedFilter = useRecoilValue(selectedFilterState); + const { applyRecordFilter } = useApplyRecordFilter(); + + const [hasFocused, setHasFocused] = useState(false); const [inputValue, setInputValue] = useState( () => selectedFilter?.value || '', diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx index 840d87ab91ef..61295cbc2b6a 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandDropdown.tsx @@ -1,12 +1,12 @@ -import { useRecoilValue } from 'recoil'; import { IconChevronDown } from 'twenty-ui'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { ObjectFilterDropdownOperandSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { getOperandLabel } from '../utils/getOperandLabel'; @@ -20,10 +20,8 @@ export const ObjectFilterDropdownOperandDropdown = ({ }: { filterDropdownId?: string; }) => { - const { selectedOperandInDropdownState } = useFilterDropdown(); - - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, ); const theme = useTheme(); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx index ba59dc97ea29..edf1216774b1 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx @@ -1,11 +1,14 @@ -import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import styled from '@emotion/styled'; import { MenuItem } from 'twenty-ui'; @@ -19,21 +22,21 @@ const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` `; export const ObjectFilterDropdownOperandSelect = () => { - const { - filterDefinitionUsedInDropdownState, - setSelectedOperandInDropdown, - selectedFilterState, - } = useFilterDropdown(); - - const { applyRecordFilter } = useApplyRecordFilter(); + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + ); - const { closeDropdown } = useDropdown(); + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( + selectedOperandInDropdownComponentState, + ); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, ); - const selectedFilter = useRecoilValue(selectedFilterState); + const { applyRecordFilter } = useApplyRecordFilter(); + + const { closeDropdown } = useDropdown(); const operandsForFilterType = isDefined(filterDefinitionUsedInDropdown) ? getOperandsForFilterDefinition(filterDefinitionUsedInDropdown) diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx index 0eb8035254ca..76e9ef3fbd25 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx @@ -4,7 +4,6 @@ import { Key } from 'ts-key-enum'; import { v4 } from 'uuid'; import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useOptionsForSelect } from '@/object-record/object-filter-dropdown/hooks/useOptionsForSelect'; import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; @@ -15,7 +14,13 @@ import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/inter import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { MenuItem, MenuItemMultiSelect } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; @@ -27,13 +32,25 @@ type SelectOptionForFilter = FieldMetadataItemOption & { }; export const ObjectFilterDropdownOptionSelect = () => { - const { - filterDefinitionUsedInDropdownState, - objectFilterDropdownSearchInputState, - selectedOperandInDropdownState, - objectFilterDropdownSelectedOptionValuesState, - selectedFilterState, - } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + ); + + const objectFilterDropdownSelectedOptionValues = useRecoilComponentValueV2( + objectFilterDropdownSelectedOptionValuesComponentState, + ); + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); + + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, + ); + + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); const { applyRecordFilter } = useApplyRecordFilter(); @@ -48,20 +65,6 @@ export const ObjectFilterDropdownOptionSelect = () => { ); const selectedItemId = useRecoilValue(selectedItemIdState); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, - ); - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, - ); - const objectFilterDropdownSearchInput = useRecoilValue( - objectFilterDropdownSearchInputState, - ); - const objectFilterDropdownSelectedOptionValues = useRecoilValue( - objectFilterDropdownSelectedOptionValuesState, - ); - - const selectedFilter = useRecoilValue(selectedFilterState); const fieldMetaDataId = filterDefinitionUsedInDropdown?.fieldMetadataId ?? ''; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx index 5982797053cd..5026922e8a2e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput.tsx @@ -1,13 +1,15 @@ -import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues'; import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata'; import { RatingInput } from '@/ui/field/input/components/RatingInput'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; const convertFieldRatingValueToNumber = ( rating: Exclude, ): string => { @@ -31,20 +33,17 @@ export const convertRatingToRatingValue = (rating: number) => { }; export const ObjectFilterDropdownRatingInput = () => { - const { - selectedOperandInDropdownState, - filterDefinitionUsedInDropdownState, - selectedFilterState, - } = useFilterDropdown(); - - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, ); - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, + + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, ); - const selectedFilter = useRecoilValue(selectedFilterState); + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); const { applyRecordFilter } = useApplyRecordFilter(); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index 1c5046a30e9b..2f95c08738ff 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -1,17 +1,21 @@ import { useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectFilterDropdownRecordPinnedItems } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems'; import { CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID } from '@/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { RelationFilterValue } from '@/views/view-filter-value/types/RelationFilterValue'; import { relationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/relationFilterValueSchema'; @@ -32,35 +36,33 @@ type ObjectFilterDropdownRecordSelectProps = { export const ObjectFilterDropdownRecordSelect = ({ viewComponentId, }: ObjectFilterDropdownRecordSelectProps) => { - const { - filterDefinitionUsedInDropdownState, - objectFilterDropdownSearchInputState, - selectedOperandInDropdownState, - selectedFilterState, - objectFilterDropdownSelectedRecordIdsState, - } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + ); + + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); + + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, + ); + + const objectFilterDropdownSelectedRecordIds = useRecoilComponentValueV2( + objectFilterDropdownSelectedRecordIdsComponentState, + ); const { applyRecordFilter } = useApplyRecordFilter(viewComponentId); const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(viewComponentId); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, - ); - const objectFilterDropdownSearchInput = useRecoilValue( - objectFilterDropdownSearchInputState, - ); - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, - ); - const objectFilterDropdownSelectedRecordIds = useRecoilValue( - objectFilterDropdownSelectedRecordIdsState, - ); const [fieldId] = useState(v4()); - const selectedFilter = useRecoilValue(selectedFilterState); - const { isCurrentWorkspaceMemberSelected } = relationFilterValueSchema .catch({ isCurrentWorkspaceMemberSelected: false, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx index 757dca89bd33..6f0c8b19ad2c 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx @@ -1,28 +1,31 @@ import { ChangeEvent, useCallback, useState } from 'react'; -import { useRecoilValue } from 'recoil'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; export const ObjectFilterDropdownSearchInput = () => { - const { - filterDefinitionUsedInDropdownState, - selectedOperandInDropdownState, - objectFilterDropdownSearchInputState, - setObjectFilterDropdownSearchInput, - } = useFilterDropdown(); - const [hasFocused, setHasFocused] = useState(false); + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + ); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, ); - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, + + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, ); - const objectFilterDropdownSearchInput = useRecoilValue( - objectFilterDropdownSearchInputState, + + const setObjectFilterDropdownSearchInput = useSetRecoilComponentStateV2( + objectFilterDropdownSearchInputComponentState, ); + const [hasFocused, setHasFocused] = useState(false); + const handleInputRef = useCallback( (node: HTMLInputElement | null) => { if (Boolean(node) && !hasFocused) { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx index 702706ba8b90..b6144584b3b0 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx @@ -1,14 +1,19 @@ import { useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; import { useEmptyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useEmptyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; @@ -24,14 +29,29 @@ type ObjectFilterDropdownSourceSelectProps = { export const ObjectFilterDropdownSourceSelect = ({ viewComponentId, }: ObjectFilterDropdownSourceSelectProps) => { - const { - filterDefinitionUsedInDropdownState, - objectFilterDropdownSearchInputState, - selectedOperandInDropdownState, - selectedFilterState, - setObjectFilterDropdownSelectedRecordIds, - objectFilterDropdownSelectedRecordIdsState, - } = useFilterDropdown(); + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, + ); + + const setObjectFilterDropdownSelectedRecordIds = useSetRecoilComponentStateV2( + objectFilterDropdownSelectedRecordIdsComponentState, + ); + + const objectFilterDropdownSelectedRecordIds = useRecoilComponentValueV2( + objectFilterDropdownSelectedRecordIdsComponentState, + ); + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); + + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, + ); + + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + ); const { applyRecordFilter } = useApplyRecordFilter(viewComponentId); @@ -41,22 +61,8 @@ export const ObjectFilterDropdownSourceSelect = ({ const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(viewComponentId); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, - ); - const objectFilterDropdownSearchInput = useRecoilValue( - objectFilterDropdownSearchInputState, - ); - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, - ); - const objectFilterDropdownSelectedRecordIds = useRecoilValue( - objectFilterDropdownSelectedRecordIdsState, - ); const [fieldId] = useState(v4()); - const selectedFilter = useRecoilValue(selectedFilterState); - const sourceTypes = getActorSourceMultiSelectOptions( objectFilterDropdownSelectedRecordIds, ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx index 9f7c13af76e6..e9a790271472 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx @@ -1,33 +1,38 @@ import { ChangeEvent, useCallback, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; export const ObjectFilterDropdownTextSearchInput = () => { - const { - filterDefinitionUsedInDropdownState, - selectedOperandInDropdownState, - objectFilterDropdownSearchInputState, - setObjectFilterDropdownSearchInput, - selectedFilterState, - } = useFilterDropdown(); - const [filterId] = useState(v4()); const [hasFocused, setHasFocused] = useState(false); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + ); + + const selectedOperandInDropdown = useRecoilComponentValueV2( + selectedOperandInDropdownComponentState, ); - const selectedOperandInDropdown = useRecoilValue( - selectedOperandInDropdownState, + + const objectFilterDropdownSearchInput = useRecoilComponentValueV2( + objectFilterDropdownSearchInputComponentState, ); - const objectFilterDropdownSearchInput = useRecoilValue( - objectFilterDropdownSearchInputState, + + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); + + const setObjectFilterDropdownSearchInput = useSetRecoilComponentStateV2( + objectFilterDropdownSearchInputComponentState, ); - const selectedFilter = useRecoilValue(selectedFilterState); const { applyRecordFilter } = useApplyRecordFilter(); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx index 571645a5c81e..8719d1bc736e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx @@ -1,17 +1,19 @@ import { useTheme } from '@emotion/react'; import React from 'react'; -import { useRecoilValue } from 'recoil'; import { IconChevronDown } from 'twenty-ui'; import { ObjectFilterDropdownRecordRemoveFilterMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordRemoveFilterMenuItem'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; import { GenericEntityFilterChip } from './GenericEntityFilterChip'; @@ -25,16 +27,21 @@ export const SingleEntityObjectFilterDropdownButton = ({ }: { hotkeyScope: HotkeyScope; }) => { - const { - selectedFilterState, - setFilterDefinitionUsedInDropdown, - setSelectedOperandInDropdown, - } = useFilterDropdown(); + const selectedFilter = useRecoilComponentValueV2( + selectedFilterComponentState, + ); + + const setFilterDefinitionUsedInDropdown = useSetRecoilComponentStateV2( + filterDefinitionUsedInDropdownComponentState, + ); + + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( + selectedOperandInDropdownComponentState, + ); const availableFilterDefinitions = useRecoilComponentValueV2( availableFilterDefinitionsComponentState, ); - const selectedFilter = useRecoilValue(selectedFilterState); const availableFilterDefinition = availableFilterDefinitions[0]; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx deleted file mode 100644 index 0ad4c528edf5..000000000000 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx +++ /dev/null @@ -1,346 +0,0 @@ -import { expect } from '@storybook/test'; -import { act, renderHook, waitFor } from '@testing-library/react'; -import { RecoilRoot, useRecoilState } from 'recoil'; - -import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { useFilterDropdownStates } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownStates'; -import { useResetFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useResetFilterDropdown'; -import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; -import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; -import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; -import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { MockedProvider } from '@apollo/client/testing'; -import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; - -const filterDropdownId = 'filterDropdownId'; -const renderHookConfig = { - wrapper: ({ children }: any) => ( - - - - - {children} - - - - - ), -}; - -const filterDefinitions: FilterDefinition[] = [ - { - fieldMetadataId: 'id', - label: 'definition label', - iconName: 'icon', - type: 'TEXT', - }, -]; - -const mockFilter: Filter = { - id: 'id', - definition: filterDefinitions[0], - displayValue: '', - fieldMetadataId: '', - operand: ViewFilterOperand.Is, - value: '', -}; - -describe('useFilterDropdown', () => { - it('should set availableFilterDefinitions', async () => { - const { result } = renderHook(() => { - useFilterDropdown({ filterDropdownId }); - - const [availableFilterDefinitions, setAvailableFilterDefinitions] = - useRecoilComponentStateV2( - availableFilterDefinitionsComponentState, - filterDropdownId, - ); - - return { availableFilterDefinitions, setAvailableFilterDefinitions }; - }, renderHookConfig); - - expect(result.current.availableFilterDefinitions).toEqual([]); - - act(() => { - result.current.setAvailableFilterDefinitions(filterDefinitions); - }); - - await waitFor(() => { - expect(result.current.availableFilterDefinitions).toEqual( - filterDefinitions, - ); - }); - }); - - it('should set onFilterSelect', async () => { - const { result } = renderHook(() => { - useFilterDropdown({ filterDropdownId }); - const { onFilterSelectState } = useFilterDropdownStates(filterDropdownId); - - const [onFilterSelect, setOnFilterSelect] = - useRecoilState(onFilterSelectState); - return { onFilterSelect, setOnFilterSelect }; - }, renderHookConfig); - - expect(result.current.onFilterSelect).toBeUndefined(); - - act(() => { - result.current.setOnFilterSelect( - (_currVal?: Filter | null) => (_filter: Filter | null) => {}, - ); - }); - await waitFor(() => { - expect(typeof result.current.onFilterSelect).toBe('function'); - }); - }); - - it('should set selectedOperandInDropdown', async () => { - const { result } = renderHook(() => { - useFilterDropdown({ filterDropdownId }); - const { selectedOperandInDropdownState } = - useFilterDropdownStates(filterDropdownId); - - const [selectedOperandInDropdown, setSelectedOperandInDropdown] = - useRecoilState(selectedOperandInDropdownState); - return { selectedOperandInDropdown, setSelectedOperandInDropdown }; - }, renderHookConfig); - - const mockOperand = ViewFilterOperand.Contains; - - expect(result.current.selectedOperandInDropdown).toBeNull(); - - act(() => { - result.current.setSelectedOperandInDropdown(mockOperand); - }); - - expect(result.current.selectedOperandInDropdown).toBe(mockOperand); - }); - - it('should set selectedFilter', async () => { - const { result } = renderHook(() => { - useFilterDropdown({ filterDropdownId }); - const { selectedFilterState } = useFilterDropdownStates(filterDropdownId); - - const [selectedFilter, setSelectedFilter] = - useRecoilState(selectedFilterState); - return { selectedFilter, setSelectedFilter }; - }, renderHookConfig); - - expect(result.current.selectedFilter).toBeUndefined(); - - act(() => { - result.current.setSelectedFilter(mockFilter); - }); - - await waitFor(() => { - expect(result.current.selectedFilter).toBe(mockFilter); - }); - }); - - it('should set filterDefinitionUsedInDropdown', async () => { - const { result } = renderHook(() => { - useFilterDropdown({ filterDropdownId }); - const { filterDefinitionUsedInDropdownState } = - useFilterDropdownStates(filterDropdownId); - - const [ - filterDefinitionUsedInDropdown, - setFilterDefinitionUsedInDropdown, - ] = useRecoilState(filterDefinitionUsedInDropdownState); - return { - filterDefinitionUsedInDropdown, - setFilterDefinitionUsedInDropdown, - }; - }, renderHookConfig); - - expect(result.current.filterDefinitionUsedInDropdown).toBeNull(); - - act(() => { - result.current.setFilterDefinitionUsedInDropdown(filterDefinitions[0]); - }); - - await waitFor(() => { - expect(result.current.filterDefinitionUsedInDropdown).toBe( - filterDefinitions[0], - ); - }); - }); - - it('should set objectFilterDropdownSearchInput', async () => { - const mockResult = 'value'; - const { result } = renderHook(() => { - useFilterDropdown({ filterDropdownId }); - const { objectFilterDropdownSearchInputState } = - useFilterDropdownStates(filterDropdownId); - - const [ - objectFilterDropdownSearchInput, - setObjectFilterDropdownSearchInput, - ] = useRecoilState(objectFilterDropdownSearchInputState); - return { - objectFilterDropdownSearchInput, - setObjectFilterDropdownSearchInput, - }; - }, renderHookConfig); - - expect(result.current.objectFilterDropdownSearchInput).toBe(''); - - act(() => { - result.current.setObjectFilterDropdownSearchInput(mockResult); - }); - - await waitFor(() => { - expect(result.current.objectFilterDropdownSearchInput).toBe(mockResult); - }); - }); - - it('should set objectFilterDropdownSelectedRecordId', async () => { - const mockResult = ['value']; - const { result } = renderHook(() => { - useFilterDropdown({ filterDropdownId }); - const { objectFilterDropdownSelectedRecordIdsState } = - useFilterDropdownStates(filterDropdownId); - - const [ - objectFilterDropdownSelectedRecordIds, - setObjectFilterDropdownSelectedRecordIds, - ] = useRecoilState(objectFilterDropdownSelectedRecordIdsState); - return { - objectFilterDropdownSelectedRecordIds, - setObjectFilterDropdownSelectedRecordIds, - }; - }, renderHookConfig); - - expect( - JSON.stringify(result.current.objectFilterDropdownSelectedRecordIds), - ).toBe(JSON.stringify([])); - - act(() => { - result.current.setObjectFilterDropdownSelectedRecordIds(mockResult); - }); - - await waitFor(() => { - expect(result.current.objectFilterDropdownSelectedRecordIds).toBe( - mockResult, - ); - }); - }); - - it('should set objectFilterDropdownSelectedRecordIds', async () => { - const mockResult = ['id-0', 'id-1', 'id-2']; - const { result } = renderHook(() => { - useFilterDropdown({ filterDropdownId }); - const { objectFilterDropdownSelectedRecordIdsState } = - useFilterDropdownStates(filterDropdownId); - - const [ - objectFilterDropdownSelectedRecordIds, - setObjectFilterDropdownSelectedRecordIds, - ] = useRecoilState(objectFilterDropdownSelectedRecordIdsState); - return { - objectFilterDropdownSelectedRecordIds, - setObjectFilterDropdownSelectedRecordIds, - }; - }, renderHookConfig); - - expect(result.current.objectFilterDropdownSelectedRecordIds).toHaveLength( - 0, - ); - - act(() => { - result.current.setObjectFilterDropdownSelectedRecordIds(mockResult); - }); - - await waitFor(() => { - expect( - result.current.objectFilterDropdownSelectedRecordIds, - ).toStrictEqual(mockResult); - }); - }); - - it('should reset filter', async () => { - const { result } = renderHook(() => { - const { resetFilterDropdown } = useResetFilterDropdown(filterDropdownId); - - const { applyRecordFilter } = useApplyRecordFilter(filterDropdownId); - - const { selectedFilterState } = useFilterDropdownStates(filterDropdownId); - - const [selectedFilter, setSelectedFilter] = - useRecoilState(selectedFilterState); - - return { - selectedFilter, - setSelectedFilter, - applyRecordFilter, - resetFilterDropdown, - }; - }, renderHookConfig); - - act(() => { - result.current.applyRecordFilter(mockFilter); - }); - - await waitFor(() => { - expect(result.current.selectedFilter).toStrictEqual(mockFilter); - }); - - act(() => { - result.current.resetFilterDropdown(); - }); - - await waitFor(() => { - expect(result.current.selectedFilter).toBeUndefined(); - }); - }); - - it('should call onFilterSelect when a filter option is set', async () => { - const { result } = renderHook(() => { - const { applyRecordFilter } = useApplyRecordFilter(filterDropdownId); - - const { onFilterSelectState } = useFilterDropdownStates(filterDropdownId); - - const [onFilterSelect, setOnFilterSelect] = - useRecoilState(onFilterSelectState); - return { onFilterSelect, setOnFilterSelect, applyRecordFilter }; - }, renderHookConfig); - const onFilterSelectMock = jest.fn(); - - expect(result.current.onFilterSelect).toBeUndefined(); - - act(() => { - result.current.setOnFilterSelect(onFilterSelectMock); - result.current.applyRecordFilter(mockFilter); - }); - - await waitFor(() => { - expect(onFilterSelectMock).toBeDefined(); - expect(onFilterSelectMock).toHaveBeenCalled(); - }); - }); - - it('should handle componentInstanceId undefined on initial values', () => { - global.console.error = jest.fn(); - - const renderFunction = () => { - renderHook(() => useFilterDropdown(), renderHookConfig); - }; - - expect(renderFunction).toThrow(Error); - expect(renderFunction).toThrow( - 'Instance id is not provided and cannot be found in context.', - ); - }); - - it('should componentInstanceId have been defined on initial values', () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); - - expect(result.current.componentInstanceId).toBeDefined(); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts deleted file mode 100644 index e75b2477d899..000000000000 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useSetRecoilState } from 'recoil'; - -import { useFilterDropdownStates } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownStates'; - -import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; -import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; - -type UseFilterDropdownProps = { - filterDropdownId?: string; -}; - -export const useFilterDropdown = (props?: UseFilterDropdownProps) => { - const componentInstanceId = useAvailableComponentInstanceIdOrThrow( - ObjectFilterDropdownComponentInstanceContext, - props?.filterDropdownId, - ); - - const { - filterDefinitionUsedInDropdownState, - objectFilterDropdownSearchInputState, - objectFilterDropdownSelectedRecordIdsState, - objectFilterDropdownSelectedOptionValuesState, - selectedFilterState, - selectedOperandInDropdownState, - onFilterSelectState, - advancedFilterViewFilterGroupIdState, - advancedFilterViewFilterIdState, - } = useFilterDropdownStates(componentInstanceId); - - const setSelectedFilter = useSetRecoilState(selectedFilterState); - const setSelectedOperandInDropdown = useSetRecoilState( - selectedOperandInDropdownState, - ); - const setFilterDefinitionUsedInDropdown = useSetRecoilState( - filterDefinitionUsedInDropdownState, - ); - const setObjectFilterDropdownSearchInput = useSetRecoilState( - objectFilterDropdownSearchInputState, - ); - const setObjectFilterDropdownSelectedRecordIds = useSetRecoilState( - objectFilterDropdownSelectedRecordIdsState, - ); - const setObjectFilterDropdownSelectedOptionValues = useSetRecoilState( - objectFilterDropdownSelectedOptionValuesState, - ); - - const setOnFilterSelect = useSetRecoilState(onFilterSelectState); - const setAdvancedFilterViewFilterGroupId = useSetRecoilState( - advancedFilterViewFilterGroupIdState, - ); - const setAdvancedFilterViewFilterId = useSetRecoilState( - advancedFilterViewFilterIdState, - ); - - return { - componentInstanceId, - setSelectedFilter, - setSelectedOperandInDropdown, - setFilterDefinitionUsedInDropdown, - setObjectFilterDropdownSearchInput, - setObjectFilterDropdownSelectedRecordIds, - setObjectFilterDropdownSelectedOptionValues, - setOnFilterSelect, - setAdvancedFilterViewFilterGroupId, - setAdvancedFilterViewFilterId, - filterDefinitionUsedInDropdownState, - objectFilterDropdownSearchInputState, - objectFilterDropdownSelectedRecordIdsState, - objectFilterDropdownSelectedOptionValuesState, - selectedFilterState, - selectedOperandInDropdownState, - onFilterSelectState, - advancedFilterViewFilterGroupIdState, - advancedFilterViewFilterIdState, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts index d919c7b20364..53288a4bb4f6 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilterDefinitionUsedInDropdown.ts @@ -1,11 +1,16 @@ import { useApplyRecordFilter } from '@/object-record/object-filter-dropdown/hooks/useApplyRecordFilter'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { advancedFilterViewFilterGroupIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterGroupIdComponentState'; +import { advancedFilterViewFilterIdComponentState } from '@/object-record/object-filter-dropdown/states/advancedFilterViewFilterIdComponentState'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { useRecoilValue } from 'recoil'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { isDefined } from 'twenty-ui'; import { v4 } from 'uuid'; @@ -14,19 +19,24 @@ type SelectFilterParams = { }; export const useSelectFilterDefinitionUsedInDropdown = () => { - const { - setFilterDefinitionUsedInDropdown, - setSelectedOperandInDropdown, - setObjectFilterDropdownSearchInput, - advancedFilterViewFilterGroupIdState, - advancedFilterViewFilterIdState, - } = useFilterDropdown(); - - const advancedFilterViewFilterId = useRecoilValue( - advancedFilterViewFilterIdState, + const setFilterDefinitionUsedInDropdown = useSetRecoilComponentStateV2( + filterDefinitionUsedInDropdownComponentState, ); - const advancedFilterViewFilterGroupId = useRecoilValue( - advancedFilterViewFilterGroupIdState, + + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( + selectedOperandInDropdownComponentState, + ); + + const setObjectFilterDropdownSearchInput = useSetRecoilComponentStateV2( + objectFilterDropdownSearchInputComponentState, + ); + + const advancedFilterViewFilterGroupId = useRecoilComponentValueV2( + advancedFilterViewFilterGroupIdComponentState, + ); + + const advancedFilterViewFilterId = useRecoilComponentValueV2( + advancedFilterViewFilterIdComponentState, ); const setHotkeyScope = useSetHotkeyScope(); diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index 7424ce73b4dc..59a5a59cbf92 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect } from 'react'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { FilterOperand } from '@/object-record/object-filter-dropdown/types/FilterOperand'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -11,6 +10,10 @@ import { EditableFilterChip } from '@/views/components/EditableFilterChip'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { isDefined } from '~/utils/isDefined'; @@ -26,13 +29,20 @@ export const EditableFilterDropdownButton = ({ viewFilter, hotkeyScope, }: EditableFilterDropdownButtonProps) => { - const { - setFilterDefinitionUsedInDropdown, - setSelectedOperandInDropdown, - setSelectedFilter, - } = useFilterDropdown({ - filterDropdownId: viewFilterDropdownId, - }); + const setFilterDefinitionUsedInDropdown = useSetRecoilComponentStateV2( + filterDefinitionUsedInDropdownComponentState, + viewFilterDropdownId, + ); + + const setSelectedOperandInDropdown = useSetRecoilComponentStateV2( + selectedOperandInDropdownComponentState, + viewFilterDropdownId, + ); + + const setSelectedFilter = useSetRecoilComponentStateV2( + selectedFilterComponentState, + viewFilterDropdownId, + ); // TODO: verify this instance id works const availableFilterDefinitions = useRecoilComponentValueV2( diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx index 7d8817adf247..6de55ad6503f 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx @@ -1,12 +1,14 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; -import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState'; +import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState'; +import { onFilterSelectComponentState } from '@/object-record/object-filter-dropdown/states/onFilterSelectComponentState'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; @@ -28,17 +30,27 @@ export const ViewBarFilterEffect = ({ availableFilterDefinitionsComponentState, ); - const { - setOnFilterSelect, - filterDefinitionUsedInDropdownState, - setObjectFilterDropdownSelectedRecordIds, - setObjectFilterDropdownSelectedOptionValues, - } = useFilterDropdown({ filterDropdownId }); + const setOnFilterSelect = useSetRecoilComponentStateV2( + onFilterSelectComponentState, + filterDropdownId, + ); - const filterDefinitionUsedInDropdown = useRecoilValue( - filterDefinitionUsedInDropdownState, + const filterDefinitionUsedInDropdown = useRecoilComponentValueV2( + filterDefinitionUsedInDropdownComponentState, + filterDropdownId, ); + const setObjectFilterDropdownSelectedRecordIds = useSetRecoilComponentStateV2( + objectFilterDropdownSelectedRecordIdsComponentState, + filterDropdownId, + ); + + const setObjectFilterDropdownSelectedOptionValues = + useSetRecoilComponentStateV2( + objectFilterDropdownSelectedOptionValuesComponentState, + filterDropdownId, + ); + // TODO: verify this instance id works const setAvailableFilterDefinitions = useSetRecoilComponentStateV2( availableFilterDefinitionsComponentState, From 5648c3b31c89c2480245a6feb6df58bc53b8f3f4 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Fri, 10 Jan 2025 14:45:35 +0100 Subject: [PATCH 17/22] [refactor]: Remove isSSOEnabled logic throughout the codebase (#9462) Eliminated all references to `isSSOEnabled` across the frontend, backend, and configuration files. This change simplifies the codebase by removing unnecessary feature flag checks, associated logic, and environment variables. The SSO feature remains available without reliance on this flag. --- .../twenty-front/src/generated/graphql.tsx | 5 +-- .../src/modules/app/components/AppRouter.tsx | 2 - .../modules/app/components/SettingsRoutes.tsx | 12 ++---- .../modules/app/hooks/useCreateAppRouter.tsx | 2 - .../components/ClientConfigProviderEffect.tsx | 4 -- .../graphql/queries/getClientConfig.ts | 1 - .../client-config/states/isSSOEnabledState.ts | 6 --- .../settings/security/SettingsSecurity.tsx | 40 +++++++------------ .../src/testing/mock-data/config.ts | 1 - packages/twenty-server/.env.example | 1 - .../typeorm-seeds/core/feature-flags.ts | 5 --- .../client-config/client-config.entity.ts | 3 -- .../client-config/client-config.resolver.ts | 1 - .../environment/environment-variables.ts | 16 -------- .../enums/feature-flag-key.enum.ts | 1 - .../core-modules/sso/services/sso.service.ts | 18 +-------- packages/twenty-server/src/main.ts | 4 +- .../content/developers/self-hosting/setup.mdx | 1 - 18 files changed, 22 insertions(+), 101 deletions(-) delete mode 100644 packages/twenty-front/src/modules/client-config/states/isSSOEnabledState.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 953c6b9e6b3f..a878cba313c5 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -178,7 +178,6 @@ export type ClientConfig = { defaultSubdomain?: Maybe; frontDomain: Scalars['String']; isMultiWorkspaceEnabled: Scalars['Boolean']; - isSSOEnabled: Scalars['Boolean']; sentry: Sentry; signInPrefilled: Scalars['Boolean']; support: Support; @@ -335,7 +334,6 @@ export enum FeatureFlagKey { IsJsonFilterEnabled = 'IsJsonFilterEnabled', IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', - IsSsoEnabled = 'IsSSOEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', IsViewGroupsEnabled = 'IsViewGroupsEnabled', @@ -2082,7 +2080,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; @@ -3493,7 +3491,6 @@ export const GetClientConfigDocument = gql` } signInPrefilled isMultiWorkspaceEnabled - isSSOEnabled defaultSubdomain frontDomain debugMode diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx index 36081f6195f4..7ded521bbc32 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouter.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx @@ -14,7 +14,6 @@ export const AppRouter = () => { const isCRMMigrationEnabled = useIsFeatureEnabled( FeatureFlagKey.IsCrmMigrationEnabled, ); - const isSSOEnabled = useIsFeatureEnabled(FeatureFlagKey.IsSsoEnabled); const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled( FeatureFlagKey.IsFunctionSettingsEnabled, ); @@ -32,7 +31,6 @@ export const AppRouter = () => { isBillingPageEnabled, isCRMMigrationEnabled, isServerlessFunctionSettingsEnabled, - isSSOEnabled, isAdminPageEnabled, )} /> diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 6077e7b457b6..0caaa75c5535 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -266,7 +266,6 @@ type SettingsRoutesProps = { isBillingEnabled?: boolean; isCRMMigrationEnabled?: boolean; isServerlessFunctionSettingsEnabled?: boolean; - isSSOEnabled?: boolean; isAdminPageEnabled?: boolean; }; @@ -274,7 +273,6 @@ export const SettingsRoutes = ({ isBillingEnabled, isCRMMigrationEnabled, isServerlessFunctionSettingsEnabled, - isSSOEnabled, isAdminPageEnabled, }: SettingsRoutesProps) => ( }> @@ -391,12 +389,10 @@ export const SettingsRoutes = ({ /> } /> } /> - {isSSOEnabled && ( - } - /> - )} + } + /> {isAdminPageEnabled && ( <> } /> diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx index 80afc3c8af56..e1a65bc85cb9 100644 --- a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx +++ b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx @@ -28,7 +28,6 @@ export const useCreateAppRouter = ( isBillingEnabled?: boolean, isCRMMigrationEnabled?: boolean, isServerlessFunctionSettingsEnabled?: boolean, - isSSOEnabled?: boolean, isAdminPageEnabled?: boolean, ) => createBrowserRouter( @@ -65,7 +64,6 @@ export const useCreateAppRouter = ( isServerlessFunctionSettingsEnabled={ isServerlessFunctionSettingsEnabled } - isSSOEnabled={isSSOEnabled} isAdminPageEnabled={isAdminPageEnabled} /> } diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index 224812287dd3..565afcba7735 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -9,7 +9,6 @@ import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabl import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState'; import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; -import { isSSOEnabledState } from '@/client-config/states/isSSOEnabledState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { supportChatState } from '@/client-config/states/supportChatState'; import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; @@ -30,7 +29,6 @@ export const ClientConfigProviderEffect = () => { const setIsMultiWorkspaceEnabled = useSetRecoilState( isMultiWorkspaceEnabledState, ); - const setIsSSOEnabledState = useSetRecoilState(isSSOEnabledState); const setBilling = useSetRecoilState(billingState); const setSupportChat = useSetRecoilState(supportChatState); @@ -107,7 +105,6 @@ export const ClientConfigProviderEffect = () => { setChromeExtensionId(data?.clientConfig?.chromeExtensionId); setApiConfig(data?.clientConfig?.api); - setIsSSOEnabledState(data?.clientConfig?.isSSOEnabled); setDomainConfiguration({ defaultSubdomain: data?.clientConfig?.defaultSubdomain, frontDomain: data?.clientConfig?.frontDomain, @@ -129,7 +126,6 @@ export const ClientConfigProviderEffect = () => { setIsAnalyticsEnabled, error, setDomainConfiguration, - setIsSSOEnabledState, setAuthProviders, setCanManageFeatureFlags, ]); diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index 57aeb22389c4..27509df4d9ff 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -22,7 +22,6 @@ export const GET_CLIENT_CONFIG = gql` } signInPrefilled isMultiWorkspaceEnabled - isSSOEnabled defaultSubdomain frontDomain debugMode diff --git a/packages/twenty-front/src/modules/client-config/states/isSSOEnabledState.ts b/packages/twenty-front/src/modules/client-config/states/isSSOEnabledState.ts deleted file mode 100644 index 7d40b673350c..000000000000 --- a/packages/twenty-front/src/modules/client-config/states/isSSOEnabledState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from '@ui/utilities/state/utils/createState'; - -export const isSSOEnabledState = createState({ - key: 'isSSOEnabledState', - defaultValue: false, -}); diff --git a/packages/twenty-front/src/pages/settings/security/SettingsSecurity.tsx b/packages/twenty-front/src/pages/settings/security/SettingsSecurity.tsx index 6c18725b2c1e..2cdf03818dda 100644 --- a/packages/twenty-front/src/pages/settings/security/SettingsSecurity.tsx +++ b/packages/twenty-front/src/pages/settings/security/SettingsSecurity.tsx @@ -1,7 +1,6 @@ import styled from '@emotion/styled'; import { H2Title, IconLock, Section, Tag } from 'twenty-ui'; -import { isSSOEnabledState } from '@/client-config/states/isSSOEnabledState'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton'; import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListCard'; @@ -9,9 +8,6 @@ import { SettingsSecurityOptionsList } from '@/settings/security/components/Sett import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useRecoilValue } from 'recoil'; -import { FeatureFlagKey } from '~/generated/graphql'; const StyledContainer = styled.div` width: 100%; @@ -29,10 +25,6 @@ const StyledSSOSection = styled(Section)` `; export const SettingsSecurity = () => { - const isSSOEnabled = useRecoilValue(isSSOEnabledState); - const isSSOSectionDisplay = - useIsFeatureEnabled(FeatureFlagKey.IsSsoEnabled) && isSSOEnabled; - return ( { > - {isSSOSectionDisplay && ( - - - } - /> - - - )} + + + } + /> + +
Boolean) isMultiWorkspaceEnabled: boolean; - @Field(() => Boolean) - isSSOEnabled: boolean; - @Field(() => String, { nullable: true }) defaultSubdomain: string; 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 bc64032070aa..e9342a4c62d2 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 @@ -29,7 +29,6 @@ export class ClientConfigResolver { microsoft: this.environmentService.get('AUTH_MICROSOFT_ENABLED'), sso: [], }, - isSSOEnabled: this.environmentService.get('AUTH_SSO_ENABLED'), signInPrefilled: this.environmentService.get('SIGN_IN_PREFILLED'), isMultiWorkspaceEnabled: this.environmentService.get( 'IS_MULTIWORKSPACE_ENABLED', diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index 96b46927df95..1edd0141710c 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -24,7 +24,6 @@ import { LLMTracingDriver } from 'src/engine/core-modules/llm-tracing/interfaces import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum'; import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces'; -import { AssertOrWarn } from 'src/engine/core-modules/environment/decorators/assert-or-warn.decorator'; import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator'; import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator'; import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator'; @@ -232,11 +231,6 @@ export class EnvironmentVariables { @ValidateIf((env) => env.AUTH_GOOGLE_ENABLED) AUTH_GOOGLE_CALLBACK_URL: string; - @CastToBoolean() - @IsOptional() - @IsBoolean() - AUTH_SSO_ENABLED = false; - @IsString() @IsOptional() ENTERPRISE_KEY: string; @@ -459,16 +453,6 @@ export class EnvironmentVariables { @IsString() @IsOptional() - @AssertOrWarn( - (env, value) => - !env.AUTH_SSO_ENABLED || - (env.AUTH_SSO_ENABLED && - value !== 'replace_me_with_a_random_string_session'), - { - message: - 'SESSION_STORE_SECRET should be changed to a secure, random string.', - }, - ) SESSION_STORE_SECRET = 'replace_me_with_a_random_string_session'; @CastToBoolean() diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 2bafc436120d..d583693c67e7 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -7,7 +7,6 @@ export enum FeatureFlagKey { IsFreeAccessEnabled = 'IS_FREE_ACCESS_ENABLED', IsFunctionSettingsEnabled = 'IS_FUNCTION_SETTINGS_ENABLED', IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED', - IsSSOEnabled = 'IS_SSO_ENABLED', IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED', IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED', IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED', diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index 6216d8019052..39e406cc80f6 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -9,8 +9,6 @@ import { Repository } from 'typeorm'; import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { SSOException, SSOExceptionCode, @@ -30,8 +28,6 @@ import { export class SSOService { private readonly featureLookUpKey = BillingEntitlementKey.SSO; constructor( - @InjectRepository(FeatureFlagEntity, 'core') - private readonly featureFlagRepository: Repository, @InjectRepository(WorkspaceSSOIdentityProvider, 'core') private readonly workspaceSSOIdentityProviderRepository: Repository, private readonly environmentService: EnvironmentService, @@ -39,18 +35,6 @@ export class SSOService { ) {} private async isSSOEnabled(workspaceId: string) { - const isSSOEnabledFeatureFlag = await this.featureFlagRepository.findOneBy({ - workspaceId, - key: FeatureFlagKey.IsSSOEnabled, - value: true, - }); - - if (!isSSOEnabledFeatureFlag?.value) { - throw new SSOException( - `${FeatureFlagKey.IsSSOEnabled} feature flag is disabled`, - SSOExceptionCode.SSO_DISABLE, - ); - } const isSSOBillingEnabled = await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement( workspaceId, @@ -59,7 +43,7 @@ export class SSOService { if (!isSSOBillingEnabled) { throw new SSOException( - `${FeatureFlagKey.IsSSOEnabled} feature is enabled but no entitlement for this workspace`, + `No entitlement found for this workspace`, SSOExceptionCode.SSO_DISABLE, ); } diff --git a/packages/twenty-server/src/main.ts b/packages/twenty-server/src/main.ts index 548540494755..96ab5b3901b2 100644 --- a/packages/twenty-server/src/main.ts +++ b/packages/twenty-server/src/main.ts @@ -84,9 +84,7 @@ const bootstrap = async () => { generateFrontConfig(); // Enable session - Today it's used only for SSO - if (environmentService.get('AUTH_SSO_ENABLED')) { - app.use(session(getSessionStorageOptions(environmentService))); - } + app.use(session(getSessionStorageOptions(environmentService))); await app.listen(environmentService.get('PORT')); }; diff --git a/packages/twenty-website/src/content/developers/self-hosting/setup.mdx b/packages/twenty-website/src/content/developers/self-hosting/setup.mdx index 1c63ee63da30..52dce83f387f 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/setup.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/setup.mdx @@ -116,7 +116,6 @@ yarn command:prod cron:calendar:ongoing-stale ['AUTH_GOOGLE_CLIENT_SECRET', '', 'Google client secret'], ['AUTH_GOOGLE_CALLBACK_URL', 'https://[YourDomain]/auth/google/redirect', 'Google auth callback'], ['AUTH_MICROSOFT_ENABLED', 'false', 'Enable Microsoft SSO login'], - ['AUTH_SSO_ENABLED', 'false', 'Enable SSO with SAML or OIDC'], ['AUTH_MICROSOFT_CLIENT_ID', '', 'Microsoft client ID'], ['AUTH_MICROSOFT_TENANT_ID', '', 'Microsoft tenant ID'], ['AUTH_MICROSOFT_CLIENT_SECRET', '', 'Microsoft client secret'], From 92c119ed439a2e372a5c1c3d1abeee31d51eb6f5 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 10 Jan 2025 16:18:37 +0100 Subject: [PATCH 18/22] Add suggested values for variable dropdown (#9437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture d’écran 2025-01-07 à 15 37 20 Here is a first version: - simple fields have a suggested value - composite do not, but sub values of composite do - json, arrays or complex values do not --- packages/twenty-e2e-testing/package.json | 2 +- packages/twenty-emails/package.json | 2 +- packages/twenty-front/package.json | 2 +- .../serverlessFunctionTestDataFamilyState.ts | 5 +- .../WorkflowVariablesDropdownFieldItems.tsx | 11 ++-- packages/twenty-server/package.json | 2 +- .../src/engine/utils/generate-fake-value.ts | 59 ++++++++++++++++--- .../utils/generate-fake-object-record.ts | 4 +- packages/twenty-ui/package.json | 2 +- .../menu-item/components/MenuItemSelect.tsx | 9 ++- .../components/MenuItemLeftContent.tsx | 13 +++- packages/twenty-website/package.json | 2 +- 12 files changed, 89 insertions(+), 24 deletions(-) diff --git a/packages/twenty-e2e-testing/package.json b/packages/twenty-e2e-testing/package.json index 8f21de70c784..08db290e0263 100644 --- a/packages/twenty-e2e-testing/package.json +++ b/packages/twenty-e2e-testing/package.json @@ -1,6 +1,6 @@ { "name": "twenty-e2e-testing", - "version": "0.40.0-canary", + "version": "0.35.4", "description": "", "author": "", "private": true, diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json index 9887a8b94024..d3f75dedcde0 100644 --- a/packages/twenty-emails/package.json +++ b/packages/twenty-emails/package.json @@ -1,6 +1,6 @@ { "name": "twenty-emails", - "version": "0.40.0-canary", + "version": "0.35.4", "description": "", "author": "", "private": true, diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 2e8fedd079bf..16a3b83ef7fc 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -1,6 +1,6 @@ { "name": "twenty-front", - "version": "0.40.0-canary", + "version": "0.35.4", "private": true, "type": "module", "scripts": { diff --git a/packages/twenty-front/src/modules/workflow/states/serverlessFunctionTestDataFamilyState.ts b/packages/twenty-front/src/modules/workflow/states/serverlessFunctionTestDataFamilyState.ts index 19ad1740931c..4f297fa74bef 100644 --- a/packages/twenty-front/src/modules/workflow/states/serverlessFunctionTestDataFamilyState.ts +++ b/packages/twenty-front/src/modules/workflow/states/serverlessFunctionTestDataFamilyState.ts @@ -1,5 +1,5 @@ -import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql'; import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; +import { ServerlessFunctionExecutionStatus } from '~/generated-metadata/graphql'; export type ServerlessFunctionTestData = { input: { [field: string]: any }; @@ -13,8 +13,7 @@ export type ServerlessFunctionTestData = { height: number; }; -export const DEFAULT_OUTPUT_VALUE = - 'Enter an input above then press "run Function"'; +export const DEFAULT_OUTPUT_VALUE = 'Enter an input above then press "Test"'; export const serverlessFunctionTestDataFamilyState = createFamilyState< ServerlessFunctionTestData, diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems.tsx b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems.tsx index 289863a80a64..41612d837c6c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems.tsx @@ -145,15 +145,18 @@ export const WorkflowVariablesDropdownFieldItems = ({ /> - {filteredOptions.map(([key, value]) => ( + {filteredOptions.map(([key, subStep]) => ( handleSelectField(key)} - text={value.label || key} - hasSubMenu={!value.isLeaf} - LeftIcon={value.icon ? getIcon(value.icon) : undefined} + text={subStep.label || key} + hasSubMenu={!subStep.isLeaf} + LeftIcon={subStep.icon ? getIcon(subStep.icon) : undefined} + contextualText={ + subStep.isLeaf ? subStep?.value?.toString() : undefined + } /> ))} diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 6de9253d6e35..5486c78aa38b 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -1,6 +1,6 @@ { "name": "twenty-server", - "version": "0.40.0-canary", + "version": "0.35.4", "description": "", "author": "", "private": true, diff --git a/packages/twenty-server/src/engine/utils/generate-fake-value.ts b/packages/twenty-server/src/engine/utils/generate-fake-value.ts index bfc87d313c4e..93839ac61e9e 100644 --- a/packages/twenty-server/src/engine/utils/generate-fake-value.ts +++ b/packages/twenty-server/src/engine/utils/generate-fake-value.ts @@ -7,13 +7,16 @@ type FakeValueTypes = | Date | FakeValueTypes[] | FieldMetadataType - | { [key: string]: FakeValueTypes }; + | { [key: string]: FakeValueTypes } + | null; -export const generateFakeValue = (valueType: string): FakeValueTypes => { +type TypeClassification = 'Primitive' | 'FieldMetadataType'; + +const generatePrimitiveValue = (valueType: string): FakeValueTypes => { if (valueType === 'string') { - return 'generated-string-value'; + return 'My text'; } else if (valueType === 'number') { - return 1; + return 20; } else if (valueType === 'boolean') { return true; } else if (valueType === 'Date') { @@ -38,9 +41,51 @@ export const generateFakeValue = (valueType: string): FakeValueTypes => { }); return objData; - } else if (valueType === FieldMetadataType.TEXT) { - return 'My text'; } else { - return 'generated-string-value'; + return null; + } +}; + +const generateFieldMetadataTypeValue = ( + valueType: string, +): FakeValueTypes | null => { + // composite types do not need to be generated + switch (valueType) { + case FieldMetadataType.TEXT: + return 'My text'; + case FieldMetadataType.NUMBER: + return 20; + case FieldMetadataType.BOOLEAN: + return true; + case FieldMetadataType.DATE: + return '01/23/2025'; + case FieldMetadataType.DATE_TIME: + return '01/23/2025 15:16'; + case FieldMetadataType.ADDRESS: + return '123 Main St, Anytown, CA 12345'; + case FieldMetadataType.FULL_NAME: + return 'Tim Cook'; + case FieldMetadataType.RAW_JSON: + return null; + case FieldMetadataType.RICH_TEXT: + return 'My rich text'; + case FieldMetadataType.UUID: + return '123e4567-e89b-12d3-a456-426614174000'; + default: + return null; + } +}; + +export const generateFakeValue = ( + valueType: string, + classification: TypeClassification = 'Primitive', +): FakeValueTypes => { + switch (classification) { + case 'Primitive': + return generatePrimitiveValue(valueType); + case 'FieldMetadataType': + return generateFieldMetadataTypeValue(valueType); + default: + return null; } }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record.ts index 945f8a3e0e32..c1e0142c356b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/utils/generate-fake-object-record.ts @@ -25,7 +25,7 @@ const generateObjectRecordFields = ( type: field.type, icon: field.icon, label: field.label, - value: generateFakeValue(field.type), + value: generateFakeValue(field.type, 'FieldMetadataType'), }; } else { acc[field.name] = { @@ -37,7 +37,7 @@ const generateObjectRecordFields = ( isLeaf: true, type: property.type, label: camelToTitleCase(property.name), - value: generateFakeValue(property.type), + value: generateFakeValue(property.type, 'FieldMetadataType'), }; return acc; diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index ec65a2cced58..3726ca89206c 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -1,6 +1,6 @@ { "name": "twenty-ui", - "version": "0.40.0-canary", + "version": "0.35.4", "type": "module", "main": "./src/index.ts", "exports": { diff --git a/packages/twenty-ui/src/navigation/menu-item/components/MenuItemSelect.tsx b/packages/twenty-ui/src/navigation/menu-item/components/MenuItemSelect.tsx index 211b8452afce..1b2a64181333 100644 --- a/packages/twenty-ui/src/navigation/menu-item/components/MenuItemSelect.tsx +++ b/packages/twenty-ui/src/navigation/menu-item/components/MenuItemSelect.tsx @@ -47,6 +47,7 @@ type MenuItemSelectProps = { disabled?: boolean; hovered?: boolean; hasSubMenu?: boolean; + contextualText?: string; }; export const MenuItemSelect = ({ @@ -59,6 +60,7 @@ export const MenuItemSelect = ({ disabled, hovered, hasSubMenu = false, + contextualText, }: MenuItemSelectProps) => { const theme = useTheme(); @@ -73,8 +75,13 @@ export const MenuItemSelect = ({ aria-selected={selected} aria-disabled={disabled} > - + {selected && needIconCheck && } + {hasSubMenu && ( theme.color.gray35}; font-family: inherit; @@ -29,6 +33,7 @@ const StyledContextualText = styled.div` white-space: nowrap; padding-left: ${({ theme }) => theme.spacing(1)}; + flex-shrink: 1; `; type MenuItemLeftContentProps = { @@ -67,7 +72,13 @@ export const MenuItemLeftContent = ({ )} - {isString(text) ? : text} + {isString(text) ? ( + + + + ) : ( + text + )} {isString(contextualText) ? ( {`· ${contextualText}`} ) : ( diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json index 0030a953ff7f..2f4abe89226a 100644 --- a/packages/twenty-website/package.json +++ b/packages/twenty-website/package.json @@ -1,6 +1,6 @@ { "name": "twenty-website", - "version": "0.40.0-canary", + "version": "0.35.4", "private": true, "scripts": { "nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js", From d1170668dff08db1e1dfef8dd4a66e1f1a66cf88 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Fri, 10 Jan 2025 16:20:10 +0100 Subject: [PATCH 19/22] refactor: update import paths for FeatureFlagKey (#9542) Replaced references to '~/generated-metadata/graphql' with '~/generated/graphql' across multiple files. This change ensures uniformity in import paths and aligns with the updated directory structure. --- .../modules/activities/components/ActivityRichTextEditor.tsx | 2 +- .../src/modules/activities/hooks/useOpenActivityRightDrawer.ts | 2 +- .../src/modules/activities/hooks/useOpenCreateActivityDrawer.ts | 2 +- .../record-right-drawer/components/RightDrawerRecord.tsx | 2 +- .../record-table/components/RecordTableRecordGroupRows.tsx | 2 +- .../src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index a39d230d8a2b..4dab9fa2fc60 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -31,7 +31,7 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import '@blocknote/core/fonts/inter.css'; import '@blocknote/mantine/style.css'; import '@blocknote/react/style.css'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { FeatureFlagKey } from '~/generated/graphql'; type ActivityRichTextEditorProps = { activityId: string; diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts index 26ca1032323b..5d0aec52a52e 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts @@ -9,7 +9,7 @@ import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPage import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { FeatureFlagKey } from '~/generated/graphql'; export const useOpenActivityRightDrawer = ({ objectNameSingular, diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index 48a98407d8c5..bbaf0c183c0e 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -19,7 +19,7 @@ import { isNewViewableRecordLoadingState } from '@/object-record/record-right-dr import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { FeatureFlagKey } from '~/generated/graphql'; import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; export const useOpenCreateActivityDrawer = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx index 4ac308ee1400..c532bf4bb97d 100644 --- a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx +++ b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx @@ -12,7 +12,7 @@ import { RecordFieldValueSelectorContextProvider } from '@/object-record/record- import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { FeatureFlagKey } from '~/generated/graphql'; const StyledRightDrawerRecord = styled.div<{ hasTopBar: boolean }>` height: ${({ theme, hasTopBar }) => diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx index 78d394fe23f3..cf29982e69ac 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx @@ -11,7 +11,7 @@ import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component- import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useMemo } from 'react'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { FeatureFlagKey } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; export const RecordTableRecordGroupRows = () => { diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts index 05c2c4e99ce4..52a604e71a2d 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts @@ -8,7 +8,7 @@ import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; import { mapRightDrawerPageToCommandMenuPage } from '@/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated-metadata/graphql'; +import { FeatureFlagKey } from '~/generated/graphql'; import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState'; import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { RightDrawerPages } from '../types/RightDrawerPages'; From b46bc84dab126bb2113234a762c9afb318c67454 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 10 Jan 2025 16:52:29 +0100 Subject: [PATCH 20/22] Fix mail import gmail driver when message has been deleted (#9546) We forgot to ignore messages that were removed --- .../drivers/gmail/services/gmail-get-messages.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts index 15590f1f5148..ce228e3d997b 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts @@ -81,6 +81,8 @@ export class GmailGetMessagesService { response.error, messageIds[index], ); + + return undefined; } return parseAndFormatGmailMessage( From 07fa58b042b0b00bd9853b9f47d2e8ada4d2f6ff Mon Sep 17 00:00:00 2001 From: Nicolas Rouanne Date: Fri, 10 Jan 2025 17:21:15 +0100 Subject: [PATCH 21/22] docs(server-commands): fix typo in command to generate migrations (#9545) - the command had `--migration:generate` instead of `migration:generate` written in the doc - when copy pasting it we got this error ``` Not enough non-option arguments: got 0, need at least 1 ``` - after removing the extra `--` it worked as expected --- .../developers/backend-development/server-commands.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/twenty-website/src/content/developers/backend-development/server-commands.mdx b/packages/twenty-website/src/content/developers/backend-development/server-commands.mdx index 441d07c7410d..94a8bd4d40f5 100644 --- a/packages/twenty-website/src/content/developers/backend-development/server-commands.mdx +++ b/packages/twenty-website/src/content/developers/backend-development/server-commands.mdx @@ -48,7 +48,7 @@ npx nx run twenty-server:database:reset #### For objects in Core/Metadata schemas (TypeORM) ```bash -npx nx run twenty-server:typeorm --migration:generate src/database/typeorm/metadata/migrations/nameOfYourMigration -d src/database/typeorm/metadata/metadata.datasource.ts # replace by core data source if necessary +npx nx run twenty-server:typeorm migration:generate src/database/typeorm/metadata/migrations/nameOfYourMigration -d src/database/typeorm/metadata/metadata.datasource.ts # replace by core data source if necessary ``` #### For Workspace objects @@ -98,4 +98,4 @@ Here's what the tech stack now looks like. **Development** - [AWS EKS](https://aws.amazon.com/eks/) - \ No newline at end of file + From 4ca03d00668b1851e8bd85f1e6b6003b051a722f Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Fri, 10 Jan 2025 17:21:33 +0100 Subject: [PATCH 22/22] fix(auth): handle error properly in loadCurrentUser (#9539) Updated the loadCurrentUser function to throw specific errors when an API error occurs. This improves clarity and error handling, replacing the generic "No current user result" exception. Fix #9536 --- packages/twenty-front/src/modules/auth/hooks/useAuth.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index d583e815d8eb..5646d5a2ff6c 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -177,6 +177,10 @@ export const useAuth = () => { const loadCurrentUser = useCallback(async () => { const currentUserResult = await getCurrentUser(); + if (isDefined(currentUserResult.error)) { + throw new Error(currentUserResult.error.message); + } + const user = currentUserResult.data?.currentUser; if (!user) {