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 all 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
@@ -1,26 +1,41 @@
import VariableTagInput from '@/workflow/search-variables/components/VariableTagInput';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { JsonValue } from 'type-fest';

type FormFieldInputProps = {
recordFieldInputdId: string;
label: string;
value: string;
onChange: (value: string) => void;
isReadOnly?: boolean;
field: FieldMetadataItem;
defaultValue: JsonValue;
onPersist: (value: JsonValue) => void;
};

export const FormFieldInput = ({
recordFieldInputdId,
label,
onChange,
value,
field,
defaultValue,
onPersist,
}: FormFieldInputProps) => {
return (
<VariableTagInput
inputId={recordFieldInputdId}
label={label}
placeholder="Enter value (use {{variable}} for dynamic content)"
value={value}
onChange={onChange}
return isFieldNumber(field) ? (
<FormNumberFieldInput
key={field.id}
defaultValue={defaultValue as string | number | undefined}
onPersist={onPersist}
placeholder="Fill with number"
/>
);
) : isFieldBoolean(field) ? (
<FormBooleanFieldInput
key={field.id}
defaultValue={defaultValue as string | boolean | undefined}
onPersist={onPersist}
/>
) : isFieldText(field) ? (
<FormTextFieldInput
defaultValue={defaultValue as string | undefined}
onPersist={onPersist}
placeholder="Enter value tiptap"
/>
) : null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FormFieldInputBase } from '@/object-record/record-field/form-types/components/FormFieldInputBase';
import { BooleanInput } from '@/ui/field/input/components/BooleanInput';
import styled from '@emotion/styled';
import { useState } from 'react';

const StyledBooleanInputContainer = styled.div`
padding-inline: ${({ theme }) => theme.spacing(2)};
`;

type FormBooleanFieldInputProps = {
defaultValue: boolean | string | undefined;
onPersist: (value: boolean | null | string) => void;
readonly?: boolean;
};

export const FormBooleanFieldInput = ({
defaultValue,
onPersist,
readonly,
}: FormBooleanFieldInputProps) => {
const [draftValue, setDraftValue] = useState(defaultValue ?? false);

const handleChange = (newValue: boolean) => {
setDraftValue(newValue);

onPersist(newValue);
};

return (
<FormFieldInputBase
Input={
<StyledBooleanInputContainer>
<BooleanInput
value={draftValue as boolean}
readonly={readonly}
onToggle={(newValue) => {
handleChange(newValue);
}}
/>
</StyledBooleanInputContainer>
}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import styled from '@emotion/styled';

const LINE_HEIGHT = 24;

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

export 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` : undefined};
max-height: ${({ multiline }) =>
multiline ? `${5 * LINE_HEIGHT}px` : undefined};
`;

export 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;
overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')};
width: 100%;
`;

type FormFieldInputBaseProps<T> = {
Input: React.ReactElement;
RightElement?: React.ReactElement;
multiline?: boolean;
};

export const FormFieldInputBase = ({
Input,
RightElement,
multiline,
}: FormFieldInputBaseProps<unknown>) => {
return (
<StyledContainer>
<StyledInputContainer multiline={multiline}>
<StyledInputContainer2 multiline={multiline}>
{Input}
</StyledInputContainer2>

{RightElement}
</StyledInputContainer>
</StyledContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { FormFieldInputBase } from '@/object-record/record-field/form-types/components/FormFieldInputBase';
import { TextInput } from '@/ui/field/input/components/TextInput';
import styled from '@emotion/styled';
import { useState } from 'react';
import {
canBeCastAsNumberOrNull,
castAsNumberOrNull,
} from '~/utils/cast-as-number-or-null';

const StyledInput = styled(TextInput)`
padding: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(2)}`};
`;

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

export const FormNumberFieldInput = ({
placeholder,
defaultValue,
onPersist,
}: FormNumberFieldInputProps) => {
const [draftValue, setDraftValue] = useState(defaultValue ?? '');

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

const castedValue = castAsNumberOrNull(newValue);

onPersist(castedValue);
};

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

persistNumber(newText.trim());
};

return (
<FormFieldInputBase
Input={
<StyledInput
placeholder={placeholder}
value={String(draftValue)}
copyButton={false}
hotkeyScope="record-create"
onChange={(value) => {
handleChange(value);
}}
/>
}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { FormFieldInputBase } from '@/object-record/record-field/form-types/components/FormFieldInputBase';
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
import { parseEditorContent } from '@/workflow/search-variables/utils/parseEditorContent';
import { isDefined } from 'twenty-ui';

type FormTextFieldInputProps = {
defaultValue: string | undefined;
placeholder: string;
onPersist: (value: null | string) => void;
multiline?: boolean;
readonly?: boolean;
};

export const FormTextFieldInput = ({
defaultValue,
placeholder,
onPersist,
multiline,
readonly,
}: FormTextFieldInputProps) => {
// TODO: Might use a specific editor that doesn't know about variables (more lightweight)
const editor = useTextVariableEditor({
placeholder,
multiline,
readonly,
defaultValue,
onUpdate: (editor) => {
const jsonContent = editor.getJSON();
const parsedContent = parseEditorContent(jsonContent);

onPersist(parsedContent);
},
});

if (!isDefined(editor)) {
return null;
}

return (
<FormFieldInputBase
Input={
<TextVariableEditor
editor={editor}
multiline={multiline}
readonly={readonly}
/>
}
multiline={multiline}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { Editor, EditorContent } from '@tiptap/react';
import { ThemeType } from 'twenty-ui';

const LINE_HEIGHT = 24;

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 }>`
width: 100%;
display: flex;
box-sizing: border-box;
height: ${({ multiline }) => (multiline ? 'auto' : `${1.5 * LINE_HEIGHT}px`)};
padding-right: ${({ multiline, theme }) =>
multiline ? theme.spacing(4) : undefined};

.editor-content {
width: 100%;
}

.tiptap {
padding: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(2)}`};
box-sizing: border-box;
display: flex;
height: 100%;
overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')};
color: ${({ theme, readonly }) =>
readonly ? theme.font.color.light : theme.font.color.primary};
font-family: ${({ theme }) => theme.font.family};
font-weight: ${({ theme }) => theme.font.weight.regular};
border: none !important;
align-items: ${({ multiline }) => (multiline ? 'top' : 'center')};
white-space: ${({ multiline }) => (multiline ? 'pre' : 'nowrap')};

p.is-editor-empty:first-of-type::before {
content: attr(data-placeholder);
color: ${({ theme }) => theme.font.color.light};
float: left;
height: 0;
pointer-events: none;
}

p {
margin: 0;
}

.variable-tag {
${VARIABLE_TAG_STYLES}
}
}

.ProseMirror-focused {
outline: none;
}
`;

type TextVariableEditorProps = {
multiline: boolean | undefined;
readonly: boolean | undefined;
editor: Editor;
};

export const TextVariableEditor = ({
multiline,
readonly,
editor,
}: TextVariableEditorProps) => {
return (
<StyledEditor multiline={multiline} readonly={readonly}>
<EditorContent className="editor-content" editor={editor} />
</StyledEditor>
);
};
Loading
Loading