Added and optimized missing RatingFieldDisplay component (#5904)
The display for Rating field type was missing, I just added it based on RatingInput in readonly mode and optimized a bit for performance also. Fixes https://github.com/twentyhq/twenty/issues/5900
This commit is contained in:
@ -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 = () => {
|
||||
<JsonFieldDisplay />
|
||||
) : isFieldBoolean(fieldDefinition) ? (
|
||||
<BooleanFieldDisplay />
|
||||
) : isFieldRating(fieldDefinition) ? (
|
||||
<RatingFieldDisplay />
|
||||
) : null;
|
||||
};
|
||||
|
||||
@ -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 <RatingInput value={rating} readonly />;
|
||||
};
|
||||
@ -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<typeof RatingFieldDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Performance = getProfilingStory({
|
||||
componentName: 'RatingFieldDisplay',
|
||||
averageThresholdInMs: 0.5,
|
||||
numberOfRuns: 50,
|
||||
numberOfTestsPerRun: 100,
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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<FieldRatingValue | null>(
|
||||
null,
|
||||
);
|
||||
const currentValue = hoveredValue ?? value;
|
||||
|
||||
const selectedIndex = RATING_VALUES.indexOf(currentValue);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
role="slider"
|
||||
@ -44,17 +52,17 @@ export const RatingInput = ({
|
||||
tabIndex={0}
|
||||
>
|
||||
{RATING_VALUES.map((value, index) => {
|
||||
const currentIndex = RATING_VALUES.indexOf(currentValue);
|
||||
const isActive = index <= selectedIndex;
|
||||
|
||||
return (
|
||||
<StyledRatingIconContainer
|
||||
key={index}
|
||||
isActive={index <= currentIndex}
|
||||
onClick={readonly ? undefined : () => onChange(value)}
|
||||
color={isActive ? activeColor : inactiveColor}
|
||||
onClick={readonly ? undefined : () => onChange?.(value)}
|
||||
onMouseEnter={readonly ? undefined : () => setHoveredValue(value)}
|
||||
onMouseLeave={readonly ? undefined : () => setHoveredValue(null)}
|
||||
>
|
||||
<IconTwentyStarFilled size={theme.icon.size.md} />
|
||||
<IconTwentyStarFilled size={iconSizeMd} />
|
||||
</StyledRatingIconContainer>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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<IconComponentProps, 'size' | 'stroke'>;
|
||||
|
||||
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 (
|
||||
<IconTwentyStarFilledRaw height={size} width={size} strokeWidth={stroke} />
|
||||
|
||||
Reference in New Issue
Block a user