mirror of
https://github.com/lingble/twenty.git
synced 2025-10-31 20:57:55 +00:00
feat: start creating the form number field input
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
import SearchVariablesDropdown from '@/workflow/search-variables/components/SearchVariablesDropdown';
|
||||||
|
import { VARIABLE_TAG_STYLES } from '@/workflow/search-variables/components/VariableTagInput';
|
||||||
|
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<{
|
||||||
|
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;
|
||||||
|
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
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={draftValue}
|
||||||
|
onChange={(event) => {
|
||||||
|
handleChange(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StyledVariableContainer>
|
||||||
|
{draftValue}
|
||||||
|
|
||||||
|
<button
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
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 { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
|
import { WorkflowEditActionFormField } from '@/workflow/components/WorkflowEditActionFormField';
|
||||||
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||||
import { WorkflowRecordCreateAction } from '@/workflow/types/Workflow';
|
import { WorkflowRecordCreateAction } from '@/workflow/types/Workflow';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
@@ -152,15 +152,20 @@ export const WorkflowEditActionFormRecordCreate = ({
|
|||||||
<HorizontalSeparator noMargin />
|
<HorizontalSeparator noMargin />
|
||||||
|
|
||||||
{editableFields.map((field) => (
|
{editableFields.map((field) => (
|
||||||
<FormFieldInput
|
<WorkflowEditActionFormField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
recordFieldInputdId={field.id}
|
defaultValue={formData[field.name] as string}
|
||||||
label={field.label}
|
|
||||||
value={formData[field.name] as string}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleFieldChange(field.name, value);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
// <FormFieldInput
|
||||||
|
// key={field.id}
|
||||||
|
// recordFieldInputdId={field.id}
|
||||||
|
// label={field.label}
|
||||||
|
// value={formData[field.name] as string}
|
||||||
|
// onChange={(value) => {
|
||||||
|
// handleFieldChange(field.name, value);
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
))}
|
))}
|
||||||
</WorkflowEditGenericFormBase>
|
</WorkflowEditGenericFormBase>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import SearchVariablesDropdown from '@/workflow/search-variables/components/Sear
|
|||||||
import { initializeEditorContent } from '@/workflow/search-variables/utils/initializeEditorContent';
|
import { initializeEditorContent } from '@/workflow/search-variables/utils/initializeEditorContent';
|
||||||
import { parseEditorContent } from '@/workflow/search-variables/utils/parseEditorContent';
|
import { parseEditorContent } from '@/workflow/search-variables/utils/parseEditorContent';
|
||||||
import { VariableTag } from '@/workflow/search-variables/utils/variableTag';
|
import { VariableTag } from '@/workflow/search-variables/utils/variableTag';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import Document from '@tiptap/extension-document';
|
import Document from '@tiptap/extension-document';
|
||||||
import HardBreak from '@tiptap/extension-hard-break';
|
import HardBreak from '@tiptap/extension-hard-break';
|
||||||
@@ -9,7 +10,7 @@ import Paragraph from '@tiptap/extension-paragraph';
|
|||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
import Text from '@tiptap/extension-text';
|
import Text from '@tiptap/extension-text';
|
||||||
import { EditorContent, useEditor } from '@tiptap/react';
|
import { EditorContent, useEditor } from '@tiptap/react';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined, ThemeType } from 'twenty-ui';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
const LINE_HEIGHT = 24;
|
const LINE_HEIGHT = 24;
|
||||||
@@ -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 }>`
|
const StyledEditor = styled.div<{ multiline?: boolean; readonly?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -119,10 +127,7 @@ const StyledEditor = styled.div<{ multiline?: boolean; readonly?: boolean }>`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.variable-tag {
|
.variable-tag {
|
||||||
color: ${({ theme }) => theme.color.blue};
|
${VARIABLE_TAG_STYLES}
|
||||||
background-color: ${({ theme }) => theme.color.blue10};
|
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user