Skip to content

Commit

Permalink
Added and optimized missing RatingFieldDisplay component (twentyhq#5904)
Browse files Browse the repository at this point in the history
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 twentyhq#5900
  • Loading branch information
lucasbordeau authored Jun 17, 2024
1 parent d8a57c6 commit 73d6017
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -82,5 +84,7 @@ export const FieldDisplay = () => {
<JsonFieldDisplay />
) : isFieldBoolean(fieldDefinition) ? (
<BooleanFieldDisplay />
) : isFieldRating(fieldDefinition) ? (
<RatingFieldDisplay />
) : null;
};
Original file line number Diff line number Diff line change
@@ -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 />;
};
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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"
Expand All @@ -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>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions packages/twenty-front/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
Original file line number Diff line number Diff line change
@@ -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} />
Expand Down

0 comments on commit 73d6017

Please sign in to comment.