-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
base: main
Are you sure you want to change the base?
Create form field number #8634
Changes from 3 commits
78b7df5
06999e9
119e207
581b94a
479c697
d5d71fe
a304a8d
7c3791a
510e0ff
1354d73
78475fc
2b98332
1bb162c
6734134
791a401
58dd960
5c9f0fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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<{ | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's separate:
|
||
type="text" | ||
placeholder={placeholder} | ||
value={draftValue} | ||
onChange={(event) => { | ||
handleChange(event.target.value); | ||
}} | ||
/> | ||
) : ( | ||
<StyledVariableContainer> | ||
{extractVariableLabel(draftValue)} | ||
|
||
<button | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
@@ -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; | ||
}; |
There was a problem hiding this comment.
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