mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-31 04:37:56 +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 { 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'; | ||||
| @@ -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> | ||||
|   ); | ||||
|   | ||||
| @@ -2,6 +2,7 @@ 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'; | ||||
| @@ -9,7 +10,7 @@ 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; | ||||
| @@ -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%; | ||||
| @@ -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} | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Devessier
					Devessier