Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create form field number #8634

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import SearchVariablesDropdown from '@/workflow/search-variables/components/SearchVariablesDropdown';
import { VARIABLE_TAG_STYLES } from '@/workflow/search-variables/components/VariableTagInput';
import { extractVariableLabel } from '@/workflow/search-variables/utils/extractVariableLabel';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useId, useState } from 'react';
import { IconX, TEXT_INPUT_STYLE, VisibilityHidden } from 'twenty-ui';
import {
canBeCastAsNumberOrNull,
castAsNumberOrNull,
} from '~/utils/cast-as-number-or-null';

const LINE_HEIGHT = 24;

const StyledContainer = styled.div`
display: flex;
flex-direction: column;
`;

const StyledInputContainer = styled.div<{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a common Overlay for primitive components

multiline?: boolean;
}>`
display: flex;
flex-direction: row;
position: relative;
line-height: ${({ multiline }) => (multiline ? `${LINE_HEIGHT}px` : 'auto')};
min-height: ${({ multiline }) =>
multiline ? `${3 * LINE_HEIGHT}px` : 'auto'};
max-height: ${({ multiline }) =>
multiline ? `${5 * LINE_HEIGHT}px` : 'auto'};
`;

const StyledInputContainer2 = styled.div<{
multiline?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multiline not needed for number

readonly?: boolean;
}>`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
border-bottom-right-radius: ${({ multiline, theme }) =>
multiline ? theme.border.radius.sm : 'none'};
border-right: ${({ multiline }) => (multiline ? 'auto' : 'none')};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-top-right-radius: ${({ multiline, theme }) =>
multiline ? theme.border.radius.sm : 'none'};
box-sizing: border-box;
display: flex;
height: ${({ multiline }) => (multiline ? 'auto' : `${1.5 * LINE_HEIGHT}px`)};
overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')};
/* padding-right: ${({ multiline, theme }) =>
multiline ? theme.spacing(6) : theme.spacing(2)}; */
width: 100%;
`;

const StyledInput = styled.input`
${TEXT_INPUT_STYLE}

padding: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(2)}`};
width: 100%;
`;

const StyledVariableContainer = styled.div`
${VARIABLE_TAG_STYLES}

margin: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(2)}`};
align-self: center;

display: flex;
align-items: center;
`;

const StyledSearchVariablesDropdownContainer = styled.div<{
multiline?: boolean;
readonly?: boolean;
}>`
align-items: center;
display: flex;
justify-content: center;

${({ theme, readonly }) =>
!readonly &&
`
:hover {
background-color: ${theme.background.transparent.light};
}`}

${({ theme, multiline }) =>
multiline
? `
position: absolute;
top: ${theme.spacing(0)};
right: ${theme.spacing(0)};
padding: ${theme.spacing(0.5)} ${theme.spacing(0)};
border-radius: ${theme.border.radius.sm};
`
: `
background-color: ${theme.background.transparent.lighter};
border-top-right-radius: ${theme.border.radius.sm};
border-bottom-right-radius: ${theme.border.radius.sm};
border: 1px solid ${theme.border.color.medium};
`}
`;

type EditingMode = 'input' | 'variable';

type FormNumberFieldInputProps = {
placeholder: string;
defaultValue: string | undefined;
onPersist: (value: number | null | string) => void;
};

export const FormNumberFieldInput = ({
placeholder,
defaultValue,
onPersist,
}: FormNumberFieldInputProps) => {
const theme = useTheme();

const id = useId();

const [draftValue, setDraftValue] = useState(defaultValue ?? '');
const [editingMode, setEditingMode] = useState<EditingMode>(() => {
return defaultValue?.startsWith('{{') ? 'variable' : 'input';
});

const persistNumber = (newValue: string) => {
if (!canBeCastAsNumberOrNull(newValue)) {
return;
}

const castedValue = castAsNumberOrNull(newValue);

onPersist(castedValue);
};

const handleChange = (newText: string) => {
setDraftValue(newText);

persistNumber(newText.trim());
};

return (
<StyledContainer>
<StyledInputContainer>
<StyledInputContainer2>
{editingMode === 'input' ? (
<StyledInput
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's separate:

  • FormNumberFieldInput should be a simple number input with a FormOverlay to add design
  • Variable button / dropdown should come from the parent

type="text"
placeholder={placeholder}
value={draftValue}
onChange={(event) => {
handleChange(event.target.value);
}}
/>
) : (
<StyledVariableContainer>
{extractVariableLabel(draftValue)}

<button
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we have a component already for this?

style={{
all: 'unset',
display: 'inline-flex',
cursor: 'pointer',
marginLeft: theme.spacing(1),
}}
onClick={() => {
setDraftValue('');
setEditingMode('input');
onPersist(null);
}}
>
<VisibilityHidden>Unlink the variable</VisibilityHidden>

<IconX size={theme.icon.size.sm} />
</button>
</StyledVariableContainer>
)}
</StyledInputContainer2>

<StyledSearchVariablesDropdownContainer
multiline={false}
readonly={false}
>
<SearchVariablesDropdown
inputId={id}
insertVariableTag={(variable) => {
setDraftValue(variable);
setEditingMode('variable');
onPersist(variable);
}}
disabled={false}
/>
</StyledSearchVariablesDropdownContainer>
</StyledInputContainer>
</StyledContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';

type WorkflowEditActionFormFieldProps = {
defaultValue: string;
};

export const WorkflowEditActionFormField = ({
defaultValue,
}: WorkflowEditActionFormFieldProps) => {
return (
<FormNumberFieldInput
defaultValue={defaultValue}
placeholder="Placeholder"
onPersist={(value) => {
console.log('save value to database', value);
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowEditActionFormField } from '@/workflow/components/WorkflowEditActionFormField';
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
import { WorkflowRecordCreateAction } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
Expand Down Expand Up @@ -152,15 +152,20 @@ export const WorkflowEditActionFormRecordCreate = ({
<HorizontalSeparator noMargin />

{editableFields.map((field) => (
<FormFieldInput
<WorkflowEditActionFormField
key={field.id}
recordFieldInputdId={field.id}
label={field.label}
value={formData[field.name] as string}
onChange={(value) => {
handleFieldChange(field.name, value);
}}
defaultValue={formData[field.name] as string}
/>

// <FormFieldInput
// key={field.id}
// recordFieldInputdId={field.id}
// label={field.label}
// value={formData[field.name] as string}
// onChange={(value) => {
// handleFieldChange(field.name, value);
// }}
// />
))}
</WorkflowEditGenericFormBase>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useAvailableVariablesInWorkflowStep } from '@/workflow/search-variables
import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Editor } from '@tiptap/react';
import { useState } from 'react';
import { IconVariablePlus } from 'twenty-ui';

Expand All @@ -29,11 +28,11 @@ const StyledDropdownVariableButtonContainer = styled(

const SearchVariablesDropdown = ({
inputId,
editor,
insertVariableTag,
disabled,
}: {
inputId: string;
editor: Editor;
insertVariableTag: (variable: string) => void;
disabled?: boolean;
}) => {
const theme = useTheme();
Expand All @@ -47,10 +46,6 @@ const SearchVariablesDropdown = ({
StepOutputSchema | undefined
>(undefined);

const insertVariableTag = (variable: string) => {
editor.commands.insertVariableTag(variable);
};

const handleStepSelect = (stepId: string) => {
setSelectedStep(
availableVariablesInWorkflowStep.find((step) => step.id === stepId),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import SearchVariablesDropdown from '@/workflow/search-variables/components/Sear
import { initializeEditorContent } from '@/workflow/search-variables/utils/initializeEditorContent';
import { parseEditorContent } from '@/workflow/search-variables/utils/parseEditorContent';
import { VariableTag } from '@/workflow/search-variables/utils/variableTag';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import Document from '@tiptap/extension-document';
import HardBreak from '@tiptap/extension-hard-break';
import Paragraph from '@tiptap/extension-paragraph';
import Placeholder from '@tiptap/extension-placeholder';
import Text from '@tiptap/extension-text';
import { EditorContent, useEditor } from '@tiptap/react';
import { isDefined } from 'twenty-ui';
import { isDefined, ThemeType } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';

const LINE_HEIGHT = 24;
Expand Down Expand Up @@ -71,6 +72,13 @@ const StyledSearchVariablesDropdownContainer = styled.div<{
`}
`;

export const VARIABLE_TAG_STYLES = ({ theme }: { theme: ThemeType }) => css`
background-color: ${theme.color.blue10};
border-radius: ${theme.border.radius.sm};
color: ${theme.color.blue};
padding: ${theme.spacing(1)};
`;

const StyledEditor = styled.div<{ multiline?: boolean; readonly?: boolean }>`
display: flex;
width: 100%;
Expand Down Expand Up @@ -119,10 +127,7 @@ const StyledEditor = styled.div<{ multiline?: boolean; readonly?: boolean }>`
}

.variable-tag {
color: ${({ theme }) => theme.color.blue};
background-color: ${({ theme }) => theme.color.blue10};
padding: ${({ theme }) => theme.spacing(1)};
border-radius: ${({ theme }) => theme.border.radius.sm};
${VARIABLE_TAG_STYLES}
}
}

Expand Down Expand Up @@ -224,7 +229,9 @@ export const VariableTagInput = ({
>
<SearchVariablesDropdown
inputId={inputId}
editor={editor}
insertVariableTag={(variable) => {
editor.commands.insertVariableTag(variable);
}}
disabled={readonly}
/>
</StyledSearchVariablesDropdownContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { extractVariableLabel } from '../extractVariableLabel';

it('returns the last part of a properly formatted variable', () => {
const rawVariable = '{{a.b.c}}';

expect(extractVariableLabel(rawVariable)).toBe('c');
});

it('stops on unclosed variables', () => {
const rawVariable = '{{ test {{a.b.c}}';

expect(extractVariableLabel(rawVariable)).toBe('c');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const extractVariableLabel = (rawVariable: string) => {
const variableWithoutBrackets = rawVariable.replace(
/\{\{([^{}]+)\}\}/g,
(_, variable) => {
return variable;
},
);

const parts = variableWithoutBrackets.split('.');
const displayText = parts.at(-1);

return displayText;
};
Loading