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 { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
|
||||||
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
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 { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
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 { isFieldChipDisplay } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
|
||||||
|
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
@ -82,5 +84,7 @@ export const FieldDisplay = () => {
|
|||||||
<JsonFieldDisplay />
|
<JsonFieldDisplay />
|
||||||
) : isFieldBoolean(fieldDefinition) ? (
|
) : isFieldBoolean(fieldDefinition) ? (
|
||||||
<BooleanFieldDisplay />
|
<BooleanFieldDisplay />
|
||||||
|
) : isFieldRating(fieldDefinition) ? (
|
||||||
|
<RatingFieldDisplay />
|
||||||
) : null;
|
) : 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 { useContext, useState } from 'react';
|
||||||
import { useTheme } from '@emotion/react';
|
import { styled } from '@linaria/react';
|
||||||
import styled from '@emotion/styled';
|
import { IconTwentyStarFilled, THEME_COMMON, ThemeContext } from 'twenty-ui';
|
||||||
import { IconTwentyStarFilled } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
|
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
|
||||||
import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldRatingValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
@ -11,29 +10,38 @@ const StyledContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledRatingIconContainer = styled.div<{ isActive?: boolean }>`
|
const StyledRatingIconContainer = styled.div<{
|
||||||
color: ${({ isActive, theme }) =>
|
color: string;
|
||||||
isActive ? theme.font.color.secondary : theme.background.quaternary};
|
}>`
|
||||||
|
color: ${({ color }) => color};
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type RatingInputProps = {
|
type RatingInputProps = {
|
||||||
onChange: (newValue: FieldRatingValue) => void;
|
onChange?: (newValue: FieldRatingValue) => void;
|
||||||
value: FieldRatingValue;
|
value: FieldRatingValue;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const iconSizeMd = THEME_COMMON.icon.size.md;
|
||||||
|
|
||||||
export const RatingInput = ({
|
export const RatingInput = ({
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
readonly,
|
readonly,
|
||||||
}: RatingInputProps) => {
|
}: 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>(
|
const [hoveredValue, setHoveredValue] = useState<FieldRatingValue | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const currentValue = hoveredValue ?? value;
|
const currentValue = hoveredValue ?? value;
|
||||||
|
|
||||||
|
const selectedIndex = RATING_VALUES.indexOf(currentValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer
|
<StyledContainer
|
||||||
role="slider"
|
role="slider"
|
||||||
@ -44,17 +52,17 @@ export const RatingInput = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{RATING_VALUES.map((value, index) => {
|
{RATING_VALUES.map((value, index) => {
|
||||||
const currentIndex = RATING_VALUES.indexOf(currentValue);
|
const isActive = index <= selectedIndex;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRatingIconContainer
|
<StyledRatingIconContainer
|
||||||
key={index}
|
key={index}
|
||||||
isActive={index <= currentIndex}
|
color={isActive ? activeColor : inactiveColor}
|
||||||
onClick={readonly ? undefined : () => onChange(value)}
|
onClick={readonly ? undefined : () => onChange?.(value)}
|
||||||
onMouseEnter={readonly ? undefined : () => setHoveredValue(value)}
|
onMouseEnter={readonly ? undefined : () => setHoveredValue(value)}
|
||||||
onMouseLeave={readonly ? undefined : () => setHoveredValue(null)}
|
onMouseLeave={readonly ? undefined : () => setHoveredValue(null)}
|
||||||
>
|
>
|
||||||
<IconTwentyStarFilled size={theme.icon.size.md} />
|
<IconTwentyStarFilled size={iconSizeMd} />
|
||||||
</StyledRatingIconContainer>
|
</StyledRatingIconContainer>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -2229,6 +2229,60 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
|||||||
"toRelationMetadata": null
|
"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",
|
"__typename": "fieldEdge",
|
||||||
"node": {
|
"node": {
|
||||||
|
|||||||
@ -63,6 +63,7 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
'**/Chip.tsx',
|
'**/Chip.tsx',
|
||||||
'**/Tag.tsx',
|
'**/Tag.tsx',
|
||||||
'**/MultiSelectFieldDisplay.tsx',
|
'**/MultiSelectFieldDisplay.tsx',
|
||||||
|
'**/RatingInput.tsx',
|
||||||
],
|
],
|
||||||
babelOptions: {
|
babelOptions: {
|
||||||
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
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 IconTwentyStarFilledRaw from '@ui/display/icon/assets/twenty-star-filled.svg?react';
|
||||||
import { IconComponentProps } from '@ui/display/icon/types/IconComponent';
|
import { IconComponentProps } from '@ui/display/icon/types/IconComponent';
|
||||||
|
import { THEME_COMMON } from '@ui/theme';
|
||||||
|
|
||||||
type IconTwentyStarFilledProps = Pick<IconComponentProps, 'size' | 'stroke'>;
|
type IconTwentyStarFilledProps = Pick<IconComponentProps, 'size' | 'stroke'>;
|
||||||
|
|
||||||
|
const iconStrokeMd = THEME_COMMON.icon.stroke.md;
|
||||||
|
|
||||||
export const IconTwentyStarFilled = (props: IconTwentyStarFilledProps) => {
|
export const IconTwentyStarFilled = (props: IconTwentyStarFilledProps) => {
|
||||||
const theme = useTheme();
|
|
||||||
const size = props.size ?? 24;
|
const size = props.size ?? 24;
|
||||||
const stroke = props.stroke ?? theme.icon.stroke.md;
|
const stroke = props.stroke ?? iconStrokeMd;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconTwentyStarFilledRaw height={size} width={size} strokeWidth={stroke} />
|
<IconTwentyStarFilledRaw height={size} width={size} strokeWidth={stroke} />
|
||||||
|
|||||||
Reference in New Issue
Block a user