diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx index 762cc0b97..c2f69cb70 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx @@ -14,6 +14,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; @@ -113,7 +114,13 @@ export const CalendarEventDetails = ({ maxWidth: 300, }} > - + + + )); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx index 8358b9f16..00d73aaef 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx @@ -8,6 +8,7 @@ import { RecordUpdateHook, RecordUpdateHookParams, } from '@/object-record/record-field/contexts/FieldContext'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; @@ -61,7 +62,13 @@ export const RecordBoardCardBody = ({ hotkeyScope: InlineCellHotkeyScope.InlineCell, }} > - + + + ))} diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx index 7875ecfe9..f486a5a68 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx @@ -17,7 +17,6 @@ import { isFieldRelationToOneObject } from '@/object-record/record-field/types/g import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput'; -import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; @@ -72,114 +71,108 @@ export const FieldInput = ({ const { fieldDefinition } = useContext(FieldContext); return ( - - - {isFieldRelationToOneObject(fieldDefinition) ? ( - - ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( - - ) : isFieldPhones(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> - ) : isFieldText(fieldDefinition) ? ( - - ) : isFieldEmails(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> - ) : isFieldFullName(fieldDefinition) ? ( - - ) : isFieldDateTime(fieldDefinition) ? ( - - ) : isFieldDate(fieldDefinition) ? ( - - ) : isFieldNumber(fieldDefinition) ? ( - - ) : isFieldLinks(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> - ) : isFieldCurrency(fieldDefinition) ? ( - - ) : isFieldBoolean(fieldDefinition) ? ( - - ) : isFieldRating(fieldDefinition) ? ( - - ) : isFieldSelect(fieldDefinition) ? ( - - ) : isFieldMultiSelect(fieldDefinition) ? ( - - ) : isFieldAddress(fieldDefinition) ? ( - - ) : isFieldRawJson(fieldDefinition) ? ( - - ) : isFieldArray(fieldDefinition) ? ( - onClickOutside?.(() => {}, event)} - /> - ) : ( - <> - )} - - + {isFieldRelationToOneObject(fieldDefinition) ? ( + + ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( + + ) : isFieldPhones(fieldDefinition) ? ( + onClickOutside?.(() => {}, event)} + /> + ) : isFieldText(fieldDefinition) ? ( + + ) : isFieldEmails(fieldDefinition) ? ( + onClickOutside?.(() => {}, event)} + /> + ) : isFieldFullName(fieldDefinition) ? ( + + ) : isFieldDateTime(fieldDefinition) ? ( + + ) : isFieldDate(fieldDefinition) ? ( + + ) : isFieldNumber(fieldDefinition) ? ( + + ) : isFieldLinks(fieldDefinition) ? ( + onClickOutside?.(() => {}, event)} + /> + ) : isFieldCurrency(fieldDefinition) ? ( + + ) : isFieldBoolean(fieldDefinition) ? ( + + ) : isFieldRating(fieldDefinition) ? ( + + ) : isFieldSelect(fieldDefinition) ? ( + + ) : isFieldMultiSelect(fieldDefinition) ? ( + + ) : isFieldAddress(fieldDefinition) ? ( + + ) : isFieldRawJson(fieldDefinition) ? ( + + ) : isFieldArray(fieldDefinition) ? ( + onClickOutside?.(() => {}, event)} + /> + ) : ( + <> + )} + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx index 3c8d1630c..639e96cb4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx @@ -1,6 +1,8 @@ import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField'; import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem'; +import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState'; import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useCallback, useMemo } from 'react'; import { isDefined } from 'twenty-shared'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -44,6 +46,14 @@ export const EmailsFieldInput = ({ const isPrimaryEmail = (index: number) => index === 0 && emails?.length > 1; + const setIsFieldInError = useSetRecoilComponentStateV2( + recordFieldInputIsFieldInErrorComponentState, + ); + + const handleError = (hasError: boolean, values: any[]) => { + setIsFieldInError(hasError && values.length === 0); + }; + return ( )} + onError={handleError} hotkeyScope={hotkeyScope} /> ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx index b1cff0767..bafa43d05 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx @@ -1,5 +1,7 @@ import { useLinksField } from '@/object-record/record-field/meta-types/hooks/useLinksField'; import { LinksFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/LinksFieldMenuItem'; +import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useMemo } from 'react'; import { absoluteUrlSchema, isDefined } from 'twenty-shared'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -47,6 +49,14 @@ export const LinksFieldInput = ({ const isPrimaryLink = (index: number) => index === 0 && links?.length > 1; + const setIsFieldInError = useSetRecoilComponentStateV2( + recordFieldInputIsFieldInErrorComponentState, + ); + + const handleError = (hasError: boolean, values: any[]) => { + setIsFieldInError(hasError && values.length === 0); + }; + return ( ({ url: input, label: '' })} renderItem={({ value: link, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemBaseInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemBaseInput.tsx index 4663b3989..b7e0554a3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemBaseInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemBaseInput.tsx @@ -20,6 +20,13 @@ const StyledInput = styled.input<{ border-radius: 4px; border: 1px solid ${theme.border.color.medium}; `} + + ${({ hasError, hasItem, theme }) => + hasError && + hasItem && + css` + border: 1px solid ${theme.border.color.danger}; + `} box-sizing: border-box; font-weight: ${({ theme }) => theme.font.weight.medium}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx index a0359c215..9ed480831 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx @@ -36,6 +36,7 @@ type MultiItemFieldInputProps = { fieldMetadataType: FieldMetadataType; renderInput?: MultiItemBaseInputProps['renderInput']; onClickOutside?: (event: MouseEvent | TouchEvent) => void; + onError?: (hasError: boolean, values: any[]) => void; }; // Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ... @@ -53,6 +54,7 @@ export const MultiItemFieldInput = ({ fieldMetadataType, renderInput, onClickOutside, + onError, }: MultiItemFieldInputProps) => { const containerRef = useRef(null); const handleDropdownClose = () => { @@ -85,6 +87,7 @@ export const MultiItemFieldInput = ({ setErrorData( errorData.isValid ? errorData : { isValid: true, errorMessage: '' }, ); + onError?.(false, items); }; const handleAddButtonClick = () => { @@ -123,6 +126,7 @@ export const MultiItemFieldInput = ({ if (validateInput !== undefined) { const validationData = validateInput(inputValue) ?? { isValid: true }; if (!validationData.isValid) { + onError?.(true, items); setErrorData(validationData); return; } diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState.ts new file mode 100644 index 000000000..cdcbade0c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState.ts @@ -0,0 +1,9 @@ +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const recordFieldInputIsFieldInErrorComponentState = + createComponentStateV2({ + key: 'recordFieldInputIsFieldInErrorComponentState', + defaultValue: false, + componentInstanceContext: RecordFieldComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx index 2d7a35a74..7db711c49 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx @@ -1,9 +1,11 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState'; import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState'; import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import styled from '@emotion/styled'; import { @@ -63,6 +65,10 @@ export const RecordInlineCellEditMode = ({ }, }; + const isFieldInError = useRecoilComponentValueV2( + recordFieldInputIsFieldInErrorComponentState, + ); + const { refs, floatingStyles } = useFloating({ placement: isCentered ? 'bottom' : 'bottom-start', middleware: [ @@ -93,6 +99,7 @@ export const RecordInlineCellEditMode = ({ ref={refs.setFloating} style={floatingStyles} borderRadius="sm" + hasDangerBorder={isFieldInError} > {children} , diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx index a313f55a9..22a0b3868 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx @@ -6,6 +6,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader'; @@ -141,7 +142,13 @@ export const FieldsCard = ({ hotkeyScope: InlineCellHotkeyScope.InlineCell, }} > - + + + ))} diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx index b1d124054..615f5ecad 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx @@ -28,6 +28,7 @@ import { } from '@/object-record/record-field/contexts/FieldContext'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; @@ -280,7 +281,13 @@ export const RecordDetailRelationRecordsListItem = ({ hotkeyScope: InlineCellHotkeyScope.InlineCell, }} > - + + + ), )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx index ac4aeb29a..b9468a10f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditMode.tsx @@ -1,8 +1,10 @@ import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/states/recordFieldInputIsFieldInErrorComponentState'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState'; import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionLoadingComponentState'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import styled from '@emotion/styled'; import { @@ -35,6 +37,10 @@ export const RecordTableCellEditMode = ({ }: RecordTableCellEditModeProps) => { const { recordId, fieldDefinition } = useContext(FieldContext); + const isFieldInError = useRecoilComponentValueV2( + recordFieldInputIsFieldInErrorComponentState, + ); + const instanceId = getRecordFieldInputId( recordId, fieldDefinition?.metadata?.fieldName, @@ -84,6 +90,7 @@ export const RecordTableCellEditMode = ({ ref={refs.setFloating} style={floatingStyles} borderRadius="sm" + hasDangerBorder={isFieldInError} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx index c45457300..c37638dc3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx @@ -2,6 +2,7 @@ import { ReactNode, useContext } from 'react'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; @@ -87,7 +88,11 @@ export const RecordTableCellFieldContextWrapper = ({ displayedMaxRows: 1, }} > - {children} + + {children} + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/overlay/components/OverlayContainer.tsx b/packages/twenty-front/src/modules/ui/layout/overlay/components/OverlayContainer.tsx index a3265a8b0..b4e434c24 100644 --- a/packages/twenty-front/src/modules/ui/layout/overlay/components/OverlayContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/overlay/components/OverlayContainer.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; // eslint-disable-next-line @nx/workspace-styled-components-prefixed-with-styled export const OverlayContainer = styled.div<{ borderRadius?: 'sm' | 'md'; + hasDangerBorder?: boolean; }>` align-items: center; display: flex; @@ -14,7 +15,9 @@ export const OverlayContainer = styled.div<{ theme.border.radius[borderRadius ?? 'md']}; background: ${({ theme }) => theme.background.transparent.primary}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; + border: 1px solid + ${({ theme, hasDangerBorder }) => + theme.border.color[hasDangerBorder ? 'danger' : 'medium']}; box-shadow: ${({ theme }) => theme.boxShadow.strong}; overflow: hidden;