From c6d763ede9019f316bfeb19f37bc4e0c3dd86313 Mon Sep 17 00:00:00 2001 From: Harshit Singh <73997189+harshit078@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:03:33 +0530 Subject: [PATCH 001/115] fix: Cursor pointer on Settings cards (#7291) > [!Note] > This PR solves the issue #7289 --- .../src/modules/settings/components/SettingsCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx index 7383b6e902ee..972bac3d77a4 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx @@ -27,6 +27,7 @@ const StyledCard = styled(Card)<{ width: 100%; & :hover { background-color: ${({ theme }) => theme.background.quaternary}; + cursor: pointer; } `; From c4762c39217edccfaac6d91936dd93f111930aed Mon Sep 17 00:00:00 2001 From: Rishi Kant <110294979+kant-github@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:39:33 +0530 Subject: [PATCH 002/115] Add Header to Email & Calendar Tabs #7288 (#7293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Fix: 7288 - Add Header to Email & Calendar Tabs (No Account Connected) ## Description Added a header to the **Email** and **Calendar** tabs when no account is connected, matching the style and spacing of the account page to prevent layout issues when switching between pages. ### Header Content: - **Connected Accounts** - **Manage your internet accounts** ## Screenshot: Screenshot 2024-09-27 at 5 20 55 PM Fixes #7288 --------- Co-authored-by: Félix Malfait --- .../components/SettingsAccountsCalendarChannelsContainer.tsx | 4 ++-- .../components/SettingsAccountsMessageChannelsContainer.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx index ab231c603a58..9556441e6267 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx @@ -8,7 +8,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails'; import { SettingsAccountsCalendarChannelsGeneral } from '@/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral'; -import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard'; +import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountCalendarChannelsTabListComponentId'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -56,7 +56,7 @@ export const SettingsAccountsCalendarChannelsContainer = () => { ]; if (!calendarChannels.length) { - return ; + return ; } return ( diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx index d2d15d02fdae..2c5e1102d3b3 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx @@ -6,8 +6,8 @@ import { MessageChannel } from '@/accounts/types/MessageChannel'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard'; import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails'; +import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -55,7 +55,7 @@ export const SettingsAccountsMessageChannelsContainer = () => { ]; if (!messageChannels.length) { - return ; + return ; } return ( From ca906bbf6b83af751406a3d960efcd4e5a83fb27 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:50:21 +0530 Subject: [PATCH 003/115] 5922 - UI Overlap and State Persistence in Filter Menus (#7270) fixes #5922 https://github.com/user-attachments/assets/07d088da-cefb-4d87-9016-e14cef18567d --- .../MultipleFiltersDropdownButton.tsx | 11 +- .../MultipleFiltersDropdownContent.tsx | 128 ++++++++++-------- .../ObjectFilterDropdownOperandButton.tsx | 8 -- .../components/ObjectSortDropdownButton.tsx | 118 +++++++++------- .../EditableFilterDropdownButton.tsx | 6 + 5 files changed, 154 insertions(+), 117 deletions(-) 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 885a6c8eb840..b88f421c9857 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 @@ -2,6 +2,7 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdow import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useCallback } from 'react'; import { MultipleFiltersButton } from './MultipleFiltersButton'; import { MultipleFiltersDropdownContent } from './MultipleFiltersDropdownContent'; @@ -13,12 +14,18 @@ type MultipleFiltersDropdownButtonProps = { export const MultipleFiltersDropdownButton = ({ hotkeyScope, }: MultipleFiltersDropdownButtonProps) => { - const { resetFilter } = useFilterDropdown(); + const { resetFilter, setIsObjectFilterDropdownOperandSelectUnfolded } = + useFilterDropdown(); + + const handleDropdownClose = useCallback(() => { + resetFilter(); + setIsObjectFilterDropdownOperandSelectUnfolded(false); + }, [resetFilter, setIsObjectFilterDropdownOperandSelectUnfolded]); return ( } dropdownComponents={} dropdownHotkeyScope={hotkeyScope} 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 ec3df12f0d1d..7638532ee3b6 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,9 @@ -import { useRecoilValue } from 'recoil'; - +import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; - -import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput'; import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect'; @@ -16,6 +14,21 @@ import { ObjectFilterDropdownOptionSelect } from './ObjectFilterDropdownOptionSe import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput'; +const StyledContainer = styled.div` + position: relative; +`; + +const StyledOperandSelectContainer = styled.div` + background: ${({ theme }) => theme.background.secondary}; + box-shadow: ${({ theme }) => theme.boxShadow.light}; + border-radius: ${({ theme }) => theme.border.radius.md}; + left: 10px; + position: absolute; + top: 10px; + width: 100%; + z-index: 1000; +`; + type MultipleFiltersDropdownContentProps = { filterDropdownId?: string; }; @@ -24,20 +37,23 @@ export const MultipleFiltersDropdownContent = ({ filterDropdownId, }: MultipleFiltersDropdownContentProps) => { const { - isObjectFilterDropdownOperandSelectUnfoldedState, filterDefinitionUsedInDropdownState, selectedOperandInDropdownState, + isObjectFilterDropdownOperandSelectUnfoldedState, } = useFilterDropdown({ filterDropdownId }); const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue( isObjectFilterDropdownOperandSelectUnfoldedState, ); + const filterDefinitionUsedInDropdown = useRecoilValue( filterDefinitionUsedInDropdownState, ); + const selectedOperandInDropdown = useRecoilValue( selectedOperandInDropdownState, ); + const isEmptyOperand = selectedOperandInDropdown && [ViewFilterOperand.IsEmpty, ViewFilterOperand.IsNotEmpty].includes( @@ -45,64 +61,64 @@ export const MultipleFiltersDropdownContent = ({ ); return ( - <> + {!filterDefinitionUsedInDropdown ? ( - ) : isObjectFilterDropdownOperandSelectUnfolded ? ( - - ) : isEmptyOperand ? ( - ) : ( - selectedOperandInDropdown && ( - <> - - - {[ - 'TEXT', - 'EMAIL', - 'EMAILS', - 'PHONE', - 'FULL_NAME', - 'LINK', - 'LINKS', - 'ADDRESS', - 'ACTOR', - 'ARRAY', - 'PHONES', - ].includes(filterDefinitionUsedInDropdown.type) && ( - - )} - {['NUMBER', 'CURRENCY'].includes( - filterDefinitionUsedInDropdown.type, - ) && } - {filterDefinitionUsedInDropdown.type === 'RATING' && ( - - )} - {['DATE_TIME', 'DATE'].includes( - filterDefinitionUsedInDropdown.type, - ) && } - {filterDefinitionUsedInDropdown.type === 'RELATION' && ( - <> - - - - - )} - {filterDefinitionUsedInDropdown.type === 'SELECT' && ( - <> - - - - - )} - - ) + <> + + {isObjectFilterDropdownOperandSelectUnfolded && ( + + + + )} + {!isEmptyOperand && selectedOperandInDropdown && ( + <> + {[ + 'TEXT', + 'EMAIL', + 'EMAILS', + 'PHONE', + 'FULL_NAME', + 'LINK', + 'LINKS', + 'ADDRESS', + 'ACTOR', + 'ARRAY', + 'PHONES', + ].includes(filterDefinitionUsedInDropdown.type) && ( + + )} + {['NUMBER', 'CURRENCY'].includes( + filterDefinitionUsedInDropdown.type, + ) && } + {filterDefinitionUsedInDropdown.type === 'RATING' && ( + + )} + {['DATE_TIME', 'DATE'].includes( + filterDefinitionUsedInDropdown.type, + ) && } + {filterDefinitionUsedInDropdown.type === 'RELATION' && ( + <> + + + + )} + {filterDefinitionUsedInDropdown.type === 'SELECT' && ( + <> + + + + )} + + )} + )} - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx index 4aa675ec3539..3931c76547e1 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx @@ -10,19 +10,11 @@ export const ObjectFilterDropdownOperandButton = () => { const { selectedOperandInDropdownState, setIsObjectFilterDropdownOperandSelectUnfolded, - isObjectFilterDropdownOperandSelectUnfoldedState, } = useFilterDropdown(); const selectedOperandInDropdown = useRecoilValue( selectedOperandInDropdownState, ); - const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue( - isObjectFilterDropdownOperandSelectUnfoldedState, - ); - - if (isObjectFilterDropdownOperandSelectUnfolded) { - return null; - } return ( theme.background.secondary}; + box-shadow: ${({ theme }) => theme.boxShadow.light}; + border-radius: ${({ theme }) => theme.border.radius.md}; + left: 10px; + position: absolute; + top: 10px; + width: 100%; + z-index: 1000; +`; + export type ObjectSortDropdownButtonProps = { sortDropdownId: string; hotkeyScope: HotkeyScope; @@ -95,60 +110,61 @@ export const ObjectSortDropdownButton = ({ } dropdownComponents={ <> - {isSortDirectionMenuUnfolded ? ( - - {SORT_DIRECTIONS.map((sortOrder, index) => ( - { - setSelectedSortDirection(sortOrder); - setIsSortDirectionMenuUnfolded(false); - }} - text={sortOrder === 'asc' ? 'Ascending' : 'Descending'} - /> - ))} - - ) : ( - <> - setIsSortDirectionMenuUnfolded(true)} - > - {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'} - - - setObjectSortDropdownSearchInput(event.target.value) - } - /> + {isSortDirectionMenuUnfolded && ( + - {[...availableSortDefinitions] - .sort((a, b) => a.label.localeCompare(b.label)) - .filter((item) => - item.label - .toLocaleLowerCase() - .includes( - objectSortDropdownSearchInput.toLocaleLowerCase(), - ), - ) - .map((availableSortDefinition, index) => ( - { - setObjectSortDropdownSearchInput(''); - handleAddSort(availableSortDefinition); - }} - LeftIcon={getIcon(availableSortDefinition.iconName)} - text={availableSortDefinition.label} - /> - ))} + {SORT_DIRECTIONS.map((sortOrder, index) => ( + { + setSelectedSortDirection(sortOrder); + setIsSortDirectionMenuUnfolded(false); + }} + text={sortOrder === 'asc' ? 'Ascending' : 'Descending'} + /> + ))} - + )} + + setIsSortDirectionMenuUnfolded(true)} + > + {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'} + + + setObjectSortDropdownSearchInput(event.target.value) + } + /> + + {[...availableSortDefinitions] + .sort((a, b) => a.label.localeCompare(b.label)) + .filter((item) => + item.label + .toLocaleLowerCase() + .includes( + objectSortDropdownSearchInput.toLocaleLowerCase(), + ), + ) + .map((availableSortDefinition, index) => ( + { + setObjectSortDropdownSearchInput(''); + handleAddSort(availableSortDefinition); + }} + LeftIcon={getIcon(availableSortDefinition.iconName)} + text={availableSortDefinition.label} + /> + ))} + + } onClose={handleDropdownButtonClose} diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index bbaa9af31b03..45c974eca420 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -29,6 +29,7 @@ export const EditableFilterDropdownButton = ({ setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, setSelectedFilter, + setIsObjectFilterDropdownOperandSelectUnfolded, } = useFilterDropdown({ filterDropdownId: viewFilterDropdownId, }); @@ -79,6 +80,10 @@ export const EditableFilterDropdownButton = ({ } }, [viewFilter, deleteCombinedViewFilter]); + const handleDropdownClose = useCallback(() => { + setIsObjectFilterDropdownOperandSelectUnfolded(false); + }, [setIsObjectFilterDropdownOperandSelectUnfolded]); + return ( ); }; From c9c2f32922adda277d1f695dcbca8c55c7535ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:52:04 +0200 Subject: [PATCH 004/115] 7154 deleted event is not emitted when calling destroyone (#7159) Closes #7154 --- .../graphql-query-runner.service.ts | 89 ++++++++++++++++++- .../jobs/call-webhook-jobs.job.ts | 1 + .../listeners/entity-events-to-db.listener.ts | 7 ++ .../types/workspace-query-hook.type.ts | 5 +- .../workspace-resolvers-builder.interface.ts | 1 + .../types/object-record-destroy.event.ts | 7 ++ ...vent-cleaner-connected-account.listener.ts | 4 +- .../listeners/connected-account.listener.ts | 4 +- ...ected-account-delete-one.pre-query.hook.ts | 4 +- ...sage-cleaner-connected-account.listener.ts | 4 +- ...import-manager-message-channel.listener.ts | 4 +- .../database-event-trigger.listener.ts | 13 ++- 12 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-destroy.event.ts diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts index cd1337b458c7..8eb4a614add5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -35,6 +35,7 @@ import { } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; +import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; @@ -284,10 +285,94 @@ export class GraphqlQueryRunnerService { async destroyOne( args: DestroyOneResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise { + ): Promise { const graphqlQueryDestroyOneResolverService = new GraphqlQueryDestroyOneResolverService(this.twentyORMGlobalManager); - return graphqlQueryDestroyOneResolverService.destroyOne(args, options); + const { authContext, objectMetadataItem } = options; + + assertMutationNotOnRemoteObject(objectMetadataItem); + assertIsValidUuid(args.id); + + const hookedArgs = + await this.workspaceQueryHookService.executePreQueryHooks( + authContext, + objectMetadataItem.nameSingular, + 'destroyOne', + args, + ); + + const computedArgs = (await this.queryRunnerArgsFactory.create( + hookedArgs, + options, + ResolverArgsType.DestroyOne, + )) as DestroyOneResolverArgs; + + const result = (await graphqlQueryDestroyOneResolverService.destroyOne( + computedArgs, + options, + )) as ObjectRecord; + + await this.workspaceQueryHookService.executePostQueryHooks( + authContext, + objectMetadataItem.nameSingular, + 'destroyOne', + [result], + ); + + await this.triggerWebhooks( + [result], + CallWebhookJobsJobOperation.destroy, + options, + ); + + this.emitDestroyEvents([result], authContext, objectMetadataItem); + + return result; + } + + private emitDestroyEvents( + records: BaseRecord[], + authContext: AuthContext, + objectMetadataItem: ObjectMetadataInterface, + ) { + this.workspaceEventEmitter.emit( + `${objectMetadataItem.nameSingular}.destroyed`, + records.map((record) => { + return { + userId: authContext.user?.id, + recordId: record.id, + objectMetadata: objectMetadataItem, + properties: { + before: this.removeNestedProperties(record), + }, + } satisfies ObjectRecordDeleteEvent; + }), + authContext.workspace.id, + ); + } + + private removeNestedProperties( + record: Record, + ) { + if (!record) { + return; + } + + const sanitizedRecord = {}; + + for (const [key, value] of Object.entries(record)) { + if (value && typeof value === 'object' && value['edges']) { + continue; + } + + if (key === '__typename') { + continue; + } + + sanitizedRecord[key] = value; + } + + return sanitizedRecord; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts index d0bbc6872c06..a1b43eb22034 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts @@ -20,6 +20,7 @@ export enum CallWebhookJobsJobOperation { create = 'create', update = 'update', delete = 'delete', + destroy = 'destroy', } export type CallWebhookJobsJobData = { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts index eb9ddbf06a6f..8eb8be61f774 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts @@ -49,6 +49,13 @@ export class EntityEventsToDbListener { return this.handle(payload); } + @OnEvent('*.destroyed') + async handleDestroy( + payload: WorkspaceEventBatch>, + ) { + return this.handle(payload); + } + private async handle(payload: WorkspaceEventBatch) { const filteredEvents = payload.events.filter( (event) => event.objectMetadata?.isAuditLogged, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts index f155475877e0..b75c939d1ac7 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts @@ -4,6 +4,7 @@ import { DeleteManyResolverArgs, DeleteOneResolverArgs, DestroyManyResolverArgs, + DestroyOneResolverArgs, FindDuplicatesResolverArgs, FindManyResolverArgs, FindOneResolverArgs, @@ -39,4 +40,6 @@ export type WorkspacePreQueryHookPayload = T extends 'createMany' ? RestoreManyResolverArgs : T extends 'destroyMany' ? DestroyManyResolverArgs - : never; + : T extends 'destroyOne' + ? DestroyOneResolverArgs + : never; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts index 4e2a0af85196..22c07059737d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts @@ -22,6 +22,7 @@ export enum ResolverArgsType { DeleteMany = 'DeleteMany', RestoreMany = 'RestoreMany', DestroyMany = 'DestroyMany', + DestroyOne = 'DestroyOne', } export interface FindManyResolverArgs< diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-destroy.event.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-destroy.event.ts new file mode 100644 index 000000000000..f12b1e17547f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-destroy.event.ts @@ -0,0 +1,7 @@ +import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; + +export class ObjectRecordDestroyEvent extends ObjectRecordBaseEvent { + properties: { + before: T; + }; +} diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts index 387dc0743c68..1406d442d02c 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts @@ -19,8 +19,8 @@ export class CalendarEventCleanerConnectedAccountListener { private readonly calendarQueueService: MessageQueueService, ) {} - @OnEvent('connectedAccount.deleted') - async handleDeletedEvent( + @OnEvent('connectedAccount.destroyed') + async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent >, diff --git a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts index da8bededdb03..62ee28d1bede 100644 --- a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts +++ b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts @@ -15,8 +15,8 @@ export class ConnectedAccountListener { private readonly accountsToReconnectService: AccountsToReconnectService, ) {} - @OnEvent('connectedAccount.deleted') - async handleDeletedEvent( + @OnEvent('connectedAccount.destroyed') + async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent >, diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts index c04999bea385..f49db465d04b 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts @@ -8,7 +8,7 @@ import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -@WorkspaceQueryHook(`connectedAccount.deleteOne`) +@WorkspaceQueryHook(`connectedAccount.destroyOne`) export class ConnectedAccountDeleteOnePreQueryHook implements WorkspaceQueryHookInstance { @@ -34,7 +34,7 @@ export class ConnectedAccountDeleteOnePreQueryHook }); this.workspaceEventEmitter.emit( - 'messageChannel.deleted', + 'messageChannel.destroyed', messageChannels.map( (messageChannel) => ({ diff --git a/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts b/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts index 8e837cce6a59..c5c3a033dedc 100644 --- a/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts @@ -19,8 +19,8 @@ export class MessagingMessageCleanerConnectedAccountListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('connectedAccount.deleted') - async handleDeletedEvent( + @OnEvent('connectedAccount.destroyed') + async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent >, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts index 513e2672adf4..80802c3fb2c4 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts @@ -19,8 +19,8 @@ export class MessagingMessageImportManagerMessageChannelListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('messageChannel.deleted') - async handleDeletedEvent( + @OnEvent('messageChannel.destroyed') + async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent >, diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts index e5b62d59afc6..5ea3f82d4781 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts @@ -1,11 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; +import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; @@ -49,11 +50,19 @@ export class DatabaseEventTriggerListener { await this.handleEvent(payload); } + @OnEvent('*.destroyed') + async handleObjectRecordDestroyEvent( + payload: WorkspaceEventBatch>, + ) { + await this.handleEvent(payload); + } + private async handleEvent( payload: WorkspaceEventBatch< | ObjectRecordCreateEvent | ObjectRecordUpdateEvent | ObjectRecordDeleteEvent + | ObjectRecordDestroyEvent >, ) { const workspaceId = payload.workspaceId; From 9d36493cf0f5e1a50f4f7ada88fec9d3fafd310c Mon Sep 17 00:00:00 2001 From: ad-elias Date: Fri, 27 Sep 2024 15:57:38 +0200 Subject: [PATCH 005/115] Date filter improvements (#5917) (#7196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Solves issue #5917. This PR is now ready for the first review! Filters do not fully work yet, there's a problem applying multiple filters like the following: ``` { and: [ { [correspondingField.name]: { gte: start.toISOString(), } as DateFilter, }, { [correspondingField.name]: { lte: end.toISOString(), } as DateFilter, }, ], } ``` I'll do my best to dig into it tonight! --------- Co-authored-by: Félix Malfait --- package.json | 1 + .../MultipleFiltersDropdownContent.tsx | 19 +- .../ObjectFilterDropdownDateInput.tsx | 83 +++++++- .../ObjectFilterDropdownOperandSelect.tsx | 22 +- .../hooks/useSelectFilter.ts | 19 ++ .../object-filter-dropdown/types/Filter.ts | 1 - .../getOperandsForFilterType.test.tsx | 13 +- .../utils/getInitialFilterValue.ts | 42 ++++ .../utils/getOperandLabel.ts | 22 ++ .../utils/getOperandsForFilterType.ts | 14 +- .../utils/getRelativeDateDisplayValue.ts | 28 +++ ...turnObjectDropdownFilterIntoQueryFilter.ts | 122 ++++++++++- .../components/AbsoluteDatePickerHeader.tsx | 108 ++++++++++ .../date/components/InternalDatePicker.tsx | 141 ++++++------- .../components/RelativeDatePickerHeader.tsx | 113 +++++++++++ .../RelativeDateDirectionSelectOptions.ts | 13 ++ .../RelativeDateUnitSelectOptions.ts | 13 ++ .../date/utils/getHighlightedDates.ts | 24 +++ .../date/utils/getMonthSelectOptions.ts | 16 ++ .../EditableFilterDropdownButton.tsx | 9 +- .../modules/views/types/ViewFilterOperand.ts | 6 + .../computeVariableDateViewFilterValue.ts | 10 + .../resolveDateViewFilterValue.ts | 190 ++++++++++++++++++ .../view-filter-value/resolveFilterValue.ts | 42 ++++ .../resolveNumberViewFilterValue.ts | 7 + .../graphql-query.parser.ts | 6 +- .../view-filter.workspace-entity.ts | 14 +- yarn.lock | 8 + 28 files changed, 979 insertions(+), 127 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts create mode 100644 packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue.ts create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/components/RelativeDatePickerHeader.tsx create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions.ts create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getHighlightedDates.ts create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getMonthSelectOptions.ts create mode 100644 packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts create mode 100644 packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts create mode 100644 packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts create mode 100644 packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts diff --git a/package.json b/package.json index 640ba3bb5c96..1e57fde1ae42 100644 --- a/package.json +++ b/package.json @@ -273,6 +273,7 @@ "@types/node": "18.19.26", "@types/passport-google-oauth20": "^2.0.11", "@types/passport-jwt": "^3.0.8", + "@types/pluralize": "^0.0.33", "@types/react": "^18.2.39", "@types/react-datepicker": "^6.2.0", "@types/react-dom": "^18.2.15", 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 7638532ee3b6..da02a28a6731 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 @@ -54,11 +54,20 @@ export const MultipleFiltersDropdownContent = ({ selectedOperandInDropdownState, ); - const isEmptyOperand = + const isConfigurable = selectedOperandInDropdown && - [ViewFilterOperand.IsEmpty, ViewFilterOperand.IsNotEmpty].includes( - selectedOperandInDropdown, - ); + [ + ViewFilterOperand.Is, + ViewFilterOperand.IsNotNull, + ViewFilterOperand.IsNot, + ViewFilterOperand.LessThan, + ViewFilterOperand.GreaterThan, + ViewFilterOperand.IsBefore, + ViewFilterOperand.IsAfter, + ViewFilterOperand.Contains, + ViewFilterOperand.DoesNotContain, + ViewFilterOperand.IsRelative, + ].includes(selectedOperandInDropdown); return ( @@ -72,7 +81,7 @@ export const MultipleFiltersDropdownContent = ({ )} - {!isEmptyOperand && selectedOperandInDropdown && ( + {isConfigurable && selectedOperandInDropdown && ( <> {[ 'TEXT', 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 20d1dee8389f..3961f28c836b 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 @@ -2,10 +2,19 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; 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'; import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { computeVariableDateViewFilterValue } from '@/views/utils/view-filter-value/computeVariableDateViewFilterValue'; +import { + VariableDateViewFilterValueDirection, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue'; import { useState } from 'react'; +import { isDefined } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { isDefined } from '~/utils/isDefined'; export const ObjectFilterDropdownDateInput = () => { const { @@ -23,28 +32,35 @@ export const ObjectFilterDropdownDateInput = () => { selectedOperandInDropdownState, ); - const selectedFilter = useRecoilValue(selectedFilterState); + const selectedFilter = useRecoilValue(selectedFilterState) as + | (Filter & { definition: { type: 'DATE' | 'DATE_TIME' } }) + | null + | undefined; + + const initialFilterValue = selectedFilter + ? resolveFilterValue(selectedFilter) + : null; const [internalDate, setInternalDate] = useState( - selectedFilter?.value ? new Date(selectedFilter.value) : new Date(), + initialFilterValue instanceof Date ? initialFilterValue : null, ); const isDateTimeInput = filterDefinitionUsedInDropdown?.type === FieldMetadataType.DateTime; - const handleChange = (date: Date | null) => { - setInternalDate(date); + const handleAbsoluteDateChange = (newDate: Date | null) => { + setInternalDate(newDate); if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return; selectFilter?.({ id: selectedFilter?.id ? selectedFilter.id : v4(), fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, - value: isDefined(date) ? date.toISOString() : '', + value: newDate?.toISOString() ?? '', operand: selectedOperandInDropdown, - displayValue: isDefined(date) + displayValue: isDefined(newDate) ? isDateTimeInput - ? date.toLocaleString() - : date.toLocaleDateString() + ? newDate.toLocaleString() + : newDate.toLocaleDateString() : '', definition: filterDefinitionUsedInDropdown, }); @@ -52,11 +68,56 @@ export const ObjectFilterDropdownDateInput = () => { setIsObjectFilterDropdownUnfolded(false); }; + const handleRelativeDateChange = ( + relativeDate: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + } | null, + ) => { + if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return; + + const value = relativeDate + ? computeVariableDateViewFilterValue( + relativeDate.direction, + relativeDate.amount, + relativeDate.unit, + ) + : ''; + + selectFilter?.({ + id: selectedFilter?.id ? selectedFilter.id : v4(), + fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, + value, + operand: selectedOperandInDropdown, + displayValue: getRelativeDateDisplayValue(relativeDate), + definition: filterDefinitionUsedInDropdown, + }); + + setIsObjectFilterDropdownUnfolded(false); + }; + + const isRelativeOperand = + selectedOperandInDropdown === ViewFilterOperand.IsRelative; + + const resolvedValue = selectedFilter + ? resolveFilterValue(selectedFilter) + : null; + + const relativeDate = + resolvedValue && !(resolvedValue instanceof Date) + ? resolvedValue + : undefined; + return ( ); 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 5f500b916461..c710d8f23334 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 @@ -2,12 +2,12 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { isDefined } from '~/utils/isDefined'; +import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; import { getOperandLabel } from '../utils/getOperandLabel'; import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; @@ -36,22 +36,25 @@ export const ObjectFilterDropdownOperandSelect = () => { ); const handleOperandChange = (newOperand: ViewFilterOperand) => { - const isEmptyOperand = [ + const isValuelessOperand = [ ViewFilterOperand.IsEmpty, ViewFilterOperand.IsNotEmpty, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, ].includes(newOperand); setSelectedOperandInDropdown(newOperand); setIsObjectFilterDropdownOperandSelectUnfolded(false); - if (isEmptyOperand) { + if (isValuelessOperand && isDefined(filterDefinitionUsedInDropdown)) { selectFilter?.({ id: v4(), fieldMetadataId: filterDefinitionUsedInDropdown?.fieldMetadataId ?? '', displayValue: '', operand: newOperand, value: '', - definition: filterDefinitionUsedInDropdown as FilterDefinition, + definition: filterDefinitionUsedInDropdown, }); return; } @@ -60,12 +63,19 @@ export const ObjectFilterDropdownOperandSelect = () => { isDefined(filterDefinitionUsedInDropdown) && isDefined(selectedFilter) ) { + const { value, displayValue } = getInitialFilterValue( + filterDefinitionUsedInDropdown.type, + newOperand, + selectedFilter.value, + selectedFilter.displayValue, + ); + selectFilter?.({ id: selectedFilter.id ? selectedFilter.id : v4(), fieldMetadataId: selectedFilter.fieldMetadataId, - displayValue: selectedFilter.displayValue, + displayValue, operand: newOperand, - value: selectedFilter.value, + value, definition: filterDefinitionUsedInDropdown, }); } 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/useSelectFilter.ts index 3954d8d6dc9f..005135444a9e 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/useSelectFilter.ts @@ -1,8 +1,10 @@ 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'; import { getOperandsForFilterType } 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 { v4 } from 'uuid'; type SelectFilterParams = { filterDefinition: FilterDefinition; @@ -13,6 +15,7 @@ export const useSelectFilter = () => { setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, setObjectFilterDropdownSearchInput, + selectFilter: filterDropdownSelectFilter, } = useFilterDropdown(); const setHotkeyScope = useSetHotkeyScope(); @@ -31,6 +34,22 @@ export const useSelectFilter = () => { getOperandsForFilterType(filterDefinition.type)?.[0], ); + const { value, displayValue } = getInitialFilterValue( + filterDefinition.type, + getOperandsForFilterType(filterDefinition.type)?.[0], + ); + + if (value !== '') { + filterDropdownSelectFilter({ + id: v4(), + fieldMetadataId: filterDefinition.fieldMetadataId, + displayValue, + operand: getOperandsForFilterType(filterDefinition.type)?.[0], + value, + definition: filterDefinition, + }); + } + setObjectFilterDropdownSearchInput(''); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/Filter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/Filter.ts index 52ed99ac5531..4d2eddb8756e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/Filter.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/Filter.ts @@ -1,5 +1,4 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; - import { FilterDefinition } from './FilterDefinition'; export type Filter = { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx index 7b4b9516e7f4..a06f8455d1f4 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx @@ -19,6 +19,16 @@ describe('getOperandsForFilterType', () => { ViewFilterOperand.LessThan, ]; + const dateOperands = [ + ViewFilterOperand.Is, + ViewFilterOperand.IsRelative, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, + ViewFilterOperand.IsBefore, + ViewFilterOperand.IsAfter, + ]; + const relationOperand = [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; const testCases = [ @@ -31,7 +41,8 @@ describe('getOperandsForFilterType', () => { ['ACTOR', [...containsOperands, ...emptyOperands]], ['CURRENCY', [...numberOperands, ...emptyOperands]], ['NUMBER', [...numberOperands, ...emptyOperands]], - ['DATE_TIME', [...numberOperands, ...emptyOperands]], + ['DATE', [...dateOperands, ...emptyOperands]], + ['DATE_TIME', [...dateOperands, ...emptyOperands]], ['RELATION', [...relationOperand, ...emptyOperands]], [undefined, []], [null, []], diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts new file mode 100644 index 000000000000..3076bef96d03 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts @@ -0,0 +1,42 @@ +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { z } from 'zod'; + +export const getInitialFilterValue = ( + newType: FilterType, + newOperand: ViewFilterOperand, + oldValue?: string, + oldDisplayValue?: string, +): Pick | Record => { + switch (newType) { + case 'DATE': + case 'DATE_TIME': { + const activeDatePickerOperands = [ + ViewFilterOperand.IsBefore, + ViewFilterOperand.Is, + ViewFilterOperand.IsAfter, + ]; + + if (activeDatePickerOperands.includes(newOperand)) { + const date = z.coerce.date().safeParse(oldValue).data ?? new Date(); + const value = date.toISOString(); + const displayValue = + newType === 'DATE' + ? date.toLocaleString() + : date.toLocaleDateString(); + + return { value, displayValue }; + } + + if (newOperand === ViewFilterOperand.IsRelative) { + return { value: '', displayValue: '' }; + } + break; + } + } + return { + value: oldValue ?? '', + displayValue: oldDisplayValue ?? '', + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts index 9c9e297ef960..b68049b51750 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts @@ -12,6 +12,10 @@ export const getOperandLabel = ( return 'Greater than'; case ViewFilterOperand.LessThan: return 'Less than'; + case ViewFilterOperand.IsBefore: + return 'Is before'; + case ViewFilterOperand.IsAfter: + return 'Is after'; case ViewFilterOperand.Is: return 'Is'; case ViewFilterOperand.IsNot: @@ -22,6 +26,14 @@ export const getOperandLabel = ( return 'Is empty'; case ViewFilterOperand.IsNotEmpty: return 'Is not empty'; + case ViewFilterOperand.IsRelative: + return 'Is relative'; + case ViewFilterOperand.IsInPast: + return 'Is in past'; + case ViewFilterOperand.IsInFuture: + return 'Is in future'; + case ViewFilterOperand.IsToday: + return 'Is today'; default: return ''; } @@ -47,6 +59,16 @@ export const getOperandLabelShort = ( return '\u00A0> '; case ViewFilterOperand.LessThan: return '\u00A0< '; + case ViewFilterOperand.IsBefore: + return '\u00A0< '; + case ViewFilterOperand.IsAfter: + return '\u00A0> '; + case ViewFilterOperand.IsInPast: + return ': Past'; + case ViewFilterOperand.IsInFuture: + return ': Future'; + case ViewFilterOperand.IsToday: + return ': Today'; default: return ': '; } diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts index f75dca40f76c..d1066e2a5491 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts @@ -31,13 +31,23 @@ export const getOperandsForFilterType = ( ]; case 'CURRENCY': case 'NUMBER': - case 'DATE_TIME': - case 'DATE': return [ ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan, ...emptyOperands, ]; + case 'DATE_TIME': + case 'DATE': + return [ + ViewFilterOperand.Is, + ViewFilterOperand.IsRelative, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, + ViewFilterOperand.IsBefore, + ViewFilterOperand.IsAfter, + ...emptyOperands, + ]; case 'RATING': return [ ViewFilterOperand.Is, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue.ts new file mode 100644 index 000000000000..fb59e540180b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue.ts @@ -0,0 +1,28 @@ +import { + VariableDateViewFilterValueDirection, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +import { plural } from 'pluralize'; +import { capitalize } from '~/utils/string/capitalize'; +export const getRelativeDateDisplayValue = ( + relativeDate: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + } | null, +) => { + if (!relativeDate) return ''; + const { direction, amount, unit } = relativeDate; + + const directionStr = capitalize(direction.toLowerCase()); + const amountStr = direction === 'THIS' ? '' : amount; + const unitStr = amount + ? amount > 1 + ? plural(unit.toLowerCase()) + : unit.toLowerCase() + : undefined; + + return [directionStr, amountStr, unitStr] + .filter((item) => item !== undefined) + .join(' '); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts index 3b0e9f8c2c72..58cbaca25e9d 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts @@ -25,6 +25,9 @@ import { convertLessThanRatingToArrayOfRatingValues, convertRatingToRatingValue, } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; +import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue'; +import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; +import { z } from 'zod'; import { Filter } from '../../object-filter-dropdown/types/Filter'; export type ObjectDropdownFilter = Omit & { @@ -289,16 +292,19 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( (field) => field.id === rawUIFilter.fieldMetadataId, ); - const isEmptyOperand = [ + const isValuelessOperand = [ ViewFilterOperand.IsEmpty, ViewFilterOperand.IsNotEmpty, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, ].includes(rawUIFilter.operand); if (!correspondingField) { continue; } - if (!isEmptyOperand) { + if (!isValuelessOperand) { if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') { continue; } @@ -341,24 +347,31 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } break; case 'DATE': - case 'DATE_TIME': + case 'DATE_TIME': { + const resolvedFilterValue = resolveFilterValue(rawUIFilter); + const now = roundToNearestMinutes(new Date()); + const date = + resolvedFilterValue instanceof Date ? resolvedFilterValue : now; + switch (rawUIFilter.operand) { - case ViewFilterOperand.GreaterThan: + case ViewFilterOperand.IsAfter: { objectRecordFilters.push({ [correspondingField.name]: { - gte: rawUIFilter.value, + gt: date.toISOString(), } as DateFilter, }); break; - case ViewFilterOperand.LessThan: + } + case ViewFilterOperand.IsBefore: { objectRecordFilters.push({ [correspondingField.name]: { - lte: rawUIFilter.value, + lt: date.toISOString(), } as DateFilter, }); break; + } case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: + case ViewFilterOperand.IsNotEmpty: { applyEmptyFilters( rawUIFilter.operand, correspondingField, @@ -366,12 +379,99 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.definition.type, ); break; + } + case ViewFilterOperand.IsRelative: { + const dateRange = z + .object({ start: z.date(), end: z.date() }) + .safeParse(resolvedFilterValue).data; + + const defaultDateRange = resolveFilterValue({ + value: 'PAST_1_DAY', + definition: { + type: 'DATE', + }, + operand: ViewFilterOperand.IsRelative, + }); + + if (!defaultDateRange) + throw new Error('Failed to resolve default date range'); + + const { start, end } = dateRange ?? defaultDateRange; + + objectRecordFilters.push({ + and: [ + { + [correspondingField.name]: { + gte: start.toISOString(), + } as DateFilter, + }, + { + [correspondingField.name]: { + lte: end.toISOString(), + } as DateFilter, + }, + ], + }); + break; + } + case ViewFilterOperand.Is: { + const isValid = resolvedFilterValue instanceof Date; + const date = isValid ? resolvedFilterValue : now; + + objectRecordFilters.push({ + and: [ + { + [correspondingField.name]: { + lte: endOfDay(date).toISOString(), + } as DateFilter, + }, + { + [correspondingField.name]: { + gte: startOfDay(date).toISOString(), + } as DateFilter, + }, + ], + }); + break; + } + case ViewFilterOperand.IsInPast: + objectRecordFilters.push({ + [correspondingField.name]: { + lte: now.toISOString(), + } as DateFilter, + }); + break; + case ViewFilterOperand.IsInFuture: + objectRecordFilters.push({ + [correspondingField.name]: { + gte: now.toISOString(), + } as DateFilter, + }); + break; + case ViewFilterOperand.IsToday: { + objectRecordFilters.push({ + and: [ + { + [correspondingField.name]: { + lte: endOfDay(now).toISOString(), + } as DateFilter, + }, + { + [correspondingField.name]: { + gte: startOfDay(now).toISOString(), + } as DateFilter, + }, + ], + }); + break; + } default: throw new Error( - `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, + `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, // ); } break; + } case 'RATING': switch (rawUIFilter.operand) { case ViewFilterOperand.Is: @@ -446,7 +546,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } break; case 'RELATION': { - if (!isEmptyOperand) { + if (!isValuelessOperand) { try { JSON.parse(rawUIFilter.value); } catch (e) { @@ -743,7 +843,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } break; case 'SELECT': { - if (isEmptyOperand) { + if (isValuelessOperand) { applyEmptyFilters( rawUIFilter.operand, correspondingField, diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx new file mode 100644 index 000000000000..1efc985d34f6 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx @@ -0,0 +1,108 @@ +import styled from '@emotion/styled'; +import { DateTime } from 'luxon'; +import { IconChevronLeft, IconChevronRight } from 'twenty-ui'; + +import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; +import { Select } from '@/ui/input/components/Select'; +import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput'; + +import { getMonthSelectOptions } from '@/ui/input/components/internal/date/utils/getMonthSelectOptions'; +import { + MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID, + MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID, +} from './InternalDatePicker'; + +const StyledCustomDatePickerHeader = styled.div` + align-items: center; + display: flex; + justify-content: flex-end; + padding-left: ${({ theme }) => theme.spacing(2)}; + padding-right: ${({ theme }) => theme.spacing(2)}; + padding-top: ${({ theme }) => theme.spacing(2)}; + + gap: ${({ theme }) => theme.spacing(1)}; +`; + +const years = Array.from( + { length: 200 }, + (_, i) => new Date().getFullYear() + 5 - i, +).map((year) => ({ label: year.toString(), value: year })); + +type AbsoluteDatePickerHeaderProps = { + date: Date; + onChange?: (date: Date | null) => void; + onChangeMonth: (month: number) => void; + onChangeYear: (year: number) => void; + onAddMonth: () => void; + onSubtractMonth: () => void; + prevMonthButtonDisabled: boolean; + nextMonthButtonDisabled: boolean; + isDateTimeInput?: boolean; + timeZone: string; +}; + +export const AbsoluteDatePickerHeader = ({ + date, + onChange, + onChangeMonth, + onChangeYear, + onAddMonth, + onSubtractMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + isDateTimeInput, + timeZone, +}: AbsoluteDatePickerHeaderProps) => { + const endOfDayDateTimeInLocalTimezone = DateTime.now().set({ + day: date.getDate(), + month: date.getMonth() + 1, + year: date.getFullYear(), + hour: 23, + minute: 59, + second: 59, + millisecond: 999, + }); + + const endOfDayInLocalTimezone = endOfDayDateTimeInLocalTimezone.toJSDate(); + + return ( + <> + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx index a3004373d059..e7a330c81ff3 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx @@ -2,52 +2,32 @@ import styled from '@emotion/styled'; import { DateTime } from 'luxon'; import ReactDatePicker from 'react-datepicker'; import { Key } from 'ts-key-enum'; -import { - IconCalendarX, - IconChevronLeft, - IconChevronRight, - OVERLAY_BACKGROUND, -} from 'twenty-ui'; +import { IconCalendarX, OVERLAY_BACKGROUND } from 'twenty-ui'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput'; -import { Select } from '@/ui/input/components/Select'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent'; import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase'; import { isDefined } from '~/utils/isDefined'; +import { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader'; +import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader'; +import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates'; import { UserContext } from '@/users/contexts/UserContext'; +import { + VariableDateViewFilterValueDirection, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; import { useContext } from 'react'; import 'react-datepicker/dist/react-datepicker.css'; -const months = [ - { label: 'January', value: 0 }, - { label: 'February', value: 1 }, - { label: 'March', value: 2 }, - { label: 'April', value: 3 }, - { label: 'May', value: 4 }, - { label: 'June', value: 5 }, - { label: 'July', value: 6 }, - { label: 'August', value: 7 }, - { label: 'September', value: 8 }, - { label: 'October', value: 9 }, - { label: 'November', value: 10 }, - { label: 'December', value: 11 }, -]; - -const years = Array.from( - { length: 200 }, - (_, i) => new Date().getFullYear() + 5 - i, -).map((year) => ({ label: year.toString(), value: year })); - export const MONTH_AND_YEAR_DROPDOWN_ID = 'date-picker-month-and-year-dropdown'; export const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID = 'date-picker-month-and-year-dropdown-month-select'; export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID = 'date-picker-month-and-year-dropdown-year-select'; -const StyledContainer = styled.div` +const StyledContainer = styled.div<{ calendarDisabled?: boolean }>` & .react-datepicker { border-color: ${({ theme }) => theme.border.color.light}; background: transparent; @@ -207,6 +187,10 @@ const StyledContainer = styled.div` & .react-datepicker__month { margin-top: 0; + + pointer-events: ${({ calendarDisabled }) => + calendarDisabled ? 'none' : 'auto'}; + opacity: ${({ calendarDisabled }) => (calendarDisabled ? '0.5' : '1')}; } & .react-datepicker__day { @@ -288,21 +272,27 @@ const StyledButton = styled(MenuItemLeftContent)` justify-content: start; `; -const StyledCustomDatePickerHeader = styled.div` - align-items: center; - display: flex; - justify-content: flex-end; - padding-left: ${({ theme }) => theme.spacing(2)}; - padding-right: ${({ theme }) => theme.spacing(2)}; - padding-top: ${({ theme }) => theme.spacing(2)}; - - gap: ${({ theme }) => theme.spacing(1)}; -`; - type InternalDatePickerProps = { + isRelative?: boolean; date: Date | null; + relativeDate?: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + }; + highlightedDateRange?: { + start: Date; + end: Date; + }; onMouseSelect?: (date: Date | null) => void; onChange?: (date: Date | null) => void; + onRelativeDateChange?: ( + relativeDate: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + } | null, + ) => void; clearable?: boolean; isDateTimeInput?: boolean; onEnter?: (date: Date | null) => void; @@ -321,6 +311,10 @@ export const InternalDatePicker = ({ isDateTimeInput, keyboardEventsDisabled, onClear, + isRelative, + relativeDate, + onRelativeDateChange, + highlightedDateRange, }: InternalDatePickerProps) => { const internalDate = date ?? new Date(); @@ -469,15 +463,20 @@ export const InternalDatePicker = ({ const dateToUse = isDateTimeInput ? endOfDayInLocalTimezone : dateWithoutTime; + const highlightedDates = getHighlightedDates(highlightedDateRange); + + const selectedDates = isRelative ? highlightedDates : [dateToUse]; + return ( - +
( - <> - + isRelative ? ( + + ) : ( + - - - - - - - )} + ) + } onSelect={handleDateSelect} + selectsMultiple={isRelative} />
{clearable && ( diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/RelativeDatePickerHeader.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/RelativeDatePickerHeader.tsx new file mode 100644 index 000000000000..0a9328577dba --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/RelativeDatePickerHeader.tsx @@ -0,0 +1,113 @@ +import { RELATIVE_DATE_DIRECTION_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions'; +import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions'; +import { Select } from '@/ui/input/components/Select'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { + VariableDateViewFilterValueDirection, + variableDateViewFilterValuePartsSchema, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; + +const StyledContainer = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(1)}; + padding: ${({ theme }) => theme.spacing(2)}; + padding-bottom: 0; +`; + +type RelativeDatePickerHeaderProps = { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + onChange?: (value: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + }) => void; +}; + +export const RelativeDatePickerHeader = ( + props: RelativeDatePickerHeaderProps, +) => { + const [direction, setDirection] = useState(props.direction); + const [amountString, setAmountString] = useState( + props.amount ? props.amount.toString() : '', + ); + const [unit, setUnit] = useState(props.unit); + + useEffect(() => { + setAmountString(props.amount ? props.amount.toString() : ''); + setUnit(props.unit); + setDirection(props.direction); + }, [props.amount, props.unit, props.direction]); + + const textInputValue = direction === 'THIS' ? '' : amountString; + const textInputPlaceholder = direction === 'THIS' ? '-' : 'Number'; + + const isUnitPlural = props.amount && props.amount > 1 && direction !== 'THIS'; + const unitSelectOptions = RELATIVE_DATE_UNITS_SELECT_OPTIONS.map((unit) => ({ + ...unit, + label: `${unit.label}${isUnitPlural ? 's' : ''}`, + })); + + return ( + + { + setUnit(newUnit); + if (direction !== 'THIS' && props.amount === undefined) return; + props.onChange?.({ + direction, + amount: props.amount, + unit: newUnit, + }); + }} + options={unitSelectOptions} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions.ts new file mode 100644 index 000000000000..d13926719f0f --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions.ts @@ -0,0 +1,13 @@ +import { VariableDateViewFilterValueDirection } from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; + +type RelativeDateDirectionOption = { + value: VariableDateViewFilterValueDirection; + label: string; +}; + +export const RELATIVE_DATE_DIRECTION_SELECT_OPTIONS: RelativeDateDirectionOption[] = + [ + { value: 'PAST', label: 'Past' }, + { value: 'THIS', label: 'This' }, + { value: 'NEXT', label: 'Next' }, + ]; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts new file mode 100644 index 000000000000..bf65953f63bc --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts @@ -0,0 +1,13 @@ +import { VariableDateViewFilterValueUnit } from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; + +type RelativeDateUnit = { + value: VariableDateViewFilterValueUnit; + label: string; +}; + +export const RELATIVE_DATE_UNITS_SELECT_OPTIONS: RelativeDateUnit[] = [ + { value: 'DAY', label: 'Day' }, + { value: 'WEEK', label: 'Week' }, + { value: 'MONTH', label: 'Month' }, + { value: 'YEAR', label: 'Year' }, +]; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getHighlightedDates.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getHighlightedDates.ts new file mode 100644 index 000000000000..813b36996833 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getHighlightedDates.ts @@ -0,0 +1,24 @@ +import { addDays, addMonths, startOfDay, subMonths } from 'date-fns'; + +export const getHighlightedDates = (highlightedDateRange?: { + start: Date; + end: Date; +}): Date[] => { + if (!highlightedDateRange) return []; + const { start, end } = highlightedDateRange; + + const highlightedDates: Date[] = []; + const currentDate = startOfDay(new Date()); + const minDate = subMonths(currentDate, 2); + const maxDate = addMonths(currentDate, 2); + + let dateToHighlight = start < minDate ? minDate : start; + const lastDate = end > maxDate ? maxDate : end; + + while (dateToHighlight <= lastDate) { + highlightedDates.push(dateToHighlight); + dateToHighlight = addDays(dateToHighlight, 1); + } + + return highlightedDates; +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getMonthSelectOptions.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getMonthSelectOptions.ts new file mode 100644 index 000000000000..3f5e395174ee --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getMonthSelectOptions.ts @@ -0,0 +1,16 @@ +const getMonthName = (index: number): string => + new Intl.DateTimeFormat('en-US', { month: 'long' }).format( + new Date(0, index, 1), + ); + +const getMonthNames = (monthNames: string[] = []): string[] => { + if (monthNames.length === 12) return monthNames; + + return getMonthNames([...monthNames, getMonthName(monthNames.length)]); +}; + +export const getMonthSelectOptions = (): { label: string; value: number }[] => + getMonthNames().map((month, index) => ({ + label: month, + value: index, + })); diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index 45c974eca420..b8b9fe56ffa4 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -9,6 +9,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { EditableFilterChip } from '@/views/components/EditableFilterChip'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; @@ -74,7 +75,13 @@ export const EditableFilterDropdownButton = ({ const { id: fieldId, value, operand } = viewFilter; if ( !value && - ![FilterOperand.IsEmpty, FilterOperand.IsNotEmpty].includes(operand) + ![ + FilterOperand.IsEmpty, + FilterOperand.IsNotEmpty, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, + ].includes(operand) ) { deleteCombinedViewFilter(fieldId); } diff --git a/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts b/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts index 025d0085d49d..0d6446de9ea4 100644 --- a/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts +++ b/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts @@ -4,8 +4,14 @@ export enum ViewFilterOperand { IsNot = 'isNot', LessThan = 'lessThan', GreaterThan = 'greaterThan', + IsBefore = 'isBefore', + IsAfter = 'isAfter', Contains = 'contains', DoesNotContain = 'doesNotContain', IsEmpty = 'isEmpty', IsNotEmpty = 'isNotEmpty', + IsRelative = 'isRelative', + IsInPast = 'isInPast', + IsInFuture = 'isInFuture', + IsToday = 'isToday', } diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts new file mode 100644 index 000000000000..1b09bc91348b --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts @@ -0,0 +1,10 @@ +import { + VariableDateViewFilterValueDirection, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; + +export const computeVariableDateViewFilterValue = ( + direction: VariableDateViewFilterValueDirection, + amount: number | undefined, + unit: VariableDateViewFilterValueUnit, +) => `${direction}_${amount?.toString()}_${unit}`; diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts new file mode 100644 index 000000000000..da940310505c --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts @@ -0,0 +1,190 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { + addDays, + addMonths, + addWeeks, + addYears, + endOfDay, + endOfMonth, + endOfWeek, + endOfYear, + roundToNearestMinutes, + startOfDay, + startOfMonth, + startOfWeek, + startOfYear, + subDays, + subMonths, + subWeeks, + subYears, +} from 'date-fns'; + +import { z } from 'zod'; + +const variableDateViewFilterValueDirectionSchema = z.enum([ + 'NEXT', + 'THIS', + 'PAST', +]); + +export type VariableDateViewFilterValueDirection = z.infer< + typeof variableDateViewFilterValueDirectionSchema +>; + +const variableDateViewFilterValueAmountSchema = z + .union([z.coerce.number().int().positive(), z.literal('undefined')]) + .transform((val) => (val === 'undefined' ? undefined : val)); + +export const variableDateViewFilterValueUnitSchema = z.enum([ + 'DAY', + 'WEEK', + 'MONTH', + 'YEAR', +]); + +export type VariableDateViewFilterValueUnit = z.infer< + typeof variableDateViewFilterValueUnitSchema +>; + +export const variableDateViewFilterValuePartsSchema = z + .object({ + direction: variableDateViewFilterValueDirectionSchema, + amount: variableDateViewFilterValueAmountSchema, + unit: variableDateViewFilterValueUnitSchema, + }) + .refine((data) => !(data.amount === undefined && data.direction !== 'THIS'), { + message: "Amount cannot be 'undefined' unless direction is 'THIS'", + }); + +const variableDateViewFilterValueSchema = z.string().transform((value) => { + const [direction, amount, unit] = value.split('_'); + + return variableDateViewFilterValuePartsSchema.parse({ + direction, + amount, + unit, + }); +}); + +const addUnit = ( + date: Date, + amount: number, + unit: VariableDateViewFilterValueUnit, +) => { + switch (unit) { + case 'DAY': + return addDays(date, amount); + case 'WEEK': + return addWeeks(date, amount); + case 'MONTH': + return addMonths(date, amount); + case 'YEAR': + return addYears(date, amount); + } +}; + +const subUnit = ( + date: Date, + amount: number, + unit: VariableDateViewFilterValueUnit, +) => { + switch (unit) { + case 'DAY': + return subDays(date, amount); + case 'WEEK': + return subWeeks(date, amount); + case 'MONTH': + return subMonths(date, amount); + case 'YEAR': + return subYears(date, amount); + } +}; + +const startOfUnit = (date: Date, unit: VariableDateViewFilterValueUnit) => { + switch (unit) { + case 'DAY': + return startOfDay(date); + case 'WEEK': + return startOfWeek(date); + case 'MONTH': + return startOfMonth(date); + case 'YEAR': + return startOfYear(date); + } +}; + +const endOfUnit = (date: Date, unit: VariableDateViewFilterValueUnit) => { + switch (unit) { + case 'DAY': + return endOfDay(date); + case 'WEEK': + return endOfWeek(date); + case 'MONTH': + return endOfMonth(date); + case 'YEAR': + return endOfYear(date); + } +}; + +const resolveVariableDateViewFilterValueFromRelativeDate = (relativeDate: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; +}) => { + const { direction, amount, unit } = relativeDate; + const now = roundToNearestMinutes(new Date()); + + switch (direction) { + case 'NEXT': + if (amount === undefined) throw new Error('Amount is required'); + return { + start: now, + end: addUnit(now, amount, unit), + ...relativeDate, + }; + case 'PAST': + if (amount === undefined) throw new Error('Amount is required'); + return { + start: subUnit(now, amount, unit), + end: now, + ...relativeDate, + }; + case 'THIS': + return { + start: startOfUnit(now, unit), + end: endOfUnit(now, unit), + ...relativeDate, + }; + } +}; + +const resolveVariableDateViewFilterValue = (value?: string | null) => { + if (!value) return null; + + const relativeDate = variableDateViewFilterValueSchema.parse(value); + return resolveVariableDateViewFilterValueFromRelativeDate(relativeDate); +}; + +export type ResolvedDateViewFilterValue = + O extends ViewFilterOperand.IsRelative + ? ReturnType + : Date | null; + +type PartialViewFilter = Pick< + ViewFilter, + 'value' +> & { operand: O }; + +export const resolveDateViewFilterValue = ( + viewFilter: PartialViewFilter, +): ResolvedDateViewFilterValue => { + if (!viewFilter.value) return null; + + if (viewFilter.operand === ViewFilterOperand.IsRelative) { + return resolveVariableDateViewFilterValue( + viewFilter.value, + ) as ResolvedDateViewFilterValue; + } + return new Date(viewFilter.value) as ResolvedDateViewFilterValue; +}; diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts new file mode 100644 index 000000000000..310666488f1b --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts @@ -0,0 +1,42 @@ +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { resolveNumberViewFilterValue } from '@/views/utils/view-filter-value/resolveNumberViewFilterValue'; +import { + resolveDateViewFilterValue, + ResolvedDateViewFilterValue, +} from './resolveDateViewFilterValue'; + +type ResolvedFilterValue< + T extends FilterType, + O extends ViewFilterOperand, +> = T extends 'DATE' | 'DATE_TIME' + ? ResolvedDateViewFilterValue + : T extends 'NUMBER' + ? ReturnType + : string; + +type PartialFilter = Pick< + Filter, + 'value' +> & { + definition: { type: T }; + operand: O; +}; + +export const resolveFilterValue = < + T extends FilterType, + O extends ViewFilterOperand, +>( + filter: PartialFilter, +) => { + switch (filter.definition.type) { + case 'DATE': + case 'DATE_TIME': + return resolveDateViewFilterValue(filter) as ResolvedFilterValue; + case 'NUMBER': + return resolveNumberViewFilterValue(filter) as ResolvedFilterValue; + default: + return filter.value as ResolvedFilterValue; + } +}; diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts new file mode 100644 index 000000000000..4e26ca096332 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts @@ -0,0 +1,7 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; + +export const resolveNumberViewFilterValue = ( + viewFilter: Pick, +) => { + return viewFilter.value === '' ? null : +viewFilter.value; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index 157187fd0bd2..d83667211bd0 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -62,9 +62,9 @@ export class GraphqlQueryParser { return queryBuilder; } - private checkForDeletedAtFilter( + private checkForDeletedAtFilter = ( filter: FindOptionsWhere | FindOptionsWhere[], - ): boolean { + ): boolean => { if (Array.isArray(filter)) { return filter.some((subFilter) => this.checkForDeletedAtFilter(subFilter), @@ -86,7 +86,7 @@ export class GraphqlQueryParser { } return false; - } + }; applyOrderToBuilder( queryBuilder: SelectQueryBuilder, 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 149171e6eb7f..3e45fee95d60 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,18 +1,18 @@ 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 { VIEW_FILTER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; -import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; -import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-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'; -import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; -import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; -import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; +import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; +import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; +import { VIEW_FILTER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.viewFilter, diff --git a/yarn.lock b/yarn.lock index 293917a2211f..46889dbdb463 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17310,6 +17310,13 @@ __metadata: languageName: node linkType: hard +"@types/pluralize@npm:^0.0.33": + version: 0.0.33 + resolution: "@types/pluralize@npm:0.0.33" + checksum: 10c0/24899caf85b79dd291a6b6e9b9f3b67b452b18d578d0ac0d531a705bf5ee0361d9386ea1f8532c64de9e22c6e9606c5497787bb5e31bd299c487980436c59785 + languageName: node + linkType: hard + "@types/pretty-hrtime@npm:^1.0.0": version: 1.0.3 resolution: "@types/pretty-hrtime@npm:1.0.3" @@ -47693,6 +47700,7 @@ __metadata: "@types/passport-google-oauth20": "npm:^2.0.11" "@types/passport-jwt": "npm:^3.0.8" "@types/passport-microsoft": "npm:^1.0.3" + "@types/pluralize": "npm:^0.0.33" "@types/react": "npm:^18.2.39" "@types/react-datepicker": "npm:^6.2.0" "@types/react-dom": "npm:^18.2.15" From 9f477129e23b907c31412505fd9319f200d8a859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:42:35 +0200 Subject: [PATCH 006/115] Fix use object metadata item (#7297) Fixes a bug which happened during workspace creation and remove duplicated code --- .../ObjectMetadataItemsLoadEffect.tsx | 8 ++------ .../hooks/useObjectMetadataItem.ts | 18 ++---------------- .../src/testing/decorators/PageDecorator.tsx | 9 +++------ 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx index cd3f0e2c2b06..ecc0772b85cd 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx @@ -16,7 +16,7 @@ export const ObjectMetadataItemsLoadEffect = () => { const currentWorkspace = useRecoilValue(currentWorkspaceState); const isLoggedIn = useIsLogged(); - const { objectMetadataItems: newObjectMetadataItems, loading } = + const { objectMetadataItems: newObjectMetadataItems } = useFindManyObjectMetadataItems({ skip: !isLoggedIn, }); @@ -31,16 +31,12 @@ export const ObjectMetadataItemsLoadEffect = () => { currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active ? generatedMockObjectMetadataItems : newObjectMetadataItems; - if ( - !loading && - !isDeeplyEqual(objectMetadataItems, toSetObjectMetadataItems) - ) { + if (!isDeeplyEqual(objectMetadataItems, toSetObjectMetadataItems)) { setObjectMetadataItems(toSetObjectMetadataItems); } }, [ currentUser, currentWorkspace?.activationStatus, - loading, newObjectMetadataItems, objectMetadataItems, setObjectMetadataItems, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index fb08e4c96a2c..0a8d3e8600a0 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -1,37 +1,23 @@ import { useRecoilValue } from 'recoil'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { isDefined } from '~/utils/isDefined'; -import { WorkspaceActivationStatus } from '~/generated/graphql'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier'; export const useObjectMetadataItem = ({ objectNameSingular, }: ObjectMetadataItemIdentifier) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState); - - let objectMetadataItem = useRecoilValue( + const objectMetadataItem = useRecoilValue( objectMetadataItemFamilySelector({ objectName: objectNameSingular, objectNameType: 'singular', }), ); - let objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - if (currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active) { - objectMetadataItem = - generatedMockObjectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === objectNameSingular, - ) ?? null; - objectMetadataItems = generatedMockObjectMetadataItems; - } + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); if (!isDefined(objectMetadataItem)) { throw new ObjectMetadataItemNotFoundError( diff --git a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx index 2381241c4a06..7125e4326dc6 100644 --- a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx @@ -12,7 +12,6 @@ import { import { RecoilRoot } from 'recoil'; import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; -import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; import { ApolloMetadataClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { UserProviderEffect } from '@/users/components/UserProviderEffect'; @@ -77,11 +76,9 @@ const Providers = () => { - - - - - + + + From e28d8dd95230b05e58c13adbb6b5d364945f6b16 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:52:06 +0200 Subject: [PATCH 007/115] Fix standardId issues with phones field migration (#7294) Co-authored-by: Weiko --- .../0-30-migrate-phone-fields-to-phones.command.ts | 10 ++++++++++ .../constants/standard-field-ids.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts index 60aacf2605c3..6560bb9625dc 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts @@ -168,6 +168,16 @@ export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRu name: 'phones', } satisfies CreateFieldInput); + // StandardId and isCustom are not exposed in CreateFieldInput + await this.metadataDataSource.query( + `UPDATE "metadata"."fieldMetadata" SET "standardId" = $1, "isCustom" = $2 where "id"=$3`, + [ + PERSON_STANDARD_FIELD_IDS.phones, + 'false', + standardPersonPhonesField.id, + ], + ); + await this.viewService.removeFieldFromViews({ workspaceId: workspaceId, fieldId: standardPersonPhonesField.id, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 8a19d49e4b2c..1fa9a7f41de1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -310,7 +310,7 @@ export const PERSON_STANDARD_FIELD_IDS = { xLink: '20202020-8fc2-487c-b84a-55a99b145cfd', jobTitle: '20202020-b0d0-415a-bef9-640a26dacd9b', phone: '20202020-4564-4b8b-a09f-05445f2e0bce', - phones: '34becd3e-3c51-43fa-8b6e-af39e29368ab', + phones: '20202020-0638-448e-8825-439134618022', city: '20202020-5243-4ffb-afc5-2c675da41346', avatarUrl: '20202020-b8a6-40df-961c-373dc5d2ec21', position: '20202020-fcd5-4231-aff5-fff583eaa0b1', From 942281f4b04c06188344a5e58a6b67bcf45ef230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:20:15 +0200 Subject: [PATCH 008/115] Fix email migration (#7298) Checks if person standard email field exists before running the migration. --- ...-migrate-email-fields-to-emails.command.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts index 87ff3e5b0f64..2e4969297a6b 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts @@ -305,14 +305,6 @@ export class MigrateEmailFieldsToEmailsCommand extends ActiveWorkspacesCommandRu ) { this.logger.log(`Migrating person email field of type EMAIL to EMAILS`); - await this.migrateDataWithinTable({ - sourceColumnName: 'email', - targetColumnName: 'emailsPrimaryEmail', - tableName: 'person', - workspaceQueryRunner, - dataSourceMetadata, - }); - const personEmailFieldMetadata = await this.fieldMetadataRepository.findOne( { where: { @@ -322,6 +314,22 @@ export class MigrateEmailFieldsToEmailsCommand extends ActiveWorkspacesCommandRu }, ); + if (!personEmailFieldMetadata) { + this.logger.log( + `Could not find person email field with standardId ${PERSON_STANDARD_FIELD_IDS.email}, skipping migration`, + ); + + return; + } + + await this.migrateDataWithinTable({ + sourceColumnName: 'email', + targetColumnName: 'emailsPrimaryEmail', + tableName: 'person', + workspaceQueryRunner, + dataSourceMetadata, + }); + if (personEmailFieldMetadata) { await this.fieldMetadataService.deleteOneField( { From ae6777fab87aac4e3f83f00fd64a432f6f3aa8d6 Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 27 Sep 2024 19:10:18 +0200 Subject: [PATCH 009/115] Fix viewFilter operand for dateTime fields (#7306) --- ...ew-filter-operand-for-date-time.command.ts | 110 ++++++++++++++++++ .../0-30/0-30-upgrade-version.command.ts | 7 ++ .../0-30/0-30-upgrade-version.module.ts | 2 + 3 files changed, 119 insertions(+) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts new file mode 100644 index 000000000000..f2e98d6600a7 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts @@ -0,0 +1,110 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { Any, Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; + +@Command({ + name: 'upgrade-0.30:fix-view-filter-operand-for-date-time', + description: 'Fix the view filter operand for date time fields', +}) +export class FixViewFilterOperandForDateTimeCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + private readonly dataSourceService: DataSourceService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + _options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + for (const workspaceId of workspaceIds) { + try { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( + workspaceId, + ); + + if (!dataSourceMetadata) { + throw new Error( + `Could not find dataSourceMetadata for workspace ${workspaceId}`, + ); + } + + const viewFilterRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'viewFilter', + ); + + const dateTimeFieldMetadata = await this.fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.DATE_TIME, + }, + }); + + const dateTimeFieldMetadataIds = dateTimeFieldMetadata.map( + (fieldMetadata) => fieldMetadata.id, + ); + + const lessThanUpdatedResult = await viewFilterRepository.update( + { + operand: 'lessThan', + fieldMetadataId: Any(dateTimeFieldMetadataIds), + }, + { + operand: 'isBefore', + }, + ); + + const greaterThanUpdatedResult = await viewFilterRepository.update( + { + operand: 'greaterThan', + fieldMetadataId: Any(dateTimeFieldMetadataIds), + }, + { + operand: 'isAfter', + }, + ); + + this.logger.log( + `Updated ${(lessThanUpdatedResult.affected ?? 0) + (greaterThanUpdatedResult.affected ?? 0)} view filters for workspace ${workspaceId}`, + ); + } catch (error) { + this.logger.log( + chalk.red( + `Error running command for workspace ${workspaceId}: ${error}`, + ), + ); + continue; + } finally { + this.logger.log( + chalk.green(`Finished running command for workspace ${workspaceId}.`), + ); + } + + this.logger.log(chalk.green(`Command completed!`)); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts index 144746f82933..25ff077ca5e1 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; import { FixEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-email-field-migration.command'; +import { FixViewFilterOperandForDateTimeCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command'; import { MigrateEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command'; import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command'; import { SetStaleMessageSyncBackToPendingCommand } from 'src/database/commands/upgrade-version/0-30/0-30-set-stale-message-sync-back-to-pending'; @@ -28,6 +29,7 @@ export class UpgradeTo0_30Command extends ActiveWorkspacesCommandRunner { private readonly setStaleMessageSyncBackToPendingCommand: SetStaleMessageSyncBackToPendingCommand, private readonly fixEmailFieldsToEmailsCommand: FixEmailFieldsToEmailsCommand, private readonly migratePhoneFieldsToPhones: MigratePhoneFieldsToPhonesCommand, + private readonly fixViewFilterOperandForDateTimeCommand: FixViewFilterOperandForDateTimeCommand, ) { super(workspaceRepository); } @@ -65,5 +67,10 @@ export class UpgradeTo0_30Command extends ActiveWorkspacesCommandRunner { options, workspaceIds, ); + await this.fixViewFilterOperandForDateTimeCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); } } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts index 3a7c51c8df6f..191bad9352cb 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { FixEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-email-field-migration.command'; +import { FixViewFilterOperandForDateTimeCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command'; import { MigrateEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command'; import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command'; import { SetStaleMessageSyncBackToPendingCommand } from 'src/database/commands/upgrade-version/0-30/0-30-set-stale-message-sync-back-to-pending'; @@ -36,6 +37,7 @@ import { ViewModule } from 'src/modules/view/view.module'; SetStaleMessageSyncBackToPendingCommand, FixEmailFieldsToEmailsCommand, MigratePhoneFieldsToPhonesCommand, + FixViewFilterOperandForDateTimeCommand, ], }) export class UpgradeTo0_30CommandModule {} From e4959ad53412fded1d88f3e3400f46402de894b4 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 27 Sep 2024 19:10:26 +0200 Subject: [PATCH 010/115] Add 0.30 release notes (#7300) In this PR: - update your environment variables to default `CACHE_STORAGE_TYPE` to `redis` and `MESSAGE_QUEUE_TYPE` to `bull-mq` - add redis container to our default docker-compose - add `REDIS_HOST` and `REDIS_PORT` to docker-compose yaml - add upgrade instructions --- packages/twenty-docker/.env.example | 4 ++-- packages/twenty-docker/docker-compose.yml | 14 ++++++++++-- packages/twenty-emails/package.json | 2 +- packages/twenty-front/package.json | 2 +- packages/twenty-server/package.json | 2 +- .../environment/environment-variables.ts | 4 ++-- packages/twenty-ui/package.json | 2 +- packages/twenty-website/package.json | 2 +- .../self-hosting/self-hosting-var.mdx | 4 ++-- .../developers/self-hosting/upgrade-guide.mdx | 22 +++++++++++++++++++ 10 files changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/twenty-docker/.env.example b/packages/twenty-docker/.env.example index c25482220fce..59d8d03f93a7 100644 --- a/packages/twenty-docker/.env.example +++ b/packages/twenty-docker/.env.example @@ -5,6 +5,8 @@ TAG=latest PG_DATABASE_HOST=db:5432 SERVER_URL=http://localhost:3000 +# REDIS_HOST=redis +# REDIS_PORT=6379 # Use openssl rand -base64 32 for each secret # ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access @@ -19,5 +21,3 @@ STORAGE_TYPE=local # STORAGE_S3_REGION=eu-west3 # STORAGE_S3_NAME=my-bucket # STORAGE_S3_ENDPOINT= - -MESSAGE_QUEUE_TYPE=pg-boss diff --git a/packages/twenty-docker/docker-compose.yml b/packages/twenty-docker/docker-compose.yml index 553d8ca6c9fa..b2efc1a168e4 100644 --- a/packages/twenty-docker/docker-compose.yml +++ b/packages/twenty-docker/docker-compose.yml @@ -25,7 +25,8 @@ services: PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default SERVER_URL: ${SERVER_URL} FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL} - MESSAGE_QUEUE_TYPE: ${MESSAGE_QUEUE_TYPE} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_HOST: ${REDIS_HOST:-redis} ENABLE_DB_MIGRATIONS: "true" @@ -34,6 +35,7 @@ services: STORAGE_S3_REGION: ${STORAGE_S3_REGION} STORAGE_S3_NAME: ${STORAGE_S3_NAME} STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} + ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET} LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET} REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET} @@ -57,7 +59,8 @@ services: PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default SERVER_URL: ${SERVER_URL} FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL} - MESSAGE_QUEUE_TYPE: ${MESSAGE_QUEUE_TYPE} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_HOST: ${REDIS_HOST:-redis} ENABLE_DB_MIGRATIONS: "false" # it already runs on the server @@ -65,6 +68,7 @@ services: STORAGE_S3_REGION: ${STORAGE_S3_REGION} STORAGE_S3_NAME: ${STORAGE_S3_NAME} STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} + ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET} LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET} REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET} @@ -89,6 +93,12 @@ services: retries: 10 restart: always + redis: + image: redis + ports: + - "6379:6379" + restart: always + volumes: docker-data: db-data: diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json index 36af44f3da6e..dc0b89328e3c 100644 --- a/packages/twenty-emails/package.json +++ b/packages/twenty-emails/package.json @@ -1,6 +1,6 @@ { "name": "twenty-emails", - "version": "0.24.2", + "version": "0.30.0", "description": "", "author": "", "private": true, diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 434934ed719d..e1a632247380 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -1,6 +1,6 @@ { "name": "twenty-front", - "version": "0.24.2", + "version": "0.30.0", "private": true, "type": "module", "scripts": { diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 1a60930c4a18..d77b2faf4d78 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -1,6 +1,6 @@ { "name": "twenty-server", - "version": "0.24.2", + "version": "0.30.0", "description": "", "author": "", "private": true, 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 4e30e84bd83e..cb5b2fbe2822 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 @@ -391,7 +391,7 @@ export class EnvironmentVariables { @CastToBoolean() MESSAGING_PROVIDER_GMAIL_ENABLED = false; - MESSAGE_QUEUE_TYPE: string = MessageQueueDriverType.Sync; + MESSAGE_QUEUE_TYPE: string = MessageQueueDriverType.BullMQ; EMAIL_FROM_ADDRESS = 'noreply@yourdomain.com'; @@ -426,7 +426,7 @@ export class EnvironmentVariables { @CastToPositiveNumber() API_RATE_LIMITING_LIMIT = 500; - CACHE_STORAGE_TYPE: CacheStorageType = CacheStorageType.Memory; + CACHE_STORAGE_TYPE: CacheStorageType = CacheStorageType.Redis; @CastToPositiveNumber() CACHE_STORAGE_TTL: number = 3600 * 24 * 7; diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index 132d5b99edaf..b004cba081fc 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -1,6 +1,6 @@ { "name": "twenty-ui", - "version": "0.24.2", + "version": "0.30.0", "type": "module", "main": "./src/index.ts", "exports": { diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json index 3219867baf33..c235f812a142 100644 --- a/packages/twenty-website/package.json +++ b/packages/twenty-website/package.json @@ -1,6 +1,6 @@ { "name": "twenty-website", - "version": "0.24.2", + "version": "0.30.0", "private": true, "scripts": { "nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js", diff --git a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx index d5219e511fba..6eb28b045504 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx @@ -41,7 +41,7 @@ yarn command:prod cron:calendar:calendar-event-list-fetch ['FRONT_BASE_URL', 'http://localhost:3001', 'Url to the hosted frontend'], ['SERVER_URL', 'http://localhost:3000', 'Url to the hosted server'], ['PORT', '3000', 'Port'], - ['CACHE_STORAGE_TYPE', 'memory', 'Cache type (memory, redis...)'], + ['CACHE_STORAGE_TYPE', 'redis', 'Cache type (memory, redis...)'], ['CACHE_STORAGE_TTL', '3600 * 24 * 7', 'Cache TTL in seconds'] ]}> @@ -162,7 +162,7 @@ yarn command:prod cron:calendar:calendar-event-list-fetch ### Message Queue ### Logging diff --git a/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx b/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx index d22c3384f0dd..8a71df02a433 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx @@ -58,4 +58,26 @@ yarn command:prod upgrade-0.24 The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas) The `yarn command:prod upgrade-0.24` takes care of the data migration of all workspaces. +# v0.24.0 to v0.30.0 + +Upgrade your Twenty instance to use v0.30.0 image + +**Breaking change**: +To enhance performances, Twenty now requires redis cache to be configured. We have updated our [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) to reflect this. +Make sure to update your configuration and to update your environment variables accordingly: +``` +REDIS_HOST={your-redis-host} +REDIS_PORT={your-redis-port} +CACHE_STORAGE_TYPE=redis +``` + +**Schema and data migration**: +``` +yarn database:migrate:prod +yarn command:prod upgrade-0.30 +``` + +The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas) +The `yarn command:prod upgrade-30` takes care of the data migration of all workspaces. + From c2a8cd0a2f25325e7e336b839ed6fae60e5b0bfa Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sat, 28 Sep 2024 16:11:10 +0200 Subject: [PATCH 011/115] Support Emails and Phones in Spreadsheet import (#7312) This is a fast follow on v0.30 release: - removing phone (deprecated PHONE field type) search from command menu. I could have replaced it by a phone (PHONES field type) search but as we are about to release the new search in v0.31 it does not seem to worse the investment - supporting EMAILS and PHONES field types in spreadsheet import Note: while working on Spreadsheet import I found the code quite complex and with areas having duplicated code. It does not seem to be a high priority as I was able to maintain it at a low cost but it's not a peaceful code surface to navigate! --- .../command-menu/components/CommandMenu.tsx | 1 - .../constants/CompositeFieldImportLabels.ts | 9 ++++ .../hooks/useBuildAvailableFieldsForImport.ts | 37 ++++++++++++++ .../buildRecordFromImportedStructuredRow.ts | 48 +++++++++++++++++-- .../utils/sanitizeRecordInput.ts | 2 + 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 2b7d13f8c52b..1c238462c55f 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -179,7 +179,6 @@ export const CommandMenu = () => { 'emails', ['primaryEmail'], ), - { phone: { ilike: `%${commandMenuSearch}%` } }, ]) : undefined, limit: 3, diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts index 35a697bc116d..030601f241bc 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts @@ -1,8 +1,10 @@ import { FieldAddressValue, FieldCurrencyValue, + FieldEmailsValue, FieldFullNameValue, FieldLinksValue, + FieldPhonesValue, } from '@/object-record/record-field/types/FieldMetadata'; import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -30,6 +32,13 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = { primaryLinkUrlLabel: 'Link URL', primaryLinkLabelLabel: 'Link Label', } satisfies Partial>, + [FieldMetadataType.Emails]: { + primaryEmailLabel: 'Email', + } satisfies Partial>, + [FieldMetadataType.Phones]: { + primaryPhoneCountryCodeLabel: 'Phone country code', + primaryPhoneNumberLabel: 'Phone number', + } satisfies Partial>, [FieldMetadataType.Actor]: { sourceLabel: 'Source', }, diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts index 7e4edf83926a..260a223f1791 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts @@ -15,6 +15,7 @@ export const useBuildAvailableFieldsForImport = () => { ) => { const availableFieldsForImport: AvailableFieldForImport[] = []; + // Todo: refactor this to avoid this else if syntax with duplicated code for (const fieldMetadataItem of fieldMetadataItems) { if (fieldMetadataItem.type === FieldMetadataType.FullName) { const { firstNameLabel, lastNameLabel } = @@ -155,6 +156,42 @@ export const useBuildAvailableFieldsForImport = () => { fieldMetadataItem.label, ), }); + } else if (fieldMetadataItem.type === FieldMetadataType.Emails) { + Object.entries( + COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.Emails], + ).forEach(([_, fieldLabel]) => { + availableFieldsForImport.push({ + icon: getIcon(fieldMetadataItem.icon), + label: `${fieldLabel} (${fieldMetadataItem.label})`, + key: `${fieldLabel} (${fieldMetadataItem.name})`, + fieldType: { + type: 'input', + }, + fieldValidationDefinitions: + getSpreadSheetFieldValidationDefinitions( + fieldMetadataItem.type, + `${fieldLabel} (${fieldMetadataItem.label})`, + ), + }); + }); + } else if (fieldMetadataItem.type === FieldMetadataType.Phones) { + Object.entries( + COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.Phones], + ).forEach(([_, fieldLabel]) => { + availableFieldsForImport.push({ + icon: getIcon(fieldMetadataItem.icon), + label: `${fieldLabel} (${fieldMetadataItem.label})`, + key: `${fieldLabel} (${fieldMetadataItem.name})`, + fieldType: { + type: 'input', + }, + fieldValidationDefinitions: + getSpreadSheetFieldValidationDefinitions( + fieldMetadataItem.type, + `${fieldLabel} (${fieldMetadataItem.label})`, + ), + }); + }); } else { availableFieldsForImport.push({ icon: getIcon(fieldMetadataItem.icon), diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts index 5ddbe06096b5..68e641656660 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts @@ -1,7 +1,9 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldAddressValue, + FieldEmailsValue, FieldLinksValue, + FieldPhonesValue, } from '@/object-record/record-field/types/FieldMetadata'; import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels'; import { ImportedStructuredRow } from '@/spreadsheet-import/types'; @@ -31,6 +33,8 @@ export const buildRecordFromImportedStructuredRow = ( CURRENCY: { amountMicrosLabel, currencyCodeLabel }, FULL_NAME: { firstNameLabel, lastNameLabel }, LINKS: { primaryLinkLabelLabel, primaryLinkUrlLabel }, + EMAILS: { primaryEmailLabel }, + PHONES: { primaryPhoneNumberLabel, primaryPhoneCountryCodeLabel }, } = COMPOSITE_FIELD_IMPORT_LABELS; for (const field of fields) { @@ -129,14 +133,48 @@ export const buildRecordFromImportedStructuredRow = ( } break; } - case FieldMetadataType.Link: - if (importedFieldValue !== undefined) { + case FieldMetadataType.Phones: { + if ( + isDefined( + importedStructuredRow[ + `${primaryPhoneCountryCodeLabel} (${field.name})` + ] || + importedStructuredRow[ + `${primaryPhoneNumberLabel} (${field.name})` + ], + ) + ) { recordToBuild[field.name] = { - label: field.name, - url: importedFieldValue || null, - }; + primaryPhoneCountryCode: castToString( + importedStructuredRow[ + `${primaryPhoneCountryCodeLabel} (${field.name})` + ], + ), + primaryPhoneNumber: castToString( + importedStructuredRow[ + `${primaryPhoneNumberLabel} (${field.name})` + ], + ), + additionalPhones: null, + } satisfies FieldPhonesValue; } break; + } + case FieldMetadataType.Emails: { + if ( + isDefined( + importedStructuredRow[`${primaryEmailLabel} (${field.name})`], + ) + ) { + recordToBuild[field.name] = { + primaryEmail: castToString( + importedStructuredRow[`${primaryEmailLabel} (${field.name})`], + ), + additionalEmails: null, + } satisfies FieldEmailsValue; + } + break; + } case FieldMetadataType.Relation: if ( isDefined(importedFieldValue) && diff --git a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts index cc3fb4ba46fa..c760351487a7 100644 --- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts @@ -50,6 +50,8 @@ export const sanitizeRecordInput = ({ return undefined; } + // Todo: we should check that the fieldValue is a valid value + // (e.g. a string for a string field, following the right composite structure for composite fields) return [fieldName, fieldValue]; }) .filter(isDefined), From b5fff7f23acd408061c6494ff72b0ec4879f4c7e Mon Sep 17 00:00:00 2001 From: Lucas zapico <12178702+LucasZapico@users.noreply.github.com> Date: Sun, 29 Sep 2024 02:33:45 -0700 Subject: [PATCH 012/115] docs: enhance localhost documentation with REST API URL (#7317) - Add callout for local REST API URL alongside the GraphQL API URL. - This change aims to reduce confusion and complexity for the self-hosted community. **Associated Issue - "(Docs) Enhance local hosting docs with reference to the REST API URL as well as the Graphql API URL [#7316]"** --- .../twenty-website/src/content/developers/local-setup.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/twenty-website/src/content/developers/local-setup.mdx b/packages/twenty-website/src/content/developers/local-setup.mdx index 874edf3c2e32..5edcd8affb85 100644 --- a/packages/twenty-website/src/content/developers/local-setup.mdx +++ b/packages/twenty-website/src/content/developers/local-setup.mdx @@ -186,8 +186,9 @@ Alternatively, you can start both applications at once: npx nx start ``` -Twenty's server will be up and running at [http://localhost:3000/graphql](http://localhost:3000/graphql). -Twenty's frontend will be running at [http://localhost:3001](http://localhost:3001). Just login using the seeded demo account: `tim@apple.dev` (password: `Applecar2025`) to start using Twenty. +Twenty's server will be up and running at [http://localhost:3000](http://localhost:3000). The GraphQL API can be accessed at [http://localhost:3000/graphql](http://localhost:3000/graphql), and the REST API can be reached at [http://localhost:3000/rest](http://localhost:3000/rest). + +Twenty's frontend will be running at [http://localhost:3001](http://localhost:3001). Just log in using the seeded demo account: `tim@apple.dev` (password: `Applecar2025`) to start using Twenty. ## Troubleshooting From dd24662590209e3dfbdfbd6927b523e702208b7f Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:15:57 +0530 Subject: [PATCH 013/115] Remove extra Billing title (#7309) fixes #7305 --- .../twenty-front/src/pages/settings/SettingsBilling.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx index ee057b7114a7..5fc2efd2e416 100644 --- a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx @@ -1,8 +1,6 @@ -import styled from '@emotion/styled'; import { useState } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { - H1Title, H2Title, IconCalendarEvent, IconCircleX, @@ -34,10 +32,6 @@ import { } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; -const StyledH1Title = styled(H1Title)` - margin-bottom: 0; -`; - type SwitchInfo = { newInterval: SubscriptionInterval; to: string; @@ -154,7 +148,6 @@ export const SettingsBilling = () => { ]} > - {displayPaymentFailInfo && ( Date: Mon, 30 Sep 2024 10:50:42 +0200 Subject: [PATCH 014/115] Remove useless hook call (#7278) Hook is no longer used after #7236 --- .../components/SettingsServerlessFunctionsTable.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTable.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTable.tsx index d34186b3f738..93785116af7e 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTable.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTable.tsx @@ -1,7 +1,6 @@ import { SettingsServerlessFunctionsFieldItemTableRow } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsFieldItemTableRow'; import { SettingsServerlessFunctionsTableEmpty } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty'; import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions'; -import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Table } from '@/ui/layout/table/components/Table'; @@ -10,7 +9,6 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import styled from '@emotion/styled'; import { ServerlessFunction } from '~/generated-metadata/graphql'; -import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; const StyledTableRow = styled(TableRow)` grid-template-columns: 312px 132px 68px; @@ -23,10 +21,6 @@ const StyledTableBody = styled(TableBody)` export const SettingsServerlessFunctionsTable = () => { const { serverlessFunctions } = useGetManyServerlessFunctions(); - useHotkeyScopeOnMount( - SettingsServerlessFunctionHotkeyScope.ServerlessFunction, - ); - return ( <> {serverlessFunctions.length ? ( From 04adcbc521794702d193c4348b2d4d831a85a5ba Mon Sep 17 00:00:00 2001 From: Vardhaman Bhandari <97441447+Vardhaman619@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:53:55 +0530 Subject: [PATCH 015/115] Fix icon resizing issue for Notes and Tasks (#7318) This pull request addresses a [resizing issue with the Notes and Tasks icons](https://github.com/twentyhq/twenty/issues/7282). Previously, the icons would change size based on the length of the title or when the column width was adjusted, leading to inconsistent UI behavior. This update ensures that the icons maintain a stable size, enhancing the overall user experience. Solves Issue : #7282 [twenty-icon-sizing-issue.webm](https://github.com/user-attachments/assets/3ef59592-4dfb-463e-bc7b-a803ee105211) --- packages/twenty-ui/src/display/chip/components/Chip.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/twenty-ui/src/display/chip/components/Chip.tsx b/packages/twenty-ui/src/display/chip/components/Chip.tsx index 02d7e2e61309..8ff16e9a9db1 100644 --- a/packages/twenty-ui/src/display/chip/components/Chip.tsx +++ b/packages/twenty-ui/src/display/chip/components/Chip.tsx @@ -111,6 +111,10 @@ const StyledContainer = withTheme(styled.div< border-radius: ${({ theme, variant }) => variant === ChipVariant.Rounded ? '50px' : theme.border.radius.sm}; + + & > svg { + flex-shrink: 0; + } `); export const Chip = ({ From 5d1208f8af3f16bc5147356b53f529b66ecb9a1e Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Mon, 30 Sep 2024 11:24:57 +0200 Subject: [PATCH 016/115] Set default zoom to workflows (#7331) ## Improvements - This PR calls `fitView` when the Reactflow component inits. It tries to fit the flow in a view with a fixed min and max zoom. - Every time the WorkflowDiagramCanvas is rendered for a different `workflowVersionId`, the `fitView` will be re-called. This is implemented with a React `key`. - The canvas will be re-rendered when an activated/deactivated version is updated (and a new draft version is created.) - It will also be re-rendered when the user selects another workflow version and this doesn't cause the `WorkflowDiagramCanvas` component to unmount. It happens if the user wants to go the previous or next workflow or workflow version. ## Previous Behavior ![CleanShot 2024-09-30 at 10 32 06@2x](https://github.com/user-attachments/assets/ea43cd43-8c9c-491c-a535-8cca9168fb22) ## New Behavior ![CleanShot 2024-09-30 at 10 26 47@2x](https://github.com/user-attachments/assets/7bfb91b2-1782-47a1-ab5a-3eaa9f1be923) https://github.com/user-attachments/assets/cb73f456-58b1-49c3-bd31-a1650810e9dd ## Notes Closes #7047 This PR is a simplification of #7151. We'll have to improve the way we manage zoom in another PR. --- .../workflow/components/WorkflowDiagramCanvas.tsx | 10 ++++++++++ .../components/WorkflowDiagramCreateStepNode.tsx | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx index 83b9c0c67ad3..43a494b9456d 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx @@ -17,6 +17,7 @@ import { applyNodeChanges, Background, EdgeChange, + FitViewOptions, NodeChange, ReactFlow, } from '@xyflow/react'; @@ -32,6 +33,11 @@ const StyledStatusTagContainer = styled.div` padding: ${({ theme }) => theme.spacing(2)}; `; +const defaultFitViewOptions: FitViewOptions = { + minZoom: 1.3, + maxZoom: 1.3, +}; + export const WorkflowDiagramCanvas = ({ diagram, workflowWithCurrentVersion, @@ -83,6 +89,10 @@ export const WorkflowDiagramCanvas = ({ return ( <> { + fitView(defaultFitViewOptions); + }} nodeTypes={{ default: WorkflowDiagramStepNode, 'create-step': WorkflowDiagramCreateStepNode, diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx index 27706668b4b1..2e1b1328a0b1 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx @@ -12,7 +12,7 @@ export const WorkflowDiagramCreateStepNode = () => { <> - + ); }; From 1e4ed1e96fb099dc6985ce12abb962d6c2d26b67 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 30 Sep 2024 11:42:06 +0200 Subject: [PATCH 017/115] Tag main as 0.31 canary (#7332) We are updating our git worklow. Case 1: **URGENT / PATCH** If you want to include something URGENT that cannot wait for the next release, you'll need to: - create a PR from the latest patch (right now v0.30.1) - create a new patch tag from this PR (would be v0.30.2 right now) - merge this PR in main so it's in 0.31 too Case 2: **REGULAR** - Open a PR from main and merge it into main I'm tagging main as v0.31.canary to make it clear! --- packages/twenty-emails/package.json | 2 +- packages/twenty-front/package.json | 2 +- packages/twenty-server/package.json | 2 +- packages/twenty-ui/package.json | 2 +- packages/twenty-website/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json index dc0b89328e3c..a792a42a373a 100644 --- a/packages/twenty-emails/package.json +++ b/packages/twenty-emails/package.json @@ -1,6 +1,6 @@ { "name": "twenty-emails", - "version": "0.30.0", + "version": "0.31.canary", "description": "", "author": "", "private": true, diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index e1a632247380..96e6442a3f56 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -1,6 +1,6 @@ { "name": "twenty-front", - "version": "0.30.0", + "version": "0.31.canary", "private": true, "type": "module", "scripts": { diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index d77b2faf4d78..dde92f42a8da 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -1,6 +1,6 @@ { "name": "twenty-server", - "version": "0.30.0", + "version": "0.31.canary", "description": "", "author": "", "private": true, diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index b004cba081fc..64e99d25f0a9 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -1,6 +1,6 @@ { "name": "twenty-ui", - "version": "0.30.0", + "version": "0.31.canary", "type": "module", "main": "./src/index.ts", "exports": { diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json index c235f812a142..42264f8fc505 100644 --- a/packages/twenty-website/package.json +++ b/packages/twenty-website/package.json @@ -1,6 +1,6 @@ { "name": "twenty-website", - "version": "0.30.0", + "version": "0.31.canary", "private": true, "scripts": { "nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js", From 95e1053b7aebcf9a60e50d9ea8dcea6c556eb6ca Mon Sep 17 00:00:00 2001 From: Harshit Singh <73997189+harshit078@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:43:33 +0530 Subject: [PATCH 018/115] fix: Title overflows in mobile viewport for right drawer (#7311) ## Description - This PR solves the issue #7310 --- .../ui/layout/show-page/components/ShowPageSummaryCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx index 137bad6002cf..94c3d934cb1d 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx @@ -60,7 +60,7 @@ const StyledTitle = styled.div<{ isMobile: boolean }>` font-size: ${({ theme }) => theme.font.size.xl}; font-weight: ${({ theme }) => theme.font.weight.semiBold}; justify-content: ${({ isMobile }) => (isMobile ? 'flex-start' : 'center')}; - width: ${({ isMobile }) => (isMobile ? '' : '100%')}; + max-width: 90%; `; const StyledAvatarWrapper = styled.div` From 06d4ba92e5bb3e20f2c15f25e92379bd2798d2a2 Mon Sep 17 00:00:00 2001 From: Weiko Date: Mon, 30 Sep 2024 15:45:17 +0200 Subject: [PATCH 019/115] increase export feature page size (#7341) ## Context Now that we have improved performances, we can increase the export feature page size from 30 to 200 (and probably above if results are good). This should be ok since we are only querying the first level of an object and omit relations. I've moved this value to a constant. --- .../options/constants/ExportTableDataDefaultPageSize.ts | 1 + .../record-index/options/hooks/useExportTableData.ts | 3 ++- .../object-record/record-index/options/hooks/useTableData.ts | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-index/options/constants/ExportTableDataDefaultPageSize.ts diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/constants/ExportTableDataDefaultPageSize.ts b/packages/twenty-front/src/modules/object-record/record-index/options/constants/ExportTableDataDefaultPageSize.ts new file mode 100644 index 000000000000..1a58deeb28ba --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/options/constants/ExportTableDataDefaultPageSize.ts @@ -0,0 +1 @@ +export const EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE = 200; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts index 8a535604dee8..532b8e0aa59b 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts @@ -2,6 +2,7 @@ import { json2csv } from 'json-2-csv'; import { useMemo } from 'react'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; import { useProcessRecordsForCSVExport } from '@/object-record/record-index/options/hooks/useProcessRecordsForCSVExport'; import { useTableData, @@ -142,7 +143,7 @@ export const useExportTableData = ({ filename, maximumRequests = 100, objectNameSingular, - pageSize = 30, + pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, recordIndexId, viewType, }: UseExportTableDataOptions) => { diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts index 1e6255276919..98294115c5d2 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts @@ -9,6 +9,7 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from '~/utils/isDefined'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { ViewType } from '@/views/types/ViewType'; import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable'; @@ -43,7 +44,7 @@ export const useTableData = ({ delayMs, maximumRequests = 100, objectNameSingular, - pageSize = 30, + pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, recordIndexId, callback, viewType = ViewType.Table, From ca027d6772083556e34103262741715ef311def1 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Mon, 30 Sep 2024 18:45:44 +0200 Subject: [PATCH 020/115] Add output to workflow run (#7276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Example of output stored for following workflow: Capture d’écran 2024-09-27 à 11 18 06 Output: ``` {"steps": [ {"type": "CODE", "result": {"email": "test@twenty.com"}}, {"type": "SEND_EMAIL", "result": {"success": true}} ]} ``` --- .../constants/standard-field-ids.ts | 1 + .../workflow-run.workspace-entity.ts | 20 ++++++ .../workflow-executor.workspace-service.ts | 65 +++++++++++++------ .../workflow-runner/jobs/run-workflow.job.ts | 27 ++++---- .../workflow-run.workspace-service.ts | 10 ++- 5 files changed, 88 insertions(+), 35 deletions(-) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 1fa9a7f41de1..890db119ebee 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -415,6 +415,7 @@ export const WORKFLOW_RUN_STANDARD_FIELD_IDS = { endedAt: '20202020-e1c1-4b6b-bbbd-b2beaf2e159e', status: '20202020-6b3e-4f9c-8c2b-2e5b8e6d6f3b', createdBy: '20202020-6007-401a-8aa5-e6f38581a6f3', + output: '20202020-7be4-4db2-8ac6-3ff0d740843d', }; export const WORKFLOW_VERSION_STANDARD_FIELD_IDS = { 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 c38cf4b5a485..388d5f1293c9 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 @@ -27,6 +27,17 @@ export enum WorkflowRunStatus { FAILED = 'FAILED', } +export type WorkflowRunOutput = { + steps: { + id: string; + name: string; + type: string; + attemptCount: number; + result: object | undefined; + error: string | undefined; + }[]; +}; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.workflowRun, namePlural: 'workflowRuns', @@ -108,6 +119,15 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { }) createdBy: ActorMetadata; + @WorkspaceField({ + standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.output, + type: FieldMetadataType.RAW_JSON, + label: 'Output', + description: 'Json object to provide output of the workflow run', + }) + @WorkspaceIsNullable() + output: WorkflowRunOutput | null; + // Relations @WorkspaceRelation({ standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.workflowVersion, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts index 593543047e06..c50684f876c6 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts @@ -1,17 +1,17 @@ import { Injectable } from '@nestjs/common'; -import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; import { - WorkflowExecutorException, - WorkflowExecutorExceptionCode, -} from 'src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception'; + WorkflowRunOutput, + WorkflowRunStatus, +} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory'; +import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; const MAX_RETRIES_ON_FAILURE = 3; -export type WorkflowExecutionOutput = { - result?: object; - error?: object; +export type WorkflowExecutorOutput = { + steps: WorkflowRunOutput['steps']; + status: WorkflowRunStatus; }; @Injectable() @@ -22,17 +22,17 @@ export class WorkflowExecutorWorkspaceService { currentStepIndex, steps, payload, + output, attemptCount = 1, }: { currentStepIndex: number; steps: WorkflowStep[]; + output: WorkflowExecutorOutput; payload?: object; attemptCount?: number; - }): Promise { + }): Promise { if (currentStepIndex >= steps.length) { - return { - result: payload, - }; + return { ...output, status: WorkflowRunStatus.COMPLETED }; } const step = steps[currentStepIndex]; @@ -44,19 +44,47 @@ export class WorkflowExecutorWorkspaceService { payload, }); + const baseStepOutput = { + id: step.id, + name: step.name, + type: step.type, + attemptCount, + }; + + const updatedOutput = { + ...output, + steps: [ + ...output.steps, + { + ...baseStepOutput, + result: result.result, + error: result.error?.errorMessage, + }, + ], + }; + if (result.result) { return await this.execute({ currentStepIndex: currentStepIndex + 1, steps, payload: result.result, + output: updatedOutput, }); } if (!result.error) { - throw new WorkflowExecutorException( - 'Execution result error, no data or error', - WorkflowExecutorExceptionCode.WORKFLOW_FAILED, - ); + return { + ...output, + steps: [ + ...output.steps, + { + ...baseStepOutput, + result: undefined, + error: 'Execution result error, no data or error', + }, + ], + status: WorkflowRunStatus.FAILED, + }; } if (step.settings.errorHandlingOptions.continueOnFailure.value) { @@ -64,6 +92,7 @@ export class WorkflowExecutorWorkspaceService { currentStepIndex: currentStepIndex + 1, steps, payload, + output: updatedOutput, }); } @@ -75,13 +104,11 @@ export class WorkflowExecutorWorkspaceService { currentStepIndex, steps, payload, + output: updatedOutput, attemptCount: attemptCount + 1, }); } - throw new WorkflowExecutorException( - `Workflow failed: ${result.error}`, - WorkflowExecutorExceptionCode.WORKFLOW_FAILED, - ); + return { ...updatedOutput, status: WorkflowRunStatus.FAILED }; } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts index 0ca2a3107a6e..5a79462a355c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts @@ -3,8 +3,8 @@ import { Scope } from '@nestjs/common'; 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'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service'; import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service'; @@ -36,24 +36,23 @@ export class RunWorkflowJob { workflowVersionId, ); - try { + const { steps, status } = await this.workflowExecutorWorkspaceService.execute({ currentStepIndex: 0, steps: workflowVersion.steps || [], payload, + output: { + steps: [], + status: WorkflowRunStatus.RUNNING, + }, }); - await this.workflowRunWorkspaceService.endWorkflowRun( - workflowRunId, - WorkflowRunStatus.COMPLETED, - ); - } catch (error) { - await this.workflowRunWorkspaceService.endWorkflowRun( - workflowRunId, - WorkflowRunStatus.FAILED, - ); - - throw error; - } + await this.workflowRunWorkspaceService.endWorkflowRun( + workflowRunId, + status, + { + steps, + }, + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service.ts index 00162c595f96..2f3aca25543c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service.ts @@ -2,11 +2,12 @@ import { Injectable } from '@nestjs/common'; import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { + WorkflowRunOutput, WorkflowRunStatus, WorkflowRunWorkspaceEntity, } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowRunException, WorkflowRunExceptionCode, @@ -70,7 +71,11 @@ export class WorkflowRunWorkspaceService { }); } - async endWorkflowRun(workflowRunId: string, status: WorkflowRunStatus) { + async endWorkflowRun( + workflowRunId: string, + status: WorkflowRunStatus, + output: WorkflowRunOutput, + ) { const workflowRunRepository = await this.twentyORMManager.getRepository( 'workflowRun', @@ -96,6 +101,7 @@ export class WorkflowRunWorkspaceService { return workflowRunRepository.update(workflowRunToUpdate.id, { status, + output, endedAt: new Date().toISOString(), }); } From 0d570caff5d923e132789d7a5a62a04253d0a531 Mon Sep 17 00:00:00 2001 From: Sachin <90304264+sachinks07@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:32:13 +0530 Subject: [PATCH 021/115] Fix cursor should not be pointer when record image identifier is not Editable (#7320) - This PR solves the issue Cursor should not be "pointer" when record image identifier is not editable #7277 --------- Co-authored-by: Sachin KS --- .../ui/layout/show-page/components/ShowPageSummaryCard.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx index 94c3d934cb1d..9627be5d7e61 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx @@ -63,8 +63,9 @@ const StyledTitle = styled.div<{ isMobile: boolean }>` max-width: 90%; `; -const StyledAvatarWrapper = styled.div` - cursor: pointer; +const StyledAvatarWrapper = styled.div<{ isAvatarEditable: boolean }>` + cursor: ${({ isAvatarEditable }) => + isAvatarEditable ? 'pointer' : 'default'}; `; const StyledFileInput = styled.input` @@ -130,7 +131,7 @@ export const ShowPageSummaryCard = ({ return ( - + Date: Tue, 1 Oct 2024 14:22:14 +0200 Subject: [PATCH 022/115] Add workflow email action (#7279) - Add the SAVE_EMAIL action. This action requires more setting parameters than the Serverless Function action. - Changed the way we computed the workflow diagram. It now preserves some properties, like the `selected` property. That's necessary to not close the right drawer when the workflow back-end data change. - Added the possibility to set a label to a TextArea. This uses a `