From e1bd3a4c5a38f93bebc9f934f0d7180091c35df4 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 17 Jun 2024 17:27:19 +0200 Subject: [PATCH] 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 --- .../record-field/components/FieldDisplay.tsx | 4 ++ .../display/components/RatingFieldDisplay.tsx | 8 +++ .../perf/RatingFieldDisplay.perf.stories.tsx | 34 ++++++++++++ .../meta-types/hooks/useRatingFieldDisplay.ts | 23 ++++++++ .../ui/field/input/components/RatingInput.tsx | 34 +++++++----- .../standard-metadata-query-result.ts | 54 +++++++++++++++++++ packages/twenty-front/vite.config.ts | 1 + .../icon/components/IconTwentyStarFilled.tsx | 8 +-- 8 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RatingFieldDisplay.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RatingFieldDisplay.perf.stories.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRatingFieldDisplay.ts 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 31a7692f21e5..58642260a54e 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 000000000000..7514480e4b00 --- /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 000000000000..cffa101426cf --- /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 000000000000..f6d0ff6603f2 --- /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 58d1fd060b06..c5dd404e59c9 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 c31eaaa65788..ff934beff8d5 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 34055bdb14ba..3ed3cdcdbfaa 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 465483017156..c82ec61041c9 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 (