feat: improve the design of the fields widget (#17003)

Closes [2005](https://github.com/twentyhq/core-team-issues/issues/2005).

This is how it looks like and I have a feeling that it matches the Figma
design. I am not sure if we need to remove more padding as mentioned in
the issue since removing it takes it away from the Figma design. Please
review and let me know.

<img width="329" height="807" alt="image"
src="https://github.com/user-attachments/assets/1d9051e4-81fc-4c43-9aa8-54857bbd8f8f"
/>

<br />
<br />

Figma design itself:

<img width="912" height="1052" alt="image"
src="https://github.com/user-attachments/assets/4174a4a3-22af-4afc-8632-a0838ec5af08"
/>

In terms of data that we receive, I believe it would be handled by
Fields Configuration coming from the Backend, so General and Other are
decided at that layer, unless I am missing something.
This commit is contained in:
Abdullah.
2026-01-09 15:27:10 +05:00
committed by GitHub
parent 8fc0bb3ed7
commit 2bb3b41e62
3 changed files with 88 additions and 18 deletions
@@ -25,26 +25,60 @@ export const useTemporaryFieldsConfiguration = (
field.type !== FieldMetadataType.RICH_TEXT_V2,
);
const fields = fieldsToDisplay.map((field, index) => ({
fieldMetadataId: field.id,
position: index,
}));
if (fieldsToDisplay.length === 0) {
return null;
}
if (fields.length === 0) {
const generalFields: Array<{ fieldMetadataId: string; position: number }> =
[];
const otherFields: Array<{ fieldMetadataId: string; position: number }> =
[];
let generalPosition = 0;
let otherPosition = 0;
fieldsToDisplay.forEach((field) => {
if (field.type === FieldMetadataType.LINKS) {
otherFields.push({
fieldMetadataId: field.id,
position: otherPosition++,
});
} else {
generalFields.push({
fieldMetadataId: field.id,
position: generalPosition++,
});
}
});
const sections = [];
if (generalFields.length > 0) {
sections.push({
id: `${objectNameSingular}-section-general`,
title: t`General`,
position: 0,
fields: generalFields,
});
}
if (otherFields.length > 0) {
sections.push({
id: `${objectNameSingular}-section-other`,
title: t`Other`,
position: 1,
fields: otherFields,
});
}
if (sections.length === 0) {
return null;
}
return {
__typename: 'FieldsConfiguration',
configurationType: 'FIELDS',
sections: [
{
id: `${objectNameSingular}-section-general`,
title: t`General`,
position: 0,
fields,
},
],
sections,
};
}, [objectMetadataItem, objectNameSingular, t]);
@@ -1,14 +1,22 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Section } from 'twenty-ui/layout';
import { useState } from 'react';
import { IconChevronDown } from 'twenty-ui/display';
import { AnimatedExpandableContainer, Section } from 'twenty-ui/layout';
const StyledFieldsWidgetSectionContainer = styled(Section)`
padding-top: ${({ theme }) => theme.spacing(3)};
padding-bottom: ${({ theme }) => theme.spacing(3)};
padding-bottom: 0;
width: auto;
&:not(:first-of-type) {
padding-top: 0;
}
`;
const StyledHeader = styled.header`
align-items: center;
cursor: pointer;
display: flex;
height: 24px;
justify-content: space-between;
@@ -17,9 +25,18 @@ const StyledHeader = styled.header`
`;
const StyledTitleLabel = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledChevronIcon = styled(IconChevronDown)<{ isExpanded: boolean }>`
color: ${({ theme }) => theme.font.color.tertiary};
transform: ${({ isExpanded }) =>
isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'};
transition: ${({ theme }) =>
`transform ${theme.animation.duration.normal}s ease`};
`;
type FieldsWidgetSectionContainerProps = {
children: React.ReactNode;
title: string;
@@ -29,12 +46,29 @@ export const FieldsWidgetSectionContainer = ({
children,
title,
}: FieldsWidgetSectionContainerProps) => {
const [isExpanded, setIsExpanded] = useState(true);
const theme = useTheme();
const handleToggleSection = () =>
setIsExpanded((previousIsExpanded) => !previousIsExpanded);
return (
<StyledFieldsWidgetSectionContainer>
<StyledHeader>
<StyledHeader onClick={handleToggleSection}>
<StyledTitleLabel>{title}</StyledTitleLabel>
<StyledChevronIcon
isExpanded={isExpanded}
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
</StyledHeader>
{children}
<AnimatedExpandableContainer
isExpanded={isExpanded}
initial={false}
mode="fit-content"
>
{children}
</AnimatedExpandableContainer>
</StyledFieldsWidgetSectionContainer>
);
};
@@ -30,6 +30,7 @@ type AnimatedExpandableContainerProps = {
animationDurations?: AnimationDurations;
mode?: AnimationMode;
containAnimation?: boolean;
initial?: boolean;
};
export const AnimatedExpandableContainer = ({
@@ -39,6 +40,7 @@ export const AnimatedExpandableContainer = ({
animationDurations = 'default',
mode = 'scroll-height',
containAnimation = true,
initial = true,
}: AnimatedExpandableContainerProps) => {
const theme = useTheme();
const contentRef = useRef<HTMLDivElement>(null);
@@ -71,7 +73,7 @@ export const AnimatedExpandableContainer = ({
);
return (
<AnimatePresence>
<AnimatePresence initial={initial}>
{isExpanded && (
<StyledMotionContainer
containAnimation={containAnimation}