From 93e4f795514522b9ede899196667a099983edfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Fri, 1 Dec 2023 15:31:01 +0100 Subject: [PATCH] feat: rename Probability field type to Rating and update preview (#2770) Closes #2593 --- .../components/SettingsObjectFieldPreview.tsx | 3 + .../SettingsObjectFieldTypeSelectSection.tsx | 1 + .../SettingsObjectFieldPreview.stories.tsx | 10 ++ .../constants/settingsFieldMetadataTypes.ts | 7 +- .../icon/assets/twenty-star-filled.svg | 3 + .../ui/display/icon/assets/twenty-star.svg | 3 + .../icon/components/IconTwentyStar.tsx | 12 ++ .../icon/components/IconTwentyStarFilled.tsx | 16 +++ .../ui/object/field/components/FieldInput.tsx | 8 +- .../object/field/hooks/useIsFieldInputOnly.ts | 4 +- .../ui/object/field/hooks/usePersistField.ts | 7 +- ...eProbabilityField.ts => useRatingField.ts} | 19 ++- .../components/ProbabilityFieldInput.tsx | 29 ----- .../input/components/RatingFieldInput.tsx | 28 +++++ ...ories.tsx => RatingFieldInput.stories.tsx} | 54 ++++----- .../components/internal/ProbabilityInput.tsx | 108 ------------------ .../input/components/internal/RatingInput.tsx | 61 ++++++++++ .../isEntityFieldEmptyFamilySelector.ts | 4 +- .../ui/object/field/types/FieldMetadata.ts | 6 +- .../field/types/guards/assertFieldMetadata.ts | 4 +- .../field/types/guards/isFieldProbability.ts | 7 -- .../types/guards/isFieldProbabilityValue.ts | 8 -- .../field/types/guards/isFieldRating.ts | 9 ++ .../field/types/guards/isFieldRatingValue.ts | 12 ++ 24 files changed, 218 insertions(+), 205 deletions(-) create mode 100644 front/src/modules/ui/display/icon/assets/twenty-star-filled.svg create mode 100644 front/src/modules/ui/display/icon/assets/twenty-star.svg create mode 100644 front/src/modules/ui/display/icon/components/IconTwentyStar.tsx create mode 100644 front/src/modules/ui/display/icon/components/IconTwentyStarFilled.tsx rename front/src/modules/ui/object/field/meta-types/hooks/{useProbabilityField.ts => useRatingField.ts} (57%) delete mode 100644 front/src/modules/ui/object/field/meta-types/input/components/ProbabilityFieldInput.tsx create mode 100644 front/src/modules/ui/object/field/meta-types/input/components/RatingFieldInput.tsx rename front/src/modules/ui/object/field/meta-types/input/components/__stories__/{ProbabilityFieldInput.stories.tsx => RatingFieldInput.stories.tsx} (57%) delete mode 100644 front/src/modules/ui/object/field/meta-types/input/components/internal/ProbabilityInput.tsx create mode 100644 front/src/modules/ui/object/field/meta-types/input/components/internal/RatingInput.tsx delete mode 100644 front/src/modules/ui/object/field/types/guards/isFieldProbability.ts delete mode 100644 front/src/modules/ui/object/field/types/guards/isFieldProbabilityValue.ts create mode 100644 front/src/modules/ui/object/field/types/guards/isFieldRating.ts create mode 100644 front/src/modules/ui/object/field/types/guards/isFieldRatingValue.ts diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx index c57378a2e..b4d095f66 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx @@ -6,6 +6,7 @@ import { Tag } from '@/ui/display/tag/components/Tag'; import { FieldDisplay } from '@/ui/object/field/components/FieldDisplay'; import { FieldContext } from '@/ui/object/field/contexts/FieldContext'; import { BooleanFieldInput } from '@/ui/object/field/meta-types/input/components/BooleanFieldInput'; +import { RatingFieldInput } from '@/ui/object/field/meta-types/input/components/RatingFieldInput'; import { Field } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -147,6 +148,8 @@ export const SettingsObjectFieldPreview = ({ > {fieldMetadata.type === FieldMetadataType.Boolean ? ( + ) : fieldMetadata.type === FieldMetadataType.Probability ? ( + ) : ( )} diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx index c81dd52c2..722b5f109 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx @@ -91,6 +91,7 @@ export const SettingsObjectFieldTypeSelectSection = ({ FieldMetadataType.Enum, FieldMetadataType.Link, FieldMetadataType.Number, + FieldMetadataType.Probability, FieldMetadataType.Relation, FieldMetadataType.Text, ].includes(values.type) && ( diff --git a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx index 8d62b3023..2962cfbcc 100644 --- a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx +++ b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx @@ -90,6 +90,16 @@ export const Number: Story = { }, }; +export const Rating: Story = { + args: { + fieldMetadata: { + icon: 'IconHandClick', + label: 'Engagement', + type: FieldMetadataType.Probability, + }, + }, +}; + export const Relation: Story = { decorators: [ (Story) => ( diff --git a/front/src/modules/settings/data-model/constants/settingsFieldMetadataTypes.ts b/front/src/modules/settings/data-model/constants/settingsFieldMetadataTypes.ts index 8ba5a59f3..1aa96cd59 100644 --- a/front/src/modules/settings/data-model/constants/settingsFieldMetadataTypes.ts +++ b/front/src/modules/settings/data-model/constants/settingsFieldMetadataTypes.ts @@ -12,6 +12,7 @@ import { IconTextSize, IconUser, } from '@/ui/display/icon'; +import { IconTwentyStar } from '@/ui/display/icon/components/IconTwentyStar'; import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -74,9 +75,9 @@ export const settingsFieldMetadataTypes: Record< [FieldMetadataType.Email]: { label: 'Email', Icon: IconMail }, [FieldMetadataType.Phone]: { label: 'Phone', Icon: IconPhone }, [FieldMetadataType.Probability]: { - label: 'Probability', - Icon: IconNumbers, - defaultValue: 50, + label: 'Rating', + Icon: IconTwentyStar, + defaultValue: '3', }, [FieldMetadataType.FullName]: { label: 'Full Name', Icon: IconUser }, }; diff --git a/front/src/modules/ui/display/icon/assets/twenty-star-filled.svg b/front/src/modules/ui/display/icon/assets/twenty-star-filled.svg new file mode 100644 index 000000000..b9b018fb7 --- /dev/null +++ b/front/src/modules/ui/display/icon/assets/twenty-star-filled.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/front/src/modules/ui/display/icon/assets/twenty-star.svg b/front/src/modules/ui/display/icon/assets/twenty-star.svg new file mode 100644 index 000000000..9d2200996 --- /dev/null +++ b/front/src/modules/ui/display/icon/assets/twenty-star.svg @@ -0,0 +1,3 @@ + + + diff --git a/front/src/modules/ui/display/icon/components/IconTwentyStar.tsx b/front/src/modules/ui/display/icon/components/IconTwentyStar.tsx new file mode 100644 index 000000000..935629c57 --- /dev/null +++ b/front/src/modules/ui/display/icon/components/IconTwentyStar.tsx @@ -0,0 +1,12 @@ +import { TablerIconsProps } from '@/ui/display/icon'; + +import { ReactComponent as IconTwentyStarRaw } from '../assets/twenty-star.svg'; + +type IconTwentyStarProps = TablerIconsProps; + +export const IconTwentyStar = (props: IconTwentyStarProps): JSX.Element => { + const size = props.size ?? 24; + const stroke = props.stroke ?? 2; + + return ; +}; diff --git a/front/src/modules/ui/display/icon/components/IconTwentyStarFilled.tsx b/front/src/modules/ui/display/icon/components/IconTwentyStarFilled.tsx new file mode 100644 index 000000000..5de47046c --- /dev/null +++ b/front/src/modules/ui/display/icon/components/IconTwentyStarFilled.tsx @@ -0,0 +1,16 @@ +import { TablerIconsProps } from '@/ui/display/icon'; + +import { ReactComponent as IconTwentyStarFilledRaw } from '../assets/twenty-star-filled.svg'; + +type IconTwentyStarFilledProps = TablerIconsProps; + +export const IconTwentyStarFilled = ( + props: IconTwentyStarFilledProps, +): JSX.Element => { + const size = props.size ?? 24; + const stroke = props.stroke ?? 2; + + return ( + + ); +}; diff --git a/front/src/modules/ui/object/field/components/FieldInput.tsx b/front/src/modules/ui/object/field/components/FieldInput.tsx index 2d1a01a48..242e45a7f 100644 --- a/front/src/modules/ui/object/field/components/FieldInput.tsx +++ b/front/src/modules/ui/object/field/components/FieldInput.tsx @@ -12,7 +12,7 @@ import { EmailFieldInput } from '../meta-types/input/components/EmailFieldInput' import { LinkFieldInput } from '../meta-types/input/components/LinkFieldInput'; import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput'; import { PhoneFieldInput } from '../meta-types/input/components/PhoneFieldInput'; -import { ProbabilityFieldInput } from '../meta-types/input/components/ProbabilityFieldInput'; +import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput'; import { RelationFieldInput } from '../meta-types/input/components/RelationFieldInput'; import { TextFieldInput } from '../meta-types/input/components/TextFieldInput'; import { FieldInputEvent } from '../types/FieldInputEvent'; @@ -23,7 +23,7 @@ import { isFieldEmail } from '../types/guards/isFieldEmail'; import { isFieldLink } from '../types/guards/isFieldLink'; import { isFieldNumber } from '../types/guards/isFieldNumber'; import { isFieldPhone } from '../types/guards/isFieldPhone'; -import { isFieldProbability } from '../types/guards/isFieldProbability'; +import { isFieldRating } from '../types/guards/isFieldRating'; import { isFieldRelation } from '../types/guards/isFieldRelation'; import { isFieldText } from '../types/guards/isFieldText'; @@ -120,8 +120,8 @@ export const FieldInput = ({ /> ) : isFieldBoolean(fieldDefinition) ? ( - ) : isFieldProbability(fieldDefinition) ? ( - + ) : isFieldRating(fieldDefinition) ? ( + ) : ( <> )} diff --git a/front/src/modules/ui/object/field/hooks/useIsFieldInputOnly.ts b/front/src/modules/ui/object/field/hooks/useIsFieldInputOnly.ts index 855fd247e..d1566823c 100644 --- a/front/src/modules/ui/object/field/hooks/useIsFieldInputOnly.ts +++ b/front/src/modules/ui/object/field/hooks/useIsFieldInputOnly.ts @@ -2,12 +2,12 @@ import { useContext } from 'react'; import { FieldContext } from '../contexts/FieldContext'; import { isFieldBoolean } from '../types/guards/isFieldBoolean'; -import { isFieldProbability } from '../types/guards/isFieldProbability'; +import { isFieldRating } from '../types/guards/isFieldRating'; export const useIsFieldInputOnly = () => { const { fieldDefinition } = useContext(FieldContext); - if (isFieldBoolean(fieldDefinition) || isFieldProbability(fieldDefinition)) { + if (isFieldBoolean(fieldDefinition) || isFieldRating(fieldDefinition)) { return true; } diff --git a/front/src/modules/ui/object/field/hooks/usePersistField.ts b/front/src/modules/ui/object/field/hooks/usePersistField.ts index 670f6da55..421abd148 100644 --- a/front/src/modules/ui/object/field/hooks/usePersistField.ts +++ b/front/src/modules/ui/object/field/hooks/usePersistField.ts @@ -20,8 +20,8 @@ import { isFieldNumber } from '../types/guards/isFieldNumber'; import { isFieldNumberValue } from '../types/guards/isFieldNumberValue'; import { isFieldPhone } from '../types/guards/isFieldPhone'; import { isFieldPhoneValue } from '../types/guards/isFieldPhoneValue'; -import { isFieldProbability } from '../types/guards/isFieldProbability'; -import { isFieldProbabilityValue } from '../types/guards/isFieldProbabilityValue'; +import { isFieldRating } from '../types/guards/isFieldRating'; +import { isFieldRatingValue } from '../types/guards/isFieldRatingValue'; import { isFieldRelation } from '../types/guards/isFieldRelation'; import { isFieldRelationValue } from '../types/guards/isFieldRelationValue'; import { isFieldText } from '../types/guards/isFieldText'; @@ -61,8 +61,7 @@ export const usePersistField = () => { isFieldBooleanValue(valueToPersist); const fieldIsProbability = - isFieldProbability(fieldDefinition) && - isFieldProbabilityValue(valueToPersist); + isFieldRating(fieldDefinition) && isFieldRatingValue(valueToPersist); const fieldIsNumber = isFieldNumber(fieldDefinition) && isFieldNumberValue(valueToPersist); diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useProbabilityField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useRatingField.ts similarity index 57% rename from front/src/modules/ui/object/field/meta-types/hooks/useProbabilityField.ts rename to front/src/modules/ui/object/field/meta-types/hooks/useRatingField.ts index 985922c32..148cbacc2 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useProbabilityField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useRatingField.ts @@ -1,30 +1,37 @@ import { useContext } from 'react'; import { useRecoilState } from 'recoil'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + import { FieldContext } from '../../contexts/FieldContext'; import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector'; import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; -import { isFieldProbability } from '../../types/guards/isFieldProbability'; +import { isFieldRating } from '../../types/guards/isFieldRating'; +import { FieldRatingValue } from '../../types/FieldMetadata'; -export const useProbabilityField = () => { +export const useRatingField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('PROBABILITY', isFieldProbability, fieldDefinition); + assertFieldMetadata( + FieldMetadataType.Probability, + isFieldRating, + fieldDefinition, + ); const fieldName = fieldDefinition.metadata.fieldName; - const [fieldValue, setFieldValue] = useRecoilState( + const [fieldValue, setFieldValue] = useRecoilState( entityFieldsFamilySelector({ entityId: entityId, fieldName: fieldName, }), ); - const probabilityIndex = Math.ceil((fieldValue ?? 0) / 25); + const rating = +(fieldValue ?? 0); return { fieldDefinition, - probabilityIndex, + rating, setFieldValue, hotkeyScope, }; diff --git a/front/src/modules/ui/object/field/meta-types/input/components/ProbabilityFieldInput.tsx b/front/src/modules/ui/object/field/meta-types/input/components/ProbabilityFieldInput.tsx deleted file mode 100644 index 33788cb1f..000000000 --- a/front/src/modules/ui/object/field/meta-types/input/components/ProbabilityFieldInput.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ProbabilityInput } from '@/ui/object/field/meta-types/input/components/internal/ProbabilityInput'; - -import { usePersistField } from '../../../hooks/usePersistField'; -import { useProbabilityField } from '../../hooks/useProbabilityField'; - -import { FieldInputEvent } from './DateFieldInput'; - -export type ProbabilityFieldInputProps = { - onSubmit?: FieldInputEvent; -}; - -export const ProbabilityFieldInput = ({ - onSubmit, -}: ProbabilityFieldInputProps) => { - const { probabilityIndex } = useProbabilityField(); - - const persistField = usePersistField(); - - const handleChange = (newValue: number) => { - onSubmit?.(() => persistField(newValue)); - }; - - return ( - - ); -}; diff --git a/front/src/modules/ui/object/field/meta-types/input/components/RatingFieldInput.tsx b/front/src/modules/ui/object/field/meta-types/input/components/RatingFieldInput.tsx new file mode 100644 index 000000000..a35a36654 --- /dev/null +++ b/front/src/modules/ui/object/field/meta-types/input/components/RatingFieldInput.tsx @@ -0,0 +1,28 @@ +import { RatingInput } from '@/ui/object/field/meta-types/input/components/internal/RatingInput'; + +import { usePersistField } from '../../../hooks/usePersistField'; +import { useRatingField } from '../../hooks/useRatingField'; + +import { FieldInputEvent } from './DateFieldInput'; + +export type RatingFieldInputProps = { + onSubmit?: FieldInputEvent; + readonly?: boolean; +}; + +export const RatingFieldInput = ({ + onSubmit, + readonly, +}: RatingFieldInputProps) => { + const { rating } = useRatingField(); + + const persistField = usePersistField(); + + const handleChange = (newRating: number) => { + onSubmit?.(() => persistField(`${newRating}`)); + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx similarity index 57% rename from front/src/modules/ui/object/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx rename to front/src/modules/ui/object/field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx index 68680fd8e..53ad24924 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx @@ -4,16 +4,19 @@ import { Decorator, Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/testing-library'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; -import { useProbabilityField } from '../../../hooks/useProbabilityField'; -import { - ProbabilityFieldInput, - ProbabilityFieldInputProps, -} from '../ProbabilityFieldInput'; +import { useRatingField } from '../../../hooks/useRatingField'; +import { RatingFieldInput, RatingFieldInputProps } from '../RatingFieldInput'; +import { FieldRatingValue } from '../../../../types/FieldMetadata'; -const ProbabilityFieldValueSetterEffect = ({ value }: { value: number }) => { - const { setFieldValue } = useProbabilityField(); +const RatingFieldValueSetterEffect = ({ + value, +}: { + value: FieldRatingValue; +}) => { + const { setFieldValue } = useRatingField(); useEffect(() => { setFieldValue(value); @@ -22,16 +25,16 @@ const ProbabilityFieldValueSetterEffect = ({ value }: { value: number }) => { return <>; }; -type ProbabilityFieldInputWithContextProps = ProbabilityFieldInputProps & { - value: number; +type RatingFieldInputWithContextProps = RatingFieldInputProps & { + value: FieldRatingValue; entityId?: string; }; -const ProbabilityFieldInputWithContext = ({ +const RatingFieldInputWithContext = ({ entityId, value, onSubmit, -}: ProbabilityFieldInputWithContextProps) => { +}: RatingFieldInputWithContextProps) => { const setHotKeyScope = useSetHotkeyScope(); useEffect(() => { @@ -41,18 +44,18 @@ const ProbabilityFieldInputWithContext = ({ return ( - - + + ); }; @@ -67,11 +70,10 @@ const clearMocksDecorator: Decorator = (Story, context) => { }; const meta: Meta = { - title: 'UI/Data/Field/Input/ProbabilityFieldInput', - component: ProbabilityFieldInputWithContext, + title: 'UI/Data/Field/Input/RatingFieldInput', + component: RatingFieldInputWithContext, args: { - value: 25, - isPositive: true, + value: '3', onSubmit: submitJestFn, }, argTypes: { @@ -85,7 +87,7 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; @@ -95,12 +97,10 @@ export const Submit: Story = { expect(submitJestFn).toHaveBeenCalledTimes(0); - const item = (await canvas.findByText('25%'))?.nextElementSibling - ?.firstElementChild; + const input = canvas.getByRole('slider', { name: 'Rating' }); + const firstStar = input.firstElementChild; - if (item) { - userEvent.click(item); - } + if (firstStar) userEvent.click(firstStar); expect(submitJestFn).toHaveBeenCalledTimes(1); }, diff --git a/front/src/modules/ui/object/field/meta-types/input/components/internal/ProbabilityInput.tsx b/front/src/modules/ui/object/field/meta-types/input/components/internal/ProbabilityInput.tsx deleted file mode 100644 index fbf3c228a..000000000 --- a/front/src/modules/ui/object/field/meta-types/input/components/internal/ProbabilityInput.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useState } from 'react'; -import styled from '@emotion/styled'; - -const StyledContainer = styled.div` - align-items: center; - display: flex; - flex-direction: row; - justify-content: flex-start; - width: 100%; -`; - -const StyledProgressBarItemContainer = styled.div` - align-items: center; - display: flex; - height: ${({ theme }) => theme.spacing(4)}; - padding-right: ${({ theme }) => theme.spacing(1)}; -`; - -const StyledProgressBarItem = styled.div<{ - isFirst: boolean; - isLast: boolean; - isActive: boolean; -}>` - background-color: ${({ theme, isActive }) => - isActive - ? theme.font.color.secondary - : theme.background.transparent.medium}; - border-bottom-left-radius: ${({ theme, isFirst }) => - isFirst ? theme.border.radius.sm : theme.border.radius.xs}; - border-bottom-right-radius: ${({ theme, isLast }) => - isLast ? theme.border.radius.sm : theme.border.radius.xs}; - border-top-left-radius: ${({ theme, isFirst }) => - isFirst ? theme.border.radius.sm : theme.border.radius.xs}; - border-top-right-radius: ${({ theme, isLast }) => - isLast ? theme.border.radius.sm : theme.border.radius.xs}; - height: ${({ theme }) => theme.spacing(2)}; - width: ${({ theme }) => theme.spacing(3)}; -`; - -const StyledProgressBarContainer = styled.div` - align-items: center; - display: flex; - flex-direction: row; - justify-content: flex-start; - width: 100%; -`; - -const StyledLabel = styled.div` - width: ${({ theme }) => theme.spacing(12)}; -`; - -const PROBABILITY_VALUES = [ - { label: '0%', value: 0 }, - { label: '25%', value: 25 }, - { label: '50%', value: 50 }, - { label: '75%', value: 75 }, - { label: '100%', value: 100 }, -]; - -type ProbabilityInputProps = { - probabilityIndex: number | null; - onChange: (newValue: number) => void; -}; - -export const ProbabilityInput = ({ - onChange, - probabilityIndex, -}: ProbabilityInputProps) => { - const [hoveredProbabilityIndex, setHoveredProbabilityIndex] = useState< - number | null - >(null); - - const probabilityIndexToShow = - hoveredProbabilityIndex ?? probabilityIndex ?? 0; - - return ( - - - {PROBABILITY_VALUES[probabilityIndexToShow].label} - - - {PROBABILITY_VALUES.map((probability, probabilityIndexToSelect) => ( - onChange(probability.value)} - onMouseEnter={() => - setHoveredProbabilityIndex(probabilityIndexToSelect) - } - onMouseLeave={() => setHoveredProbabilityIndex(null)} - > - - - ))} - - - ); -}; diff --git a/front/src/modules/ui/object/field/meta-types/input/components/internal/RatingInput.tsx b/front/src/modules/ui/object/field/meta-types/input/components/internal/RatingInput.tsx new file mode 100644 index 000000000..549c10841 --- /dev/null +++ b/front/src/modules/ui/object/field/meta-types/input/components/internal/RatingInput.tsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { IconTwentyStarFilled } from '@/ui/display/icon/components/IconTwentyStarFilled'; + +const StyledContainer = styled.div` + align-items: center; + display: flex; +`; + +const StyledRatingIconContainer = styled.div<{ isActive?: boolean }>` + color: ${({ isActive, theme }) => + isActive ? theme.font.color.secondary : theme.background.quaternary}; + display: inline-flex; +`; + +type RatingInputProps = { + onChange: (newValue: number) => void; + value: number; + readonly?: boolean; +}; + +const RATING_LEVELS_NB = 5; + +export const RatingInput = ({ + onChange, + value, + readonly, +}: RatingInputProps) => { + const theme = useTheme(); + const [hoveredValue, setHoveredValue] = useState(null); + const currentValue = hoveredValue ?? value; + + return ( + + {Array.from({ length: RATING_LEVELS_NB }, (_, index) => { + const rating = index + 1; + + return ( + onChange(rating)} + onMouseEnter={readonly ? undefined : () => setHoveredValue(rating)} + onMouseLeave={readonly ? undefined : () => setHoveredValue(null)} + > + + + ); + })} + + ); +}; diff --git a/front/src/modules/ui/object/field/states/selectors/isEntityFieldEmptyFamilySelector.ts b/front/src/modules/ui/object/field/states/selectors/isEntityFieldEmptyFamilySelector.ts index cb189bd8d..a2015410f 100644 --- a/front/src/modules/ui/object/field/states/selectors/isEntityFieldEmptyFamilySelector.ts +++ b/front/src/modules/ui/object/field/states/selectors/isEntityFieldEmptyFamilySelector.ts @@ -15,7 +15,7 @@ import { isFieldEmail } from '../../types/guards/isFieldEmail'; import { isFieldLink } from '../../types/guards/isFieldLink'; import { isFieldLinkValue } from '../../types/guards/isFieldLinkValue'; import { isFieldNumber } from '../../types/guards/isFieldNumber'; -import { isFieldProbability } from '../../types/guards/isFieldProbability'; +import { isFieldRating } from '../../types/guards/isFieldRating'; import { isFieldRelation } from '../../types/guards/isFieldRelation'; import { isFieldRelationValue } from '../../types/guards/isFieldRelationValue'; import { isFieldText } from '../../types/guards/isFieldText'; @@ -40,7 +40,7 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({ isFieldText(fieldDefinition) || isFieldDateTime(fieldDefinition) || isFieldNumber(fieldDefinition) || - isFieldProbability(fieldDefinition) || + isFieldRating(fieldDefinition) || isFieldEmail(fieldDefinition) || isFieldBoolean(fieldDefinition) //|| isFieldPhone(fieldDefinition) diff --git a/front/src/modules/ui/object/field/types/FieldMetadata.ts b/front/src/modules/ui/object/field/types/FieldMetadata.ts index bb8a2fe86..7b17f81e9 100644 --- a/front/src/modules/ui/object/field/types/FieldMetadata.ts +++ b/front/src/modules/ui/object/field/types/FieldMetadata.ts @@ -61,7 +61,7 @@ export type FieldPhoneMetadata = { fieldName: string; }; -export type FieldProbabilityMetadata = { +export type FieldRatingMetadata = { objectMetadataNameSingular?: string; fieldName: string; }; @@ -95,7 +95,7 @@ export type FieldMetadata = | FieldLinkMetadata | FieldNumberMetadata | FieldPhoneMetadata - | FieldProbabilityMetadata + | FieldRatingMetadata | FieldRelationMetadata | FieldSelectMetadata | FieldTextMetadata @@ -115,7 +115,7 @@ export type FieldCurrencyValue = { amountMicros: number | null; }; export type FieldFullNameValue = { firstName: string; lastName: string }; -export type FieldProbabilityValue = number; +export type FieldRatingValue = '1' | '2' | '3' | '4' | '5'; export type FieldSelectValue = { color: ThemeColor; label: string }; export type FieldRelationValue = EntityForSelect | null; diff --git a/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts b/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts index 878ae281b..44ca2a495 100644 --- a/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts +++ b/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts @@ -9,7 +9,7 @@ import { FieldMetadata, FieldNumberMetadata, FieldPhoneMetadata, - FieldProbabilityMetadata, + FieldRatingMetadata, FieldRelationMetadata, FieldSelectMetadata, FieldTextMetadata, @@ -38,7 +38,7 @@ type AssertFieldMetadataFunction = < : E extends 'PHONE' ? FieldPhoneMetadata : E extends 'PROBABILITY' - ? FieldProbabilityMetadata + ? FieldRatingMetadata : E extends 'RELATION' ? FieldRelationMetadata : E extends 'TEXT' diff --git a/front/src/modules/ui/object/field/types/guards/isFieldProbability.ts b/front/src/modules/ui/object/field/types/guards/isFieldProbability.ts deleted file mode 100644 index 49d1eeb19..000000000 --- a/front/src/modules/ui/object/field/types/guards/isFieldProbability.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { FieldDefinition } from '../FieldDefinition'; -import { FieldMetadata, FieldProbabilityMetadata } from '../FieldMetadata'; - -export const isFieldProbability = ( - field: Pick, 'type'>, -): field is FieldDefinition => - field.type === 'PROBABILITY'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldProbabilityValue.ts b/front/src/modules/ui/object/field/types/guards/isFieldProbabilityValue.ts deleted file mode 100644 index 1c989fda9..000000000 --- a/front/src/modules/ui/object/field/types/guards/isFieldProbabilityValue.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isNumber } from '@sniptt/guards'; - -import { FieldProbabilityValue } from '../FieldMetadata'; - -// TODO: add zod -export const isFieldProbabilityValue = ( - fieldValue: unknown, -): fieldValue is FieldProbabilityValue => isNumber(fieldValue); diff --git a/front/src/modules/ui/object/field/types/guards/isFieldRating.ts b/front/src/modules/ui/object/field/types/guards/isFieldRating.ts new file mode 100644 index 000000000..1490150ea --- /dev/null +++ b/front/src/modules/ui/object/field/types/guards/isFieldRating.ts @@ -0,0 +1,9 @@ +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +import { FieldDefinition } from '../FieldDefinition'; +import { FieldMetadata, FieldRatingMetadata } from '../FieldMetadata'; + +export const isFieldRating = ( + field: Pick, 'type'>, +): field is FieldDefinition => + field.type === FieldMetadataType.Probability; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldRatingValue.ts b/front/src/modules/ui/object/field/types/guards/isFieldRatingValue.ts new file mode 100644 index 000000000..645c5a00f --- /dev/null +++ b/front/src/modules/ui/object/field/types/guards/isFieldRatingValue.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +import { FieldRatingValue } from '../FieldMetadata'; + +const ratingSchema = z + .string() + .transform((value) => +value) + .pipe(z.number().int().min(1).max(5)); + +export const isFieldRatingValue = ( + fieldValue: unknown, +): fieldValue is FieldRatingValue => ratingSchema.safeParse(fieldValue).success;