Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9154f7a4c2 | |||
| 1ce64f5028 |
+17
-15
@@ -56,29 +56,29 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
type FormFieldInputProps = {
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'label' | 'metadata' | 'type'>;
|
||||
defaultValue: JsonValue;
|
||||
error?: string;
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'label' | 'metadata' | 'type'>;
|
||||
onChange: (value: JsonValue) => void;
|
||||
onClear?: () => void;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
readonly?: boolean;
|
||||
placeholder?: string;
|
||||
error?: string;
|
||||
onError?: (error: string | undefined) => void;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
timeZone?: string;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const FormFieldInput = ({
|
||||
field,
|
||||
defaultValue,
|
||||
error,
|
||||
field,
|
||||
onChange,
|
||||
onClear,
|
||||
VariablePicker,
|
||||
readonly,
|
||||
placeholder,
|
||||
error,
|
||||
onError,
|
||||
placeholder,
|
||||
readonly,
|
||||
timeZone,
|
||||
VariablePicker,
|
||||
}: FormFieldInputProps) => {
|
||||
return isFieldNumber(field) || field.type === FieldMetadataType.NUMERIC ? (
|
||||
<FormNumberFieldInput
|
||||
@@ -168,21 +168,23 @@ export const FormFieldInput = ({
|
||||
/>
|
||||
) : isFieldDate(field) ? (
|
||||
<FormDateFieldInput
|
||||
label={field.label}
|
||||
defaultValue={defaultValue as string | undefined}
|
||||
error={error}
|
||||
label={field.label}
|
||||
onChange={onChange}
|
||||
VariablePicker={VariablePicker}
|
||||
readonly={readonly}
|
||||
onError={onError}
|
||||
placeholder={placeholder}
|
||||
readonly={readonly}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
) : isFieldDateTime(field) ? (
|
||||
<FormDateTimeFieldInput
|
||||
label={field.label}
|
||||
defaultValue={defaultValue as string | undefined}
|
||||
label={field.label}
|
||||
onChange={onChange}
|
||||
VariablePicker={VariablePicker}
|
||||
readonly={readonly}
|
||||
timeZone={timeZone}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
) : isFieldMultiSelect(field) ? (
|
||||
<FormMultiSelectFieldInput
|
||||
|
||||
+58
-103
@@ -1,17 +1,16 @@
|
||||
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
|
||||
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
|
||||
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
|
||||
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
|
||||
import { VariableChipStandalone } from '@/object-record/record-field/ui/form-types/components/VariableChipStandalone';
|
||||
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
|
||||
import { InputHint } from '@/ui/input/components/InputHint';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { DatePicker } from '@/ui/input/components/internal/date/components/DatePicker';
|
||||
import { DatePickerInput } from '@/ui/input/components/internal/date/components/DatePickerInput';
|
||||
import {
|
||||
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||
} from '@/ui/input/components/internal/date/components/DateTimePicker';
|
||||
import { useParseDateInputStringToPlainDate } from '@/ui/input/components/internal/date/hooks/useParseDateInputStringToPlainDate';
|
||||
import { useParsePlainDateToDateInputString } from '@/ui/input/components/internal/date/hooks/useParsePlainDateToDateInputString';
|
||||
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
@@ -20,25 +19,21 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { styled } from '@linaria/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import {
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
type ChangeEvent,
|
||||
type KeyboardEvent,
|
||||
} from 'react';
|
||||
import { useId, useRef, useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type Nullable } from 'twenty-ui/utilities';
|
||||
import { getDateFormatStringForDatePickerInputMask } from '~/utils/date-utils';
|
||||
|
||||
const StyledInputContainerWrapper = styled.div`
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 0;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDateInputAbsoluteContainer = styled.div`
|
||||
@@ -46,31 +41,11 @@ const StyledDateInputAbsoluteContainer = styled.div`
|
||||
top: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledDateInput = styled.input<{ hasError?: boolean }>`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: ${({ hasError }) =>
|
||||
hasError
|
||||
? themeCssVariables.color.red
|
||||
: themeCssVariables.font.color.primary};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-family: ${themeCssVariables.font.family};
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
outline: none;
|
||||
|
||||
&::placeholder,
|
||||
&::-webkit-input-placeholder {
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
font-family: ${themeCssVariables.font.family};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
}
|
||||
|
||||
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[2]};
|
||||
const StyledDateInputTextContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDateInputContainer = styled.div`
|
||||
@@ -90,29 +65,31 @@ type DraftValue =
|
||||
};
|
||||
|
||||
type FormDateFieldInputProps = {
|
||||
label?: string;
|
||||
defaultValue: string | undefined;
|
||||
error?: string;
|
||||
label?: string;
|
||||
onChange: (value: string | null) => void;
|
||||
onError?: (error: string | undefined) => void;
|
||||
placeholder?: string;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
readonly?: boolean;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const FormDateFieldInput = ({
|
||||
label,
|
||||
defaultValue,
|
||||
error: errorFromProps,
|
||||
label,
|
||||
onChange,
|
||||
VariablePicker,
|
||||
onError,
|
||||
placeholder: _placeholder,
|
||||
readonly,
|
||||
placeholder,
|
||||
VariablePicker,
|
||||
}: FormDateFieldInputProps) => {
|
||||
const instanceId = useId();
|
||||
const { dateFormat } = useDateTimeFormat();
|
||||
|
||||
const { parsePlainDateToDateInputString } =
|
||||
useParsePlainDateToDateInputString();
|
||||
const { parseDateInputStringToPlainDate } =
|
||||
useParseDateInputStringToPlainDate();
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const [draftValue, setDraftValue] = useState<DraftValue>(
|
||||
isStandaloneVariableString(defaultValue)
|
||||
@@ -139,12 +116,6 @@ export const FormDateFieldInput = ({
|
||||
|
||||
const datePickerWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [inputDate, setInputDate] = useState(
|
||||
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
|
||||
? parsePlainDateToDateInputString(draftValueAsDate)
|
||||
: '',
|
||||
);
|
||||
|
||||
const persistDate = (newDate: Nullable<string>) => {
|
||||
if (!isDefined(newDate)) {
|
||||
onChange(null);
|
||||
@@ -159,11 +130,6 @@ export const FormDateFieldInput = ({
|
||||
const displayDatePicker =
|
||||
draftValue.type === 'static' && draftValue.mode === 'edit';
|
||||
|
||||
const defaultPlaceHolder =
|
||||
getDateFormatStringForDatePickerInputMask(dateFormat);
|
||||
|
||||
const placeholderToDisplay = placeholder ?? defaultPlaceHolder;
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [datePickerWrapperRef],
|
||||
listenerId: 'FormDateTimeFieldInputBase',
|
||||
@@ -182,16 +148,15 @@ export const FormDateFieldInput = ({
|
||||
});
|
||||
|
||||
const handlePickerChange = (newDate: Nullable<string>) => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
mode: 'edit',
|
||||
value: newDate ?? null,
|
||||
});
|
||||
|
||||
setInputDate(
|
||||
isDefined(newDate) ? parsePlainDateToDateInputString(newDate) : '',
|
||||
);
|
||||
|
||||
setPickerDate(newDate);
|
||||
|
||||
persistDate(newDate);
|
||||
@@ -216,6 +181,9 @@ export const FormDateFieldInput = ({
|
||||
};
|
||||
|
||||
const handlePickerClear = () => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: null,
|
||||
@@ -224,12 +192,13 @@ export const FormDateFieldInput = ({
|
||||
|
||||
setPickerDate(null);
|
||||
|
||||
setInputDate('');
|
||||
|
||||
persistDate(null);
|
||||
};
|
||||
|
||||
const handlePickerMouseSelect = (newDate: Nullable<string>) => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: newDate ?? null,
|
||||
@@ -238,10 +207,6 @@ export const FormDateFieldInput = ({
|
||||
|
||||
setPickerDate(newDate);
|
||||
|
||||
setInputDate(
|
||||
isDefined(newDate) ? parsePlainDateToDateInputString(newDate) : '',
|
||||
);
|
||||
|
||||
persistDate(newDate);
|
||||
};
|
||||
|
||||
@@ -253,55 +218,42 @@ export const FormDateFieldInput = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputDate(event.target.value);
|
||||
};
|
||||
const handleMaskedDateChange = (newPlainDate: string | null) => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
const handleInputKeydown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key !== 'Enter') {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputDateTime = inputDate.trim();
|
||||
|
||||
if (inputDateTime === '') {
|
||||
if (!isDefined(newPlainDate)) {
|
||||
handlePickerClear();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedInputPlainDate = parseDateInputStringToPlainDate(inputDateTime);
|
||||
|
||||
if (!isDefined(parsedInputPlainDate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let validatedDate = parsedInputPlainDate;
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: validatedDate,
|
||||
mode: 'edit',
|
||||
value: newPlainDate,
|
||||
});
|
||||
|
||||
setPickerDate(validatedDate);
|
||||
setPickerDate(newPlainDate);
|
||||
|
||||
setInputDate(parsePlainDateToDateInputString(validatedDate));
|
||||
|
||||
persistDate(validatedDate);
|
||||
persistDate(newPlainDate);
|
||||
};
|
||||
|
||||
const handleVariableTagInsert = (variableName: string) => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'variable',
|
||||
value: variableName,
|
||||
});
|
||||
|
||||
setInputDate('');
|
||||
|
||||
onChange(variableName);
|
||||
};
|
||||
|
||||
const handleUnlinkVariable = () => {
|
||||
setErrorMessage(undefined);
|
||||
onError?.(undefined);
|
||||
|
||||
setDraftValue({
|
||||
type: 'static',
|
||||
value: null,
|
||||
@@ -313,6 +265,8 @@ export const FormDateFieldInput = ({
|
||||
onChange(null);
|
||||
};
|
||||
|
||||
const error = errorMessage ?? errorFromProps;
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Escape],
|
||||
callback: handlePickerEscape,
|
||||
@@ -332,16 +286,15 @@ export const FormDateFieldInput = ({
|
||||
>
|
||||
{draftValue.type === 'static' ? (
|
||||
<>
|
||||
<StyledDateInput
|
||||
type="text"
|
||||
placeholder={placeholderToDisplay}
|
||||
value={inputDate}
|
||||
onFocus={handleInputFocus}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeydown}
|
||||
disabled={readonly}
|
||||
/>
|
||||
|
||||
<StyledDateInputTextContainer>
|
||||
<DatePickerInput
|
||||
date={pickerDate ?? null}
|
||||
hasError={isDefined(error)}
|
||||
onChange={handleMaskedDateChange}
|
||||
onFocus={handleInputFocus}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</StyledDateInputTextContainer>
|
||||
{draftValue.mode === 'edit' ? (
|
||||
<StyledDateInputContainer>
|
||||
<StyledDateInputAbsoluteContainer>
|
||||
@@ -377,6 +330,8 @@ export const FormDateFieldInput = ({
|
||||
/>
|
||||
) : null}
|
||||
</FormFieldInputRowContainer>
|
||||
|
||||
{error ? <InputHint danger>{error}</InputHint> : null}
|
||||
</FormFieldInputContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+10
-4
@@ -28,10 +28,13 @@ import { type Nullable } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledInputContainerWrapper = styled.div`
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 0;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDateInputAbsoluteContainer = styled.div`
|
||||
@@ -42,6 +45,8 @@ const StyledDateInputAbsoluteContainer = styled.div`
|
||||
const StyledDateInputTextContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledDateInputContainer = styled.div`
|
||||
@@ -61,22 +66,22 @@ type DraftValue =
|
||||
};
|
||||
|
||||
type FormDateTimeFieldInputProps = {
|
||||
label?: string;
|
||||
defaultValue: string | undefined;
|
||||
label?: string;
|
||||
onChange: (value: string | null) => void;
|
||||
placeholder?: string;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
readonly?: boolean;
|
||||
timeZone?: string;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const FormDateTimeFieldInput = ({
|
||||
label,
|
||||
defaultValue,
|
||||
label,
|
||||
onChange,
|
||||
VariablePicker,
|
||||
readonly,
|
||||
timeZone,
|
||||
VariablePicker,
|
||||
}: FormDateTimeFieldInputProps) => {
|
||||
const instanceId = useId();
|
||||
|
||||
@@ -259,6 +264,7 @@ export const FormDateTimeFieldInput = ({
|
||||
<StyledDateInputTextContainer>
|
||||
<DateTimePickerInput
|
||||
date={dateValue}
|
||||
fullWidth
|
||||
onChange={handleInputChange}
|
||||
onFocus={handleInputFocus}
|
||||
readonly={readonly}
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
|
||||
const StyledFormFieldInputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
|
||||
+4
-2
@@ -23,8 +23,10 @@ const StyledFormFieldInputInnerContainer = styled.div<
|
||||
border-bottom-left-radius: ${themeCssVariables.border.radius.sm};
|
||||
border-bottom-right-radius: ${({ multiline, hasRightElement }) =>
|
||||
multiline || !hasRightElement ? themeCssVariables.border.radius.sm : '0'};
|
||||
border-right: ${({ multiline, hasRightElement }) =>
|
||||
multiline || !hasRightElement ? 'auto' : 'none'};
|
||||
border-right: ${({ hasRightElement }) =>
|
||||
hasRightElement
|
||||
? 'none'
|
||||
: `1px solid ${themeCssVariables.border.color.medium}`};
|
||||
border-top-left-radius: ${themeCssVariables.border.radius.sm};
|
||||
border-top-right-radius: ${({ multiline, hasRightElement }) =>
|
||||
multiline || !hasRightElement ? themeCssVariables.border.radius.sm : '0'};
|
||||
|
||||
+3
-1
@@ -9,14 +9,16 @@ const StyledFormFieldInputRowContainer = styled.div<{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: ${({ multiline }) => (multiline ? 'auto' : '32px')};
|
||||
|
||||
line-height: ${({ multiline }) =>
|
||||
multiline ? `${LINE_HEIGHT}px` : 'normal'};
|
||||
max-height: ${({ multiline, maxHeight }) =>
|
||||
multiline ? `${maxHeight ?? 5 * LINE_HEIGHT}px` : 'none'};
|
||||
|
||||
min-height: ${({ multiline }) =>
|
||||
multiline ? `${3 * LINE_HEIGHT}px` : 'auto'};
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const FormFieldInputRowContainer = StyledFormFieldInputRowContainer;
|
||||
|
||||
+4
@@ -30,9 +30,13 @@ import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTabListContainer = styled.div`
|
||||
|
||||
+38
-10
@@ -22,6 +22,7 @@ const StyledInputContainer = styled.div`
|
||||
border-top-right-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
height: ${themeCssVariables.spacing[8]};
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
@@ -32,19 +33,34 @@ const StyledInput = styled.input<{ hasError?: boolean }>`
|
||||
hasError
|
||||
? themeCssVariables.color.red
|
||||
: themeCssVariables.font.color.primary};
|
||||
flex: 1 1 auto;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
outline: none;
|
||||
padding: 4px 8px 4px 8px;
|
||||
width: 100%;
|
||||
|
||||
&:disabled {
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
type DatePickerInputProps = {
|
||||
onChange?: (date: string | null) => void;
|
||||
date: string | null;
|
||||
hasError?: boolean;
|
||||
onChange?: (date: string | null) => void;
|
||||
onFocus?: () => void;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const DatePickerInput = ({ date, onChange }: DatePickerInputProps) => {
|
||||
export const DatePickerInput = ({
|
||||
date,
|
||||
hasError,
|
||||
onChange,
|
||||
onFocus,
|
||||
readonly,
|
||||
}: DatePickerInputProps) => {
|
||||
const { dateFormat } = useDateTimeFormat();
|
||||
|
||||
const [internalDate, setInternalDate] = useState(date);
|
||||
@@ -71,15 +87,17 @@ export const DatePickerInput = ({ date, onChange }: DatePickerInputProps) => {
|
||||
? (parsePlainDateToDateInputString(internalDate) ?? undefined)
|
||||
: undefined;
|
||||
|
||||
const { ref, setValue, value } = useIMask(
|
||||
const { ref, setValue } = useIMask(
|
||||
{
|
||||
mask: Date,
|
||||
pattern,
|
||||
blocks,
|
||||
min: MIN_DATE,
|
||||
max: MAX_DATE,
|
||||
format: (date: any) =>
|
||||
isDefined(date) ? parseIMaskJSDateIMaskDateInputString(date) : '',
|
||||
format: (dateValue: unknown) =>
|
||||
isDefined(dateValue) && dateValue instanceof Date
|
||||
? parseIMaskJSDateIMaskDateInputString(dateValue)
|
||||
: '',
|
||||
parse: parseIMaskDateInputStringToJSDate,
|
||||
lazy: false,
|
||||
autofix: true,
|
||||
@@ -89,25 +107,35 @@ export const DatePickerInput = ({ date, onChange }: DatePickerInputProps) => {
|
||||
onComplete: (newValue) => {
|
||||
const parsedDate = parseDateInputStringToPlainDate(newValue);
|
||||
|
||||
onChange?.(parsedDate);
|
||||
onChange?.(isDefined(parsedDate) ? parsedDate : null);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(date) && internalDate !== date) {
|
||||
setInternalDate(date);
|
||||
if (date === internalDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInternalDate(date);
|
||||
|
||||
if (isDefined(date)) {
|
||||
setValue(parsePlainDateToDateInputString(date));
|
||||
} else {
|
||||
setValue('');
|
||||
}
|
||||
}, [date, internalDate, parsePlainDateToDateInputString, setValue]);
|
||||
|
||||
const shouldDisplayReadOnly = readonly === true;
|
||||
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
disabled={shouldDisplayReadOnly}
|
||||
hasError={hasError}
|
||||
type="text"
|
||||
ref={ref as any}
|
||||
value={value}
|
||||
onChange={() => {}} // Prevent React warning
|
||||
onFocus={!shouldDisplayReadOnly ? onFocus : undefined}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
);
|
||||
|
||||
+11
-6
@@ -20,30 +20,33 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDifferentZonedDateTime } from '~/utils/dates/isDifferentZonedDateTime';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
const StyledInputContainer = styled.div<{ fullWidth?: boolean }>`
|
||||
align-items: center;
|
||||
|
||||
border-top-left-radius: ${themeCssVariables.border.radius.md};
|
||||
border-top-right-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
height: ${themeCssVariables.spacing[8]};
|
||||
min-width: ${({ fullWidth }) => (fullWidth === true ? '0' : 'auto')};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input<{ hasError?: boolean }>`
|
||||
const StyledInput = styled.input<{ fullWidth?: boolean; hasError?: boolean }>`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
flex: ${({ fullWidth }) => (fullWidth === true ? '1 1 auto' : 'none')};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: 500;
|
||||
min-width: ${({ fullWidth }) => (fullWidth === true ? '0' : 'auto')};
|
||||
outline: none;
|
||||
padding-left: ${themeCssVariables.spacing[2]};
|
||||
width: 140px;
|
||||
width: ${({ fullWidth }) => (fullWidth === true ? '100%' : '140px')};
|
||||
`;
|
||||
|
||||
type DateTimePickerInputProps = {
|
||||
onChange?: (date: Temporal.ZonedDateTime | null) => void;
|
||||
date: Temporal.ZonedDateTime | null;
|
||||
fullWidth?: boolean;
|
||||
onChange?: (date: Temporal.ZonedDateTime | null) => void;
|
||||
onFocus?: () => void;
|
||||
readonly?: boolean;
|
||||
timeZone?: string;
|
||||
@@ -51,6 +54,7 @@ type DateTimePickerInputProps = {
|
||||
|
||||
export const DateTimePickerInput = ({
|
||||
date,
|
||||
fullWidth = false,
|
||||
onChange,
|
||||
onFocus,
|
||||
readonly,
|
||||
@@ -167,9 +171,10 @@ export const DateTimePickerInput = ({
|
||||
internalDate?.toInstant() ?? Temporal.Now.instant();
|
||||
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledInputContainer fullWidth={fullWidth}>
|
||||
<StyledInput
|
||||
disabled={shouldDisplayReadOnly}
|
||||
fullWidth={fullWidth}
|
||||
type="text"
|
||||
ref={ref as any}
|
||||
onFocus={!shouldDisplayReadOnly ? onFocus : undefined}
|
||||
|
||||
+3
@@ -13,17 +13,20 @@ const StyledWorkflowStepBody = styled.div<{
|
||||
gridTemplateRows?: string;
|
||||
}>`
|
||||
background: ${themeCssVariables.background.primary};
|
||||
box-sizing: border-box;
|
||||
display: ${({ display }) => display ?? 'flex'};
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
grid-template-rows: ${({ gridTemplateRows }) => gridTemplateRows ?? 'none'};
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
overflow: ${({ overflow }) => overflow ?? 'hidden scroll'};
|
||||
padding-block: ${({ paddingBlock }) =>
|
||||
paddingBlock ?? themeCssVariables.spacing[4]};
|
||||
padding-inline: ${({ paddingInline }) =>
|
||||
paddingInline ?? themeCssVariables.spacing[3]};
|
||||
row-gap: ${({ rowGap }) => rowGap ?? themeCssVariables.spacing[4]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const WorkflowStepBody = ({
|
||||
|
||||
+57
-43
@@ -17,8 +17,8 @@ import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-
|
||||
import { styled } from '@linaria/react';
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -83,6 +83,8 @@ const StyledTrashButtonContainer = styled.div`
|
||||
|
||||
const StyledFormFieldInputContainerWrapper = styled.div`
|
||||
grid-area: input;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledOpenedSettingsContainer = styled.div`
|
||||
@@ -93,11 +95,13 @@ const StyledFieldContainer = styled.div<{
|
||||
readonly?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: ${({ readonly }) => (readonly ? 'default' : 'pointer')};
|
||||
display: flex;
|
||||
font-family: inherit;
|
||||
min-height: 0;
|
||||
padding-left: ${themeCssVariables.spacing[2]};
|
||||
padding-right: ${themeCssVariables.spacing[2]};
|
||||
|
||||
@@ -116,10 +120,19 @@ const StyledPlaceholderContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAddFieldButtonContainer = styled.div`
|
||||
padding-left: ${themeCssVariables.spacing[7]};
|
||||
padding-right: ${themeCssVariables.spacing[7]};
|
||||
const StyledAddFieldButtonOuterGrid = styled.div`
|
||||
column-gap: ${themeCssVariables.spacing[1]};
|
||||
display: grid;
|
||||
grid-template-columns: 24px 1fr 24px;
|
||||
min-width: 0;
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAddFieldButtonGridCenter = styled.div`
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledAddFieldButtonContentContainer = styled.div`
|
||||
@@ -323,8 +336,7 @@ export const WorkflowEditActionFormBuilder = ({
|
||||
>
|
||||
<StyledPlaceholderContainer>
|
||||
<FormFieldPlaceholder>
|
||||
{isDefined(field.placeholder) &&
|
||||
isNonEmptyString(field.placeholder)
|
||||
{isNonEmptyString(field.placeholder)
|
||||
? field.placeholder
|
||||
: getDefaultFormFieldSettings(field.type)
|
||||
.placeholder}
|
||||
@@ -391,47 +403,49 @@ export const WorkflowEditActionFormBuilder = ({
|
||||
/>
|
||||
|
||||
{!actionOptions.readonly && (
|
||||
<StyledAddFieldButtonContainer>
|
||||
<FormFieldInputContainer>
|
||||
<FormFieldInputRowContainer>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId="add-field-button"
|
||||
hasRightElement={false}
|
||||
onClick={() => {
|
||||
const { label, name } = getDefaultFormFieldSettings(
|
||||
FieldMetadataType.TEXT,
|
||||
);
|
||||
<StyledAddFieldButtonOuterGrid>
|
||||
<StyledAddFieldButtonGridCenter>
|
||||
<FormFieldInputContainer>
|
||||
<FormFieldInputRowContainer>
|
||||
<FormFieldInputInnerContainer
|
||||
formFieldInputInstanceId="add-field-button"
|
||||
hasRightElement={false}
|
||||
onClick={() => {
|
||||
const { label, name } = getDefaultFormFieldSettings(
|
||||
FieldMetadataType.TEXT,
|
||||
);
|
||||
|
||||
const newField: WorkflowFormActionField = {
|
||||
id: v4(),
|
||||
name,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label,
|
||||
};
|
||||
const newField: WorkflowFormActionField = {
|
||||
id: v4(),
|
||||
name,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label,
|
||||
};
|
||||
|
||||
setFormData([...formData, newField]);
|
||||
setFormData([...formData, newField]);
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: [...action.settings.input, newField],
|
||||
},
|
||||
});
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: [...action.settings.input, newField],
|
||||
},
|
||||
});
|
||||
|
||||
setSelectedField(newField.id);
|
||||
}}
|
||||
>
|
||||
<StyledFieldContainer>
|
||||
<StyledAddFieldButtonContentContainer>
|
||||
<IconPlus size={theme.icon.size.sm} />
|
||||
{t`Add Field`}
|
||||
</StyledAddFieldButtonContentContainer>
|
||||
</StyledFieldContainer>
|
||||
</FormFieldInputInnerContainer>
|
||||
</FormFieldInputRowContainer>
|
||||
</FormFieldInputContainer>
|
||||
</StyledAddFieldButtonContainer>
|
||||
setSelectedField(newField.id);
|
||||
}}
|
||||
>
|
||||
<StyledFieldContainer>
|
||||
<StyledAddFieldButtonContentContainer>
|
||||
<IconPlus size={theme.icon.size.sm} />
|
||||
{t`Add Field`}
|
||||
</StyledAddFieldButtonContentContainer>
|
||||
</StyledFieldContainer>
|
||||
</FormFieldInputInnerContainer>
|
||||
</FormFieldInputRowContainer>
|
||||
</FormFieldInputContainer>
|
||||
</StyledAddFieldButtonGridCenter>
|
||||
</StyledAddFieldButtonOuterGrid>
|
||||
)}
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
|
||||
+89
-64
@@ -1,22 +1,44 @@
|
||||
import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton';
|
||||
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
|
||||
import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton';
|
||||
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
|
||||
import { WorkflowFormFieldInput } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowFormFieldInput';
|
||||
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
|
||||
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
const StyledWorkflowFormFillerRoot = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledWorkflowFormFillerFields = styled.div`
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type WorkflowEditActionFormFillerProps = {
|
||||
action: WorkflowFormAction;
|
||||
actionOptions: {
|
||||
@@ -99,86 +121,89 @@ export const WorkflowEditActionFormFiller = ({
|
||||
}, [saveAction]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledWorkflowFormFillerRoot>
|
||||
<WorkflowStepBody>
|
||||
{formData.map((field) => {
|
||||
if (field.type === 'RECORD') {
|
||||
const objectNameSingular = field.settings?.objectName;
|
||||
<StyledWorkflowFormFillerFields>
|
||||
{formData.map((field) => {
|
||||
if (field.type === 'RECORD') {
|
||||
const objectNameSingular = field.settings?.objectName;
|
||||
|
||||
if (!isDefined(objectNameSingular)) {
|
||||
return null;
|
||||
if (!isDefined(objectNameSingular)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recordId = field.value?.id;
|
||||
|
||||
return (
|
||||
<FormSingleRecordPicker
|
||||
key={field.id}
|
||||
label={field.label}
|
||||
defaultValue={recordId}
|
||||
onChange={(recordId) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value: {
|
||||
id: recordId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
objectNameSingulars={[objectNameSingular]}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const recordId = field.value?.id;
|
||||
if (field.type === 'SELECT' || field.type === 'MULTI_SELECT') {
|
||||
const selectedFieldId = field.settings?.selectedFieldId;
|
||||
|
||||
return (
|
||||
<FormSingleRecordPicker
|
||||
key={field.id}
|
||||
label={field.label}
|
||||
defaultValue={recordId}
|
||||
onChange={(recordId) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value: {
|
||||
id: recordId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
objectNameSingulars={[objectNameSingular]}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!isDefined(selectedFieldId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (field.type === 'SELECT' || field.type === 'MULTI_SELECT') {
|
||||
const selectedFieldId = field.settings?.selectedFieldId;
|
||||
|
||||
if (!isDefined(selectedFieldId)) {
|
||||
return null;
|
||||
return (
|
||||
<WorkflowFormFieldInput
|
||||
key={field.id}
|
||||
fieldMetadataId={selectedFieldId}
|
||||
defaultValue={field.value}
|
||||
readonly={actionOptions.readonly}
|
||||
onChange={(value) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowFormFieldInput
|
||||
<FormFieldInput
|
||||
key={field.id}
|
||||
fieldMetadataId={selectedFieldId}
|
||||
defaultValue={field.value}
|
||||
readonly={actionOptions.readonly}
|
||||
field={{
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
metadata: {} as FieldMetadata,
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
onError={(error) => {
|
||||
setError(error);
|
||||
}}
|
||||
placeholder={
|
||||
isNonEmptyString(field.placeholder)
|
||||
? field.placeholder
|
||||
: getDefaultFormFieldSettings(field.type).placeholder
|
||||
}
|
||||
readonly={actionOptions.readonly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={field.id}
|
||||
field={{
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
metadata: {} as FieldMetadata,
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onFieldUpdate({
|
||||
fieldId: field.id,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
readonly={actionOptions.readonly}
|
||||
placeholder={
|
||||
field.placeholder ??
|
||||
getDefaultFormFieldSettings(field.type).placeholder
|
||||
}
|
||||
onError={(error) => {
|
||||
setError(error);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
})}
|
||||
</StyledWorkflowFormFillerFields>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && (
|
||||
<SidePanelFooter
|
||||
@@ -191,6 +216,6 @@ export const WorkflowEditActionFormFiller = ({
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</StyledWorkflowFormFillerRoot>
|
||||
);
|
||||
};
|
||||
|
||||
-1
@@ -69,7 +69,6 @@ const mockAction: WorkflowFormAction = {
|
||||
name: 'date',
|
||||
label: 'Date',
|
||||
type: FieldMetadataType.DATE,
|
||||
placeholder: 'mm/dd/yyyy',
|
||||
settings: {},
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user