diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
index 31a7692f2..58642260a 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
@@ -2,9 +2,11 @@ import { useContext } from 'react';
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
+import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
+import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
import { isFieldChipDisplay } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
import { FieldContext } from '../contexts/FieldContext';
@@ -82,5 +84,7 @@ export const FieldDisplay = () => {
) : isFieldBoolean(fieldDefinition) ? (
+ ) : isFieldRating(fieldDefinition) ? (
+
) : null;
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RatingFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RatingFieldDisplay.tsx
new file mode 100644
index 000000000..7514480e4
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RatingFieldDisplay.tsx
@@ -0,0 +1,8 @@
+import { useRatingFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRatingFieldDisplay';
+import { RatingInput } from '@/ui/field/input/components/RatingInput';
+
+export const RatingFieldDisplay = () => {
+ const { rating } = useRatingFieldDisplay();
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RatingFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RatingFieldDisplay.perf.stories.tsx
new file mode 100644
index 000000000..cffa10142
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RatingFieldDisplay.perf.stories.tsx
@@ -0,0 +1,34 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { ComponentDecorator } from 'twenty-ui';
+
+import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
+import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
+import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
+import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
+
+const meta: Meta = {
+ title: 'UI/Data/Field/Display/RatingFieldDisplay',
+ decorators: [
+ MemoryRouterDecorator,
+ getFieldDecorator('person', 'testRating'),
+ ComponentDecorator,
+ ],
+ component: RatingFieldDisplay,
+ args: {},
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Performance = getProfilingStory({
+ componentName: 'RatingFieldDisplay',
+ averageThresholdInMs: 0.5,
+ numberOfRuns: 50,
+ numberOfTestsPerRun: 100,
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRatingFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRatingFieldDisplay.ts
new file mode 100644
index 000000000..f6d0ff660
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRatingFieldDisplay.ts
@@ -0,0 +1,23 @@
+import { useContext } from 'react';
+
+import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata';
+import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
+
+import { FieldContext } from '../../contexts/FieldContext';
+
+export const useRatingFieldDisplay = () => {
+ const { entityId, fieldDefinition } = useContext(FieldContext);
+
+ const fieldName = fieldDefinition.metadata.fieldName;
+
+ const fieldValue = useRecordFieldValue(entityId, fieldName) as
+ | FieldRatingValue
+ | undefined;
+
+ const rating = fieldValue ?? 'RATING_1';
+
+ return {
+ fieldDefinition,
+ rating,
+ };
+};
diff --git a/packages/twenty-front/src/modules/ui/field/input/components/RatingInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/RatingInput.tsx
index 58d1fd060..c5dd404e5 100644
--- a/packages/twenty-front/src/modules/ui/field/input/components/RatingInput.tsx
+++ b/packages/twenty-front/src/modules/ui/field/input/components/RatingInput.tsx
@@ -1,7 +1,6 @@
-import { useState } from 'react';
-import { useTheme } from '@emotion/react';
-import styled from '@emotion/styled';
-import { IconTwentyStarFilled } from 'twenty-ui';
+import { useContext, useState } from 'react';
+import { styled } from '@linaria/react';
+import { IconTwentyStarFilled, THEME_COMMON, ThemeContext } from 'twenty-ui';
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata';
@@ -11,29 +10,38 @@ const StyledContainer = styled.div`
display: flex;
`;
-const StyledRatingIconContainer = styled.div<{ isActive?: boolean }>`
- color: ${({ isActive, theme }) =>
- isActive ? theme.font.color.secondary : theme.background.quaternary};
+const StyledRatingIconContainer = styled.div<{
+ color: string;
+}>`
+ color: ${({ color }) => color};
display: inline-flex;
`;
type RatingInputProps = {
- onChange: (newValue: FieldRatingValue) => void;
+ onChange?: (newValue: FieldRatingValue) => void;
value: FieldRatingValue;
readonly?: boolean;
};
+const iconSizeMd = THEME_COMMON.icon.size.md;
+
export const RatingInput = ({
onChange,
value,
readonly,
}: RatingInputProps) => {
- const theme = useTheme();
+ const { theme } = useContext(ThemeContext);
+
+ const activeColor = theme.font.color.secondary;
+ const inactiveColor = theme.background.quaternary;
+
const [hoveredValue, setHoveredValue] = useState(
null,
);
const currentValue = hoveredValue ?? value;
+ const selectedIndex = RATING_VALUES.indexOf(currentValue);
+
return (
{RATING_VALUES.map((value, index) => {
- const currentIndex = RATING_VALUES.indexOf(currentValue);
+ const isActive = index <= selectedIndex;
return (
onChange(value)}
+ color={isActive ? activeColor : inactiveColor}
+ onClick={readonly ? undefined : () => onChange?.(value)}
onMouseEnter={readonly ? undefined : () => setHoveredValue(value)}
onMouseLeave={readonly ? undefined : () => setHoveredValue(null)}
>
-
+
);
})}
diff --git a/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts b/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts
index c31eaaa65..ff934beff 100644
--- a/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts
+++ b/packages/twenty-front/src/testing/mock-data/generated/standard-metadata-query-result.ts
@@ -2229,6 +2229,60 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
"toRelationMetadata": null
}
},
+ {
+ "__typename": "fieldEdge",
+ "node": {
+ "__typename": "field",
+ "id": "3715c0ac-c16f-4db3-b9be-e908b787929e",
+ "type": "RATING",
+ "name": "testRating",
+ "label": "Test rating",
+ "description": null,
+ "icon": "IconUsers",
+ "isCustom": true,
+ "isActive": true,
+ "isSystem": false,
+ "isNullable": true,
+ "createdAt": "2024-06-17T13:03:52.175Z",
+ "updatedAt": "2024-06-17T13:03:52.175Z",
+ "defaultValue": null,
+ "options": [
+ {
+ "id": "9876aaeb-91ac-4e02-b521-356ff0c0a6f9",
+ "label": "1",
+ "value": "RATING_1",
+ "position": 0
+ },
+ {
+ "id": "4651d042-0804-465b-8265-5fae554de3a8",
+ "label": "2",
+ "value": "RATING_2",
+ "position": 1
+ },
+ {
+ "id": "a6942bdd-a8c8-44f9-87fc-b9a7f64ee5dd",
+ "label": "3",
+ "value": "RATING_3",
+ "position": 2
+ },
+ {
+ "id": "a838666f-cd2f-4feb-a72f-d3447b23ad42",
+ "label": "4",
+ "value": "RATING_4",
+ "position": 3
+ },
+ {
+ "id": "428f765e-4792-4cea-8270-9dba60f45fd9",
+ "label": "5",
+ "value": "RATING_5",
+ "position": 4
+ }
+ ],
+ "relationDefinition": null,
+ "fromRelationMetadata": null,
+ "toRelationMetadata": null
+ }
+ },
{
"__typename": "fieldEdge",
"node": {
diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts
index 34055bdb1..3ed3cdcdb 100644
--- a/packages/twenty-front/vite.config.ts
+++ b/packages/twenty-front/vite.config.ts
@@ -63,6 +63,7 @@ export default defineConfig(({ command, mode }) => {
'**/Chip.tsx',
'**/Tag.tsx',
'**/MultiSelectFieldDisplay.tsx',
+ '**/RatingInput.tsx',
],
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
diff --git a/packages/twenty-ui/src/display/icon/components/IconTwentyStarFilled.tsx b/packages/twenty-ui/src/display/icon/components/IconTwentyStarFilled.tsx
index 465483017..c82ec6104 100644
--- a/packages/twenty-ui/src/display/icon/components/IconTwentyStarFilled.tsx
+++ b/packages/twenty-ui/src/display/icon/components/IconTwentyStarFilled.tsx
@@ -1,14 +1,14 @@
-import { useTheme } from '@emotion/react';
-
import IconTwentyStarFilledRaw from '@ui/display/icon/assets/twenty-star-filled.svg?react';
import { IconComponentProps } from '@ui/display/icon/types/IconComponent';
+import { THEME_COMMON } from '@ui/theme';
type IconTwentyStarFilledProps = Pick;
+const iconStrokeMd = THEME_COMMON.icon.stroke.md;
+
export const IconTwentyStarFilled = (props: IconTwentyStarFilledProps) => {
- const theme = useTheme();
const size = props.size ?? 24;
- const stroke = props.stroke ?? theme.icon.stroke.md;
+ const stroke = props.stroke ?? iconStrokeMd;
return (