From be08233fa4df8866d1e343e31101efe5330e08bb Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 28 Oct 2025 09:54:47 +0100 Subject: [PATCH] Add sort section in find records action (#15340) https://github.com/user-attachments/assets/020f3e97-316b-41db-b95d-7b938a7f82cf --- .../WorkflowEditActionFindRecords.tsx | 52 +++++- .../components/WorkflowFindRecordsSorts.tsx | 163 ++++++++++++++++++ .../components/KeyValuePairInput.tsx | 17 +- .../services/find-records.service.ts | 2 +- .../types/find-records-params.type.ts | 6 +- .../types/record-crud-input.type.ts | 6 +- .../find-records.workflow-action.ts | 2 +- .../find-records-action-settings-schema.ts | 8 + 8 files changed, 241 insertions(+), 15 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsSorts.tsx diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx index 913fe708046..093849417a8 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowEditActionFindRecords.tsx @@ -5,6 +5,7 @@ import { type WorkflowFindRecordsAction } from '@/workflow/types/Workflow'; import { useEffect, useState } from 'react'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput'; import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup'; @@ -12,12 +13,14 @@ import { RecordFiltersComponentInstanceContext } from '@/object-record/record-fi import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates'; +import { type RecordSort } from '@/object-record/record-sort/types/RecordSort'; import { InputLabel } from '@/ui/input/components/InputLabel'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; import { FIND_RECORDS_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FindRecordsAction'; import { WorkflowFindRecordsFilters } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFilters'; import { WorkflowFindRecordsFiltersEffect } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFiltersEffect'; +import { WorkflowFindRecordsSorts } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsSorts'; import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader'; import { useLingui } from '@lingui/react/macro'; import { isNumber } from '@sniptt/guards'; @@ -42,6 +45,7 @@ type WorkflowEditActionFindRecordsProps = { type FindRecordsFormData = { objectNameSingular: string; filter?: FindRecordsActionFilter; + orderBy?: FindRecordsActionOrderBy; limit?: number; }; @@ -51,6 +55,11 @@ export type FindRecordsActionFilter = { gqlOperationFilter?: JsonValue; }; +export type FindRecordsActionOrderBy = { + recordSorts?: RecordSort[]; + gqlOperationOrderBy?: JsonValue; +}; + export const WorkflowEditActionFindRecords = ({ action, actionOptions, @@ -72,8 +81,9 @@ export const WorkflowEditActionFindRecords = ({ objectNameSingular: action.settings.input.objectName, limit: action.settings.input.limit, filter: action.settings.input.filter as FindRecordsActionFilter, + orderBy: action.settings.input.orderBy as FindRecordsActionOrderBy, }); - const isFormDisabled = actionOptions.readonly; + const isFormDisabled = actionOptions.readonly ?? false; const instanceId = `workflow-edit-action-record-find-records-${action.id}-${formData.objectNameSingular}`; const selectedObjectMetadataItem = activeNonSystemObjectMetadataItems.find( @@ -105,6 +115,7 @@ export const WorkflowEditActionFindRecords = ({ objectNameSingular: updatedObjectName, limit: updatedLimit, filter: updatedFilter, + orderBy: updatedOrderBy, } = formData; actionOptions.onActionUpdate({ @@ -115,6 +126,7 @@ export const WorkflowEditActionFindRecords = ({ objectName: updatedObjectName, limit: updatedLimit ?? 1, filter: updatedFilter, + orderBy: updatedOrderBy as Record | undefined, }, }, }); @@ -179,7 +191,7 @@ export const WorkflowEditActionFindRecords = ({ {isDefined(selectedObjectMetadataItem) && (
- {t`Conditions`} + {t`Filter`} '', @@ -229,10 +241,46 @@ export const WorkflowEditActionFindRecords = ({
)} + {isDefined(selectedObjectMetadataItem) && ( + <> +
+ {t`Sort`} + { + if (isFormDisabled === true) { + return; + } + + const gqlOperationOrderBy = + sorts.length > 0 + ? turnSortsIntoOrderBy(selectedObjectMetadataItem, sorts) + : undefined; + + const newFormData: FindRecordsFormData = { + ...formData, + orderBy: { + recordSorts: sorts, + gqlOperationOrderBy, + }, + }; + + setFormData(newFormData); + + saveAction(newFormData); + }} + readonly={isFormDisabled} + /> +
+ + )} + { if (isFormDisabled === true || !isNumber(limit)) { return; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsSorts.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsSorts.tsx new file mode 100644 index 00000000000..1d841a56aa4 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsSorts.tsx @@ -0,0 +1,163 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems'; +import { type RecordSort } from '@/object-record/record-sort/types/RecordSort'; +import { Select } from '@/ui/input/components/Select'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { IconArrowsSort, IconTrash, useIcons } from 'twenty-ui/display'; +import { Button, type SelectOption } from 'twenty-ui/input'; +import { v4 as uuidv4 } from 'uuid'; +import { ViewSortDirection } from '~/generated/graphql'; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledSortItemContainer = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledAddButtonContainer = styled.div` + margin-top: ${({ theme }) => theme.spacing(1)}; +`; + +type WorkflowFindRecordsSortsProps = { + recordSorts: RecordSort[]; + onChange: (recordSorts: RecordSort[]) => void; + objectMetadataItem: ObjectMetadataItem; + readonly: boolean; +}; + +export const WorkflowFindRecordsSorts = ({ + recordSorts, + onChange, + objectMetadataItem, + readonly, +}: WorkflowFindRecordsSortsProps) => { + const { t } = useLingui(); + const { getIcon } = useIcons(); + + const sortableFields = objectMetadataItem.fields + .filter(filterSortableFieldMetadataItems) + .map((field) => ({ + Icon: getIcon(field.icon), + label: field.label, + value: field.id, + })); + + const directionOptions: Array> = [ + { + label: t`Ascending`, + value: ViewSortDirection.ASC, + }, + { + label: t`Descending`, + value: ViewSortDirection.DESC, + }, + ]; + + const handleAddSort = () => { + if (readonly) { + return; + } + + const newSort: RecordSort = { + id: uuidv4(), + fieldMetadataId: sortableFields[0]?.value ?? '', + direction: ViewSortDirection.ASC, + }; + + onChange([...recordSorts, newSort]); + }; + + const handleRemoveSort = (sortId: string) => { + if (readonly) { + return; + } + + onChange(recordSorts.filter((sort) => sort.id !== sortId)); + }; + + const handleFieldChange = (sortId: string, fieldMetadataId: string) => { + if (readonly) { + return; + } + + onChange( + recordSorts.map((sort) => + sort.id === sortId ? { ...sort, fieldMetadataId } : sort, + ), + ); + }; + + const handleDirectionChange = ( + sortId: string, + direction: ViewSortDirection, + ) => { + if (readonly) { + return; + } + + onChange( + recordSorts.map((sort) => + sort.id === sortId ? { ...sort, direction } : sort, + ), + ); + }; + + return ( + + {recordSorts.map((sort) => { + return ( + + + handleDirectionChange(sort.id, value as ViewSortDirection) + } + /> + + {!readonly && ( +