{(draggableProvided, draggableSnapshot) => {
const draggableStyle = draggableProvided.draggableProps.style;
- const isDragged = draggableSnapshot.isDragging;
+ const isDragging = draggableSnapshot.isDragging;
+
return (
- {itemComponent}
+ {isFunction(itemComponent)
+ ? itemComponent({
+ isDragging,
+ })
+ : itemComponent}
);
}}
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx
index c6ad92d00..0629a57bf 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormBuilder.tsx
@@ -2,6 +2,8 @@ import { FormFieldInputContainer } from '@/object-record/record-field/form-types
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { InputLabel } from '@/ui/input/components/InputLabel';
+import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
+import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
@@ -13,19 +15,21 @@ import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-ac
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
+import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
-import { useEffect, useState } from 'react';
-import { useDebouncedCallback } from 'use-debounce';
-
import { isNonEmptyString } from '@sniptt/guards';
+import { useEffect, useState } from 'react';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
IconChevronDown,
+ IconGripVertical,
IconPlus,
IconTrash,
useIcons,
} from 'twenty-ui/display';
+import { LightIconButton } from 'twenty-ui/input';
+import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
export type WorkflowEditActionFormBuilderProps = {
@@ -42,10 +46,45 @@ export type WorkflowEditActionFormBuilderProps = {
type FormData = WorkflowFormActionField[];
-const StyledRowContainer = styled.div`
+const StyledWorkflowStepBody = styled(WorkflowStepBody)`
+ display: block;
+ padding-inline: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledFormFieldContainer = styled.div`
+ align-items: flex-end;
column-gap: ${({ theme }) => theme.spacing(1)};
display: grid;
- grid-template-columns: 1fr 16px;
+ grid-template-areas:
+ 'grip input delete'
+ '. settings .';
+ grid-template-columns: 24px 1fr 24px;
+ position: relative;
+`;
+
+const StyledDraggingIndicator = styled.div`
+ position: absolute;
+ inset: ${({ theme }) => theme.spacing(-2)};
+ top: ${({ theme }) => theme.spacing(-1)};
+ background-color: ${({ theme }) => theme.background.transparent.light};
+`;
+
+const StyledLightGripIconButton = styled(LightIconButton)`
+ grid-area: grip;
+ margin-bottom: ${({ theme }) => theme.spacing(1)};
+`;
+
+const StyledLightTrashIconButton = styled(LightIconButton)`
+ grid-area: delete;
+ margin-bottom: ${({ theme }) => theme.spacing(1)};
+`;
+
+const StyledFormFieldInputContainer = styled(FormFieldInputContainer)`
+ grid-area: input;
+`;
+
+const StyledOpenedSettingsContainer = styled.div`
+ grid-area: settings;
`;
const StyledFieldContainer = styled.div`
@@ -71,22 +110,8 @@ const StyledPlaceholder = styled.div`
width: 100%;
`;
-const StyledIconButtonContainer = styled.div`
- align-items: center;
- border-radius: ${({ theme }) => theme.border.radius.md};
- display: flex;
- justify-content: center;
- width: 24px;
-
- cursor: pointer;
-
- &:hover,
- &[data-open='true'] {
- background-color: ${({ theme }) => theme.background.transparent.lighter};
- }
-`;
-
const StyledAddFieldButtonContainer = styled.div`
+ padding-inline: ${({ theme }) => theme.spacing(7)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
@@ -100,10 +125,6 @@ const StyledAddFieldButtonContentContainer = styled.div`
width: 100%;
`;
-const StyledLabelContainer = styled.div`
- min-height: 17px;
-`;
-
export const WorkflowEditActionFormBuilder = ({
action,
actionOptions,
@@ -121,8 +142,11 @@ export const WorkflowEditActionFormBuilder = ({
const [selectedField, setSelectedField] = useState(null);
const [hoveredField, setHoveredField] = useState(null);
+
const isFieldSelected = (fieldName: string) => selectedField === fieldName;
+
const isFieldHovered = (fieldName: string) => hoveredField === fieldName;
+
const handleFieldClick = (fieldName: string) => {
if (actionOptions.readonly === true) {
return;
@@ -149,6 +173,27 @@ export const WorkflowEditActionFormBuilder = ({
saveAction(updatedFormData);
};
+ const handleDragEnd: OnDragEndResponder = ({ source, destination }) => {
+ if (actionOptions.readonly === true) {
+ return;
+ }
+
+ const movedField = formData.at(source.index);
+
+ if (!isDefined(movedField) || !isDefined(destination)) {
+ return;
+ }
+
+ const copiedFormData = [...formData];
+
+ copiedFormData.splice(source.index, 1);
+ copiedFormData.splice(destination.index, 0, movedField);
+
+ setFormData(copiedFormData);
+
+ saveAction(copiedFormData);
+ };
+
const saveAction = useDebouncedCallback(async (formData: FormData) => {
if (actionOptions.readonly === true) {
return;
@@ -188,121 +233,158 @@ export const WorkflowEditActionFormBuilder = ({
headerType={headerType}
disabled={actionOptions.readonly}
/>
-
- {formData.map((field) => (
-
-
- {field.label || ''}
-
+
+
+ {formData.map((field, index) => (
+ {
+ const showButtons =
+ !actionOptions.readonly &&
+ (isFieldSelected(field.id) ||
+ isFieldHovered(field.id) ||
+ isDragging);
- setHoveredField(field.id)}
- onMouseLeave={() => setHoveredField(null)}
- >
+ return (
+ setHoveredField(field.id)}
+ onMouseLeave={() => setHoveredField(null)}
+ >
+ {isDragging && }
+
+ {showButtons && (
+
+ )}
+
+
+ {field.label || ''}
+
+
+ {
+ handleFieldClick(field.id);
+ }}
+ >
+
+
+ {isDefined(field.placeholder) &&
+ isNonEmptyString(field.placeholder)
+ ? field.placeholder
+ : getDefaultFormFieldSettings(field.type)
+ .placeholder}
+
+ {field.type === 'RECORD' && (
+
+ )}
+
+
+
+
+
+ {showButtons && (
+ {
+ const updatedFormData = formData.filter(
+ (currentField) => currentField.id !== field.id,
+ );
+
+ setFormData(updatedFormData);
+
+ actionOptions.onActionUpdate({
+ ...action,
+ settings: {
+ ...action.settings,
+ input: updatedFormData,
+ },
+ });
+ }}
+ />
+ )}
+
+ {isFieldSelected(field.id) && (
+
+ {
+ setSelectedField(null);
+ }}
+ />
+
+ )}
+
+ );
+ }}
+ />
+ ))}
+ >
+ }
+ />
+
+ {!actionOptions.readonly && (
+
+
{
- handleFieldClick(field.id);
+ const { label, name } = getDefaultFormFieldSettings(
+ FieldMetadataType.TEXT,
+ );
+
+ const newField: WorkflowFormActionField = {
+ id: v4(),
+ name,
+ type: FieldMetadataType.TEXT,
+ label,
+ };
+
+ setFormData([...formData, newField]);
+
+ actionOptions.onActionUpdate({
+ ...action,
+ settings: {
+ ...action.settings,
+ input: [...action.settings.input, newField],
+ },
+ });
+
+ setSelectedField(newField.id);
}}
>
-
- {isDefined(field.placeholder) &&
- isNonEmptyString(field.placeholder)
- ? field.placeholder
- : getDefaultFormFieldSettings(field.type).placeholder}
-
- {field.type === 'RECORD' && (
-
- )}
+
+
+ {t`Add Field`}
+
- {!actionOptions.readonly &&
- (isFieldSelected(field.id) || isFieldHovered(field.id)) && (
-
- {
- const updatedFormData = formData.filter(
- (currentField) => currentField.id !== field.id,
- );
-
- setFormData(updatedFormData);
-
- actionOptions.onActionUpdate({
- ...action,
- settings: {
- ...action.settings,
- input: updatedFormData,
- },
- });
- }}
- />
-
- )}
- {isFieldSelected(field.id) && (
- {
- setSelectedField(null);
- }}
- />
- )}
-
-
- ))}
- {!actionOptions.readonly && (
-
-
-
-
- {
- const { label, name } = getDefaultFormFieldSettings(
- FieldMetadataType.TEXT,
- );
-
- const newField: WorkflowFormActionField = {
- id: v4(),
- name,
- type: FieldMetadataType.TEXT,
- label,
- };
-
- setFormData([...formData, newField]);
-
- actionOptions.onActionUpdate({
- ...action,
- settings: {
- ...action.settings,
- input: [...action.settings.input, newField],
- },
- });
-
- setSelectedField(newField.id);
- }}
- >
-
-
-
- {t`Add Field`}
-
-
-
-
-
-
+