Add sort section in find records action (#15340)
https://github.com/user-attachments/assets/020f3e97-316b-41db-b95d-7b938a7f82cf
This commit is contained in:
+50
-2
@@ -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;
|
||||
|
||||
+163
@@ -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>
|
||||
);
|
||||
};
|
||||
+8
-9
@@ -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>
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ export class FindRecordsService {
|
||||
withFilterQueryBuilder,
|
||||
orderByWithIdCondition,
|
||||
objectName,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
const queryBuilderWithSelect = this.applyRestrictedFieldsToQueryBuilder(
|
||||
|
||||
+5
-1
@@ -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;
|
||||
};
|
||||
|
||||
+5
-1
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user