From 96d56f8883e446ebba0126dc259bccdf903450f6 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 10 Dec 2024 16:33:52 +0100 Subject: [PATCH 01/11] Fix removeBillingFKWithCore migration location (#9006) removeBillingFKWithCore migration is in the wrong folder and is not executed as intended. Moving to billing folder to fix that and to be only run in billing mode --- .../{ => billing}/1733753649142-removeBillingFKWithCore.ts | 0 .../workspace/services/workspace.service.spec.ts | 7 ++++++- 2 files changed, 6 insertions(+), 1 deletion(-) rename packages/twenty-server/src/database/typeorm/core/migrations/{ => billing}/1733753649142-removeBillingFKWithCore.ts (100%) diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1733753649142-removeBillingFKWithCore.ts b/packages/twenty-server/src/database/typeorm/core/migrations/billing/1733753649142-removeBillingFKWithCore.ts similarity index 100% rename from packages/twenty-server/src/database/typeorm/core/migrations/1733753649142-removeBillingFKWithCore.ts rename to packages/twenty-server/src/database/typeorm/core/migrations/billing/1733753649142-removeBillingFKWithCore.ts diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts index 6a3d15c8fae8..18e0f3a109c0 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts @@ -2,6 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @@ -13,7 +15,6 @@ import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service'; import { WorkspaceService } from './workspace.service'; @@ -56,6 +57,10 @@ describe('WorkspaceService', () => { provide: BillingSubscriptionService, useValue: {}, }, + { + provide: BillingService, + useValue: {}, + }, { provide: EnvironmentService, useValue: {}, From b6e02b630d62aaef6ff54cdb8824d2aa9da1ad2a Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 10 Dec 2024 17:29:55 +0100 Subject: [PATCH 02/11] Add full name composite field (#9008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add composite field - Add test - Fix search variable dropdown scroll Capture d’écran 2024-12-10 à 16 52 43 --- .../components/FormFieldInput.tsx | 14 +++- .../components/FormFullNameFieldInput.tsx | 66 +++++++++++++++++++ ...StyledFormCompositeFieldInputContainer.tsx | 11 ++++ .../StyledFormFieldInputContainer.tsx | 1 + .../FormFullNameFieldInput.stories.tsx | 31 +++++++++ .../input/components/FullNameFieldInput.tsx | 8 +-- .../input/constants/FirstNamePlaceholder.ts | 2 + .../input/constants/LastNamePlaceholder.ts | 2 + .../components/SearchVariablesDropdown.tsx | 7 +- .../SearchVariablesDropdownFieldItems.tsx | 13 ++-- .../SearchVariablesDropdownObjectItems.tsx | 5 +- ...archVariablesDropdownWorkflowStepItems.tsx | 5 +- ...rkflowEditActionFormServerlessFunction.tsx | 16 +---- 13 files changed, 148 insertions(+), 33 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFullNameFieldInput.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormFullNameFieldInput.stories.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/input/constants/LastNamePlaceholder.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx index 5a99fd7d32ee..dd0d999dac43 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx @@ -1,11 +1,16 @@ import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput'; +import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput'; import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput'; import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; -import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { + FieldFullNameValue, + FieldMetadata, +} from '@/object-record/record-field/types/FieldMetadata'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; +import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; @@ -55,5 +60,12 @@ export const FormFieldInput = ({ field={field} VariablePicker={VariablePicker} /> + ) : isFieldFullName(field) ? ( + ) : null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFullNameFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFullNameFieldInput.tsx new file mode 100644 index 000000000000..e4cf57b42362 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFullNameFieldInput.tsx @@ -0,0 +1,66 @@ +import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; +import { StyledFormCompositeFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer'; +import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer'; +import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; +import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder'; +import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder'; +import { FieldFullNameValue } from '@/object-record/record-field/types/FieldMetadata'; +import { InputLabel } from '@/ui/input/components/InputLabel'; + +type FormFullNameFieldInputProps = { + label?: string; + defaultValue: FieldFullNameValue | undefined; + onPersist: (value: FieldFullNameValue) => void; + VariablePicker?: VariablePickerComponent; + readonly?: boolean; +}; + +export const FormFullNameFieldInput = ({ + label, + defaultValue, + onPersist, + readonly, + VariablePicker, +}: FormFullNameFieldInputProps) => { + const handleFirstNameChange = (newText: string) => { + onPersist({ + lastName: defaultValue?.lastName ?? '', + firstName: newText, + }); + }; + + const handleLastNameChange = (newText: string) => { + onPersist({ + firstName: defaultValue?.firstName ?? '', + lastName: newText, + }); + }; + + return ( + + {label ? {label} : null} + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer.tsx new file mode 100644 index 000000000000..47a8bfe774a4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer.tsx @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +export const StyledFormCompositeFieldInputContainer = styled.div` + display: flex; + flex-direction: column; + background: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + gap: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(2)}; +`; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/StyledFormFieldInputContainer.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/StyledFormFieldInputContainer.tsx index 5615ab80b77f..cc04e0f7a70d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/StyledFormFieldInputContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/StyledFormFieldInputContainer.tsx @@ -3,4 +3,5 @@ import styled from '@emotion/styled'; export const StyledFormFieldInputContainer = styled.div` display: flex; flex-direction: column; + width: 100%; `; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormFullNameFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormFullNameFieldInput.stories.tsx new file mode 100644 index 000000000000..62def51e57e6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormFullNameFieldInput.stories.tsx @@ -0,0 +1,31 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { within } from '@storybook/test'; +import { FormFullNameFieldInput } from '../FormFullNameFieldInput'; + +const meta: Meta = { + title: 'UI/Data/Field/Form/Input/FormFullNameFieldInput', + component: FormFullNameFieldInput, + args: {}, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Name', + defaultValue: { + firstName: 'John', + lastName: 'Doe', + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Name'); + await canvas.findByText('First Name'); + await canvas.findByText('Last Name'); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx index bdba34f0c567..49335d42b08b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx @@ -3,18 +3,14 @@ import { FieldDoubleText } from '@/object-record/record-field/types/FieldDoubleT import { DoubleTextInput } from '@/ui/field/input/components/DoubleTextInput'; import { FieldInputOverlay } from '@/ui/field/input/components/FieldInputOverlay'; +import { FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder'; +import { LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS } from '@/object-record/record-field/meta-types/input/constants/LastNamePlaceholder'; import { isDoubleTextFieldEmpty } from '@/object-record/record-field/meta-types/input/utils/isDoubleTextFieldEmpty'; import { FieldInputClickOutsideEvent, FieldInputEvent, } from './DateTimeFieldInput'; -const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS = - 'F‌‌irst name'; - -const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS = - 'L‌‌ast name'; - type FullNameFieldInputProps = { onClickOutside?: FieldInputClickOutsideEvent; onEnter?: FieldInputEvent; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder.ts new file mode 100644 index 000000000000..7eac65e02e17 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/constants/FirstNamePlaceholder.ts @@ -0,0 +1,2 @@ +export const FIRST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS = + 'F‌‌irst name'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/constants/LastNamePlaceholder.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/constants/LastNamePlaceholder.ts new file mode 100644 index 000000000000..04f082144a95 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/constants/LastNamePlaceholder.ts @@ -0,0 +1,2 @@ +export const LAST_NAME_PLACEHOLDER_WITH_SPECIAL_CHARACTER_TO_AVOID_PASSWORD_MANAGERS = + 'L‌‌ast name'; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx index 965a41f40ab7..64eebb382f29 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx @@ -1,4 +1,5 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { SearchVariablesDropdownFieldItems } from '@/workflow/search-variables/components/SearchVariablesDropdownFieldItems'; @@ -26,7 +27,7 @@ const StyledDropdownVariableButtonContainer = styled( } `; -const StyledDropdownComponetsContainer = styled.div` +const StyledDropdownComponentsContainer = styled(DropdownMenuItemsContainer)` background-color: ${({ theme }) => theme.background.transparent.light}; `; @@ -136,9 +137,9 @@ const SearchVariablesDropdown = ({ } dropdownComponents={ - + {renderSearchVariablesDropdownComponents()} - + } dropdownPlacement="bottom-end" dropdownOffset={{ x: 0, y: 4 }} diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx index 8dcd83572d8b..ac274e131751 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownFieldItems.tsx @@ -1,5 +1,4 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { BaseOutputSchema, @@ -100,8 +99,14 @@ export const SearchVariablesDropdownFieldItems = ({ : options; return ( - - + <> + ))} - + ); }; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx index 92aace99af84..b48cf0bffc7d 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownObjectItems.tsx @@ -1,5 +1,4 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { OutputSchema, @@ -112,7 +111,7 @@ export const SearchVariablesDropdownObjectItems = ({ : options; return ( - + <> @@ -154,6 +153,6 @@ export const SearchVariablesDropdownObjectItems = ({ LeftIcon={value.icon ? getIcon(value.icon) : undefined} /> ))} - + ); }; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems.tsx index 4c0520752c5e..b4a390c736dc 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownWorkflowStepItems.tsx @@ -1,5 +1,4 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema'; @@ -36,7 +35,7 @@ export const SearchVariablesDropdownWorkflowStepItems = ({ ); return ( - + <> @@ -74,6 +73,6 @@ export const SearchVariablesDropdownWorkflowStepItems = ({ hasSubMenu={false} /> )} - + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx index a5685313ee2b..93527dd2abf1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx @@ -1,4 +1,5 @@ import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; +import { StyledFormCompositeFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormCompositeFieldInputContainer'; import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction'; @@ -44,17 +45,6 @@ const StyledLabel = styled.div` margin-bottom: ${({ theme }) => theme.spacing(2)}; `; -const StyledInputContainer = styled.div` - background: ${({ theme }) => theme.background.secondary}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.md}; - display: flex; - flex-direction: column; - gap: ${({ theme }) => theme.spacing(2)}; - padding: ${({ theme }) => theme.spacing(2)}; - position: relative; -`; - type WorkflowEditActionFormServerlessFunctionProps = { action: WorkflowCodeAction; actionOptions: @@ -223,9 +213,9 @@ export const WorkflowEditActionFormServerlessFunction = ({ return ( {inputKey} - + {renderFields(inputValue, currentPath, false)} - + ); } else { From 8ecf07f112940696fe9983ee32bf66235be6967c Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Tue, 10 Dec 2024 19:03:38 +0100 Subject: [PATCH 03/11] [Aggregate queries for table views - #1] Introduce aggregateOperationForViewFieldState (#9010) Introducing aggregateOperationForViewFieldState to add a state storing the aggregate operation for each view field --- .../components/RecordIndexContainer.tsx | 20 ++++++++++ .../RecordIndexTableContainerEffect.tsx | 38 +++++++++++++++++++ .../aggregateOperationForViewFieldState.ts | 10 +++++ 3 files changed, 68 insertions(+) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/aggregateOperationForViewFieldState.ts diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index 7af9419473e1..276348c9d83c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -28,6 +28,7 @@ import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetReco import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; +import { aggregateOperationForViewFieldState } from '@/object-record/record-table/record-table-footer/states/aggregateOperationForViewFieldState'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewField } from '@/views/types/ViewField'; @@ -118,6 +119,25 @@ export const RecordIndexContainer = () => { ) { set(recordIndexFieldDefinitionsState, newFieldDefinitions); } + + for (const viewField of viewFields) { + const aggregateOperationForViewField = snapshot + .getLoadable( + aggregateOperationForViewFieldState({ + viewFieldId: viewField.id, + }), + ) + .getValue(); + + if (aggregateOperationForViewField !== viewField.aggregateOperation) { + set( + aggregateOperationForViewFieldState({ + viewFieldId: viewField.id, + }), + viewField.aggregateOperation, + ); + } + } }, [columnDefinitions, setTableColumns], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index 2ff744268dd0..aa7e470c9c70 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -6,7 +6,11 @@ import { RecordIndexRootPropsContext } from '@/object-record/record-index/contex import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter'; import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; +import { aggregateOperationForViewFieldState } from '@/object-record/record-table/record-table-footer/states/aggregateOperationForViewFieldState'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; +import { ViewField } from '@/views/types/ViewField'; +import { useRecoilCallback } from 'recoil'; export const RecordIndexTableContainerEffect = () => { const { recordIndexId, objectNameSingular } = useContext( @@ -48,6 +52,8 @@ export const RecordIndexTableContainerEffect = () => { viewBarId, }); + const { currentViewWithSavedFiltersAndSorts } = useGetCurrentView(); + useEffect(() => { setOnToggleColumnFilter( () => (fieldMetadataId: string) => @@ -68,5 +74,37 @@ export const RecordIndexTableContainerEffect = () => { ); }, [setRecordCountInCurrentView, setOnEntityCountChange]); + const setViewFieldAggregateOperation = useRecoilCallback( + ({ set, snapshot }) => + (viewField: ViewField) => { + const aggregateOperationForViewField = snapshot + .getLoadable( + aggregateOperationForViewFieldState({ + viewFieldId: viewField.id, + }), + ) + .getValue(); + + if (aggregateOperationForViewField !== viewField.aggregateOperation) { + set( + aggregateOperationForViewFieldState({ + viewFieldId: viewField.id, + }), + viewField.aggregateOperation, + ); + } + }, + [], + ); + + useEffect(() => { + currentViewWithSavedFiltersAndSorts?.viewFields.forEach((viewField) => { + setViewFieldAggregateOperation(viewField); + }); + }, [ + currentViewWithSavedFiltersAndSorts?.viewFields, + setViewFieldAggregateOperation, + ]); + return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/aggregateOperationForViewFieldState.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/aggregateOperationForViewFieldState.ts new file mode 100644 index 000000000000..7e39ed2b48d1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/states/aggregateOperationForViewFieldState.ts @@ -0,0 +1,10 @@ +import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; + +export const aggregateOperationForViewFieldState = createFamilyState< + AGGREGATE_OPERATIONS | null | undefined, + { viewFieldId: string } +>({ + key: 'aggregateOperationForViewFieldState', + defaultValue: null, +}); From c1fff908fe9e83a1451815d7fa7aee72f22c5bc3 Mon Sep 17 00:00:00 2001 From: Naifer <161821705+omarNaifer12@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:06:23 +0100 Subject: [PATCH 04/11] Fix:Hide deleted filter component when reset button is clicked (#8880) Resolve: #8874 Problem : When we are on the deleted records page and use the filter, if no records are found, we see the no deleted recordName message along with a button to remove the deleted filter. However, if we reset and filter again, and still don't find any records, this message and button for the deleted filter continue to display. Solution: I noticed that the component RecordTableEmptyStateSoftDelete has this button, and its visibility is controlled by the function toggleSoftDeleteFilterState. If the state is true, the button appears; if it's false, it doesn't. Therefore, we just need to call this function when the reset button is clicked and set the state to false. ![All-Peopleand1morepage-Personal-MicrosoftEdge2024-12-0421-04-12-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/68e524ff-2902-4a25-a361-3bb8e1220ff8) --------- Co-authored-by: bosiraphael --- .../src/modules/views/components/ViewBar.tsx | 1 + .../src/modules/views/components/ViewBarDetails.tsx | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx index 59642d58a301..41a0224469ec 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx @@ -82,6 +82,7 @@ export const ViewBar = ({ filterDropdownId={filterDropdownId} hasFilterButton viewBarId={viewBarId} + objectNamePlural={objectNamePlural} rightComponent={ { const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); @@ -125,6 +129,13 @@ export const ViewBarDetails = ({ availableSortDefinitionsComponentState, ); + const { objectNameSingular } = useObjectNameSingularFromPlural({ + objectNamePlural: objectNamePlural, + }); + const { toggleSoftDeleteFilterState } = useHandleToggleTrashColumnFilter({ + objectNameSingular: objectNameSingular, + viewBarId: viewBarId, + }); const { resetUnsavedViewStates } = useResetUnsavedViewStates(); const canResetView = canPersistView && !hasFiltersQueryParams; @@ -159,6 +170,7 @@ export const ViewBarDetails = ({ const handleCancelClick = () => { if (isDefined(viewId)) { resetUnsavedViewStates(viewId); + toggleSoftDeleteFilterState(false); } }; From 89f6f322436c95951739d0e0a03c220aada4822e Mon Sep 17 00:00:00 2001 From: Guillim Date: Wed, 11 Dec 2024 11:37:08 +0100 Subject: [PATCH 05/11] Title overflow (#9009) fix #8803 --- .../record-board-card/components/RecordBoardCard.tsx | 1 + .../record-index/components/RecordIndexRecordChip.tsx | 3 +++ .../twenty-ui/src/display/chip/components/AvatarChip.tsx | 9 ++++++--- packages/twenty-ui/src/display/chip/components/Chip.tsx | 5 +++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index a456c89a3d9f..67968652f846 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -307,6 +307,7 @@ export const RecordBoardCard = ({ objectNameSingular={objectMetadataItem.nameSingular} record={record as ObjectRecord} variant={AvatarChipVariant.Transparent} + maxWidth={150} /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx index 51121c41c7d0..b02591e8c813 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx @@ -10,6 +10,7 @@ export type RecordIdentifierChipProps = { record: ObjectRecord; variant?: AvatarChipVariant; size?: ChipSize; + maxWidth?: number; }; export const RecordIdentifierChip = ({ @@ -17,6 +18,7 @@ export const RecordIdentifierChip = ({ record, variant, size, + maxWidth, }: RecordIdentifierChipProps) => { const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext); const { recordChipData } = useRecordChipData({ @@ -37,6 +39,7 @@ export const RecordIdentifierChip = ({ LeftIcon={LeftIcon} LeftIconColor={LeftIconColor} size={size} + maxWidth={maxWidth} /> ); }; diff --git a/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx b/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx index 32aec0420e5d..4cd7736a4567 100644 --- a/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx +++ b/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx @@ -24,6 +24,7 @@ export type AvatarChipProps = { placeholderColorSeed?: string; onClick?: (event: MouseEvent) => void; to?: string; + maxWidth?: number; }; export enum AvatarChipVariant { @@ -60,6 +61,7 @@ export const AvatarChip = ({ onClick, to, size = ChipSize.Small, + maxWidth, }: AvatarChipProps) => { const { theme } = useContext(ThemeContext); @@ -106,14 +108,15 @@ export const AvatarChip = ({ clickable={isDefined(onClick) || isDefined(to)} onClick={to ? undefined : onClick} className={className} + maxWidth={maxWidth} /> ); - return to ? ( + if (!isDefined(to)) return chip; + + return ( {chip} - ) : ( - chip ); }; diff --git a/packages/twenty-ui/src/display/chip/components/Chip.tsx b/packages/twenty-ui/src/display/chip/components/Chip.tsx index 42afc99b5168..eda194d5e571 100644 --- a/packages/twenty-ui/src/display/chip/components/Chip.tsx +++ b/packages/twenty-ui/src/display/chip/components/Chip.tsx @@ -64,7 +64,7 @@ const StyledContainer = withTheme(styled.div< : 'inherit'}; display: inline-flex; - justify-content: center; + justify-content: flex-start; gap: ${({ theme }) => theme.spacing(1)}; height: ${({ theme, size }) => size === ChipSize.Large ? theme.spacing(4) : theme.spacing(3)}; @@ -72,7 +72,6 @@ const StyledContainer = withTheme(styled.div< maxWidth ? `calc(${maxWidth}px - 2 * var(--chip-horizontal-padding))` : '200px'}; - overflow: hidden; padding: var(--chip-vertical-padding) var(--chip-horizontal-padding); user-select: none; @@ -129,6 +128,7 @@ export const Chip = ({ accent = ChipAccent.TextPrimary, onClick, className, + maxWidth, }: ChipProps) => { return ( {leftComponent} From 5c60a5511ef894e0a0bba8eeb09cc5944587f8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:51:33 +0100 Subject: [PATCH 06/11] 8929 move recordindexactionmenu menu inside the recordindexpageheader (#9007) Closes #8929 Users with the feature flag `IS_PAGE_HEADER_V2_ENABLED` set to false will still have the old behavior with the action bar. To test the PR, test with and without the feature flag. --- .../components/RecordIndexActionMenu.tsx | 11 +++++- .../components/RecordIndexActionMenuBar.tsx | 4 +- .../RecordIndexActionMenuBarEntry.tsx | 6 +-- .../RecordIndexActionMenuButtons.tsx | 37 +++++++++++++++++++ .../RecordIndexActionMenuBar.stories.tsx | 16 ++------ .../RecordIndexActionMenuBarEntry.stories.tsx | 2 - .../components/RecordIndexContainer.tsx | 7 +++- .../components/RecordIndexPageHeader.tsx | 36 +++++++++++++----- .../RecordTableBodyUnselectEffect.tsx | 1 + .../ui/layout/page/components/PageHeader.tsx | 2 +- .../pages/object-record/RecordIndexPage.tsx | 36 +++++++++--------- 11 files changed, 107 insertions(+), 51 deletions(-) create mode 100644 packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuButtons.tsx diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx index a20ff64c96a8..8e3864ad766f 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx @@ -2,6 +2,7 @@ import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-acti import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect'; import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar'; +import { RecordIndexActionMenuButtons } from '@/action-menu/components/RecordIndexActionMenuButtons'; import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown'; import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; @@ -17,6 +18,10 @@ export const RecordIndexActionMenu = () => { const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); + const isPageHeaderV2Enabled = useIsFeatureEnabled( + 'IS_PAGE_HEADER_V2_ENABLED', + ); + return ( <> {contextStoreCurrentObjectMetadataId && ( @@ -26,7 +31,11 @@ export const RecordIndexActionMenu = () => { onActionExecutedCallback: () => {}, }} > - + {isPageHeaderV2Enabled ? ( + + ) : ( + + )} diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx index fcc93f5bc50e..2800c622758c 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx @@ -1,5 +1,3 @@ -import styled from '@emotion/styled'; - import { RecordIndexActionMenuBarAllActionsButton } from '@/action-menu/components/RecordIndexActionMenuBarAllActionsButton'; import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; @@ -10,6 +8,7 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import styled from '@emotion/styled'; const StyledLabel = styled.div` color: ${({ theme }) => theme.font.color.tertiary}; @@ -33,7 +32,6 @@ export const RecordIndexActionMenuBar = () => { ); const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned); - if (contextStoreNumberOfSelectedRecords === 0) { return null; } diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx index ffa52d20590b..1c73010da1a2 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx @@ -1,8 +1,7 @@ +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; - type RecordIndexActionMenuBarEntryProps = { entry: ActionMenuEntry; }; @@ -13,11 +12,9 @@ const StyledButton = styled.div` cursor: pointer; display: flex; justify-content: center; - padding: ${({ theme }) => theme.spacing(2)}; transition: background ${({ theme }) => theme.animation.duration.fast} ease; user-select: none; - &:hover { background: ${({ theme }) => theme.background.tertiary}; } @@ -32,6 +29,7 @@ export const RecordIndexActionMenuBarEntry = ({ entry, }: RecordIndexActionMenuBarEntryProps) => { const theme = useTheme(); + return ( entry.onClick?.()}> {entry.Icon && } diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuButtons.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuButtons.tsx new file mode 100644 index 000000000000..34090f9922d4 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuButtons.tsx @@ -0,0 +1,37 @@ +import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { Button } from 'twenty-ui'; + +export const RecordIndexActionMenuButtons = () => { + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, + ); + + const actionMenuEntries = useRecoilComponentValueV2( + actionMenuEntriesComponentSelector, + ); + + const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned); + + if (contextStoreNumberOfSelectedRecords === 0) { + return null; + } + + return ( + <> + {pinnedEntries.map((entry, index) => ( +