Add sort section in find records action (#15340)

https://github.com/user-attachments/assets/020f3e97-316b-41db-b95d-7b938a7f82cf
This commit is contained in:
Thomas Trompette
2025-10-28 09:54:47 +01:00
committed by GitHub
parent 01d8b99c38
commit be08233fa4
8 changed files with 241 additions and 15 deletions
@@ -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<string, any[]> | undefined,
},
},
});
@@ -179,7 +191,7 @@ export const WorkflowEditActionFindRecords = ({
<HorizontalSeparator noMargin />
{isDefined(selectedObjectMetadataItem) && (
<div>
<InputLabel>{t`Conditions`}</InputLabel>
<InputLabel>{t`Filter`}</InputLabel>
<RecordIndexContextProvider
value={{
indexIdentifierUrl: () => '',
@@ -229,10 +241,46 @@ export const WorkflowEditActionFindRecords = ({
</div>
)}
{isDefined(selectedObjectMetadataItem) && (
<>
<div>
<InputLabel>{t`Sort`}</InputLabel>
<WorkflowFindRecordsSorts
recordSorts={formData.orderBy?.recordSorts ?? []}
objectMetadataItem={selectedObjectMetadataItem}
onChange={(sorts: RecordSort[]) => {
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}
/>
</div>
</>
)}
<FormNumberFieldInput
label="Limit"
defaultValue={formData.limit}
placeholder="Enter limit"
readonly={isFormDisabled}
onChange={(limit) => {
if (isFormDisabled === true || !isNumber(limit)) {
return;
@@ -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<SelectOption<ViewSortDirection>> = [
{
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 (
<StyledContainer>
{recordSorts.map((sort) => {
return (
<StyledSortItemContainer key={sort.id}>
<Select
dropdownId={`workflow-find-records-sort-field-${sort.id}`}
fullWidth
disabled={readonly}
value={sort.fieldMetadataId}
options={sortableFields}
onChange={(value) => handleFieldChange(sort.id, value)}
withSearchInput
/>
<Select
dropdownId={`workflow-find-records-sort-direction-${sort.id}`}
fullWidth
disabled={readonly}
value={sort.direction}
options={directionOptions}
onChange={(value) =>
handleDirectionChange(sort.id, value as ViewSortDirection)
}
/>
{!readonly && (
<Button
onClick={() => handleRemoveSort(sort.id)}
Icon={IconTrash}
disabled={readonly}
/>
)}
</StyledSortItemContainer>
);
})}
<StyledAddButtonContainer>
<Button
Icon={IconArrowsSort}
size="small"
variant="secondary"
accent="default"
onClick={handleAddSort}
ariaLabel="Add sort"
title="Add sort"
disabled={readonly}
/>
</StyledAddButtonContainer>
</StyledContainer>
);
};
@@ -168,15 +168,14 @@ export const KeyValuePairInput = ({
/>
{!readonly &&
pair.id !== pairs[pairs.length - 1].id &&
!pair.isAutoSet ? (
<Button
onClick={() => handleRemovePair(pair.id)}
Icon={IconTrash}
/>
) : (
<Button Icon={IconTrash} disabled={true} />
)}
(pair.id !== pairs[pairs.length - 1].id && !pair.isAutoSet ? (
<Button
onClick={() => handleRemovePair(pair.id)}
Icon={IconTrash}
/>
) : (
<Button Icon={IconTrash} disabled={true} />
))}
</StyledKeyValueContainer>
))}
</StyledContainer>
@@ -177,7 +177,7 @@ export class FindRecordsService {
withFilterQueryBuilder,
orderByWithIdCondition,
objectName,
false,
true,
);
const queryBuilderWithSelect = this.applyRestrictedFieldsToQueryBuilder(
@@ -1,4 +1,7 @@
import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import {
type ObjectRecordFilter,
type ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type RecordCrudExecutionContext } from './execution-context.type';
import { type FindRecordsInput } from './record-crud-input.type';
@@ -10,5 +13,6 @@ export type FindRecordsParams = FindRecordsInput &
| Record<string, unknown>[]
| Partial<ObjectRecordFilter>
| Partial<ObjectRecordFilter>[];
orderBy?: Partial<ObjectRecordOrderBy>;
offset?: number;
};
@@ -32,7 +32,11 @@ export type FindRecordsInput = {
recordFilters?: any;
gqlOperationFilter?: Partial<ObjectRecordFilter>[];
};
orderBy?: Partial<ObjectRecordOrderBy>;
orderBy?: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
recordSorts?: any;
gqlOperationOrderBy?: Partial<ObjectRecordOrderBy>;
};
limit?: number;
};
@@ -67,7 +67,7 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
const toolOutput = await this.findRecordsService.execute({
objectName: workflowActionInput.objectName,
filter: workflowActionInput.filter?.gqlOperationFilter,
orderBy: workflowActionInput.orderBy,
orderBy: workflowActionInput.orderBy?.gqlOperationOrderBy,
limit: workflowActionInput.limit,
workspaceId,
rolePermissionConfig: executionContext.rolePermissionConfig,
@@ -13,5 +13,13 @@ export const workflowFindRecordsActionSettingsSchema =
gqlOperationFilter: z.any().optional().nullable(),
})
.optional(),
orderBy: z
.object({
recordSorts: z.array(z.any()).optional(),
gqlOperationOrderBy: z
.array(z.record(z.string(), z.any()))
.optional(),
})
.optional(),
}),
});