mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 20:27: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
	 Devessier
					Devessier