feat: rename Probability field type to Rating and update preview (#2770)

Closes #2593
This commit is contained in:
Thaïs
2023-12-01 15:31:01 +01:00
committed by GitHub
parent 474db1e142
commit 93e4f79551
24 changed files with 218 additions and 205 deletions
@@ -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) ? (
<BooleanFieldInput onSubmit={onSubmit} />
) : isFieldProbability(fieldDefinition) ? (
<ProbabilityFieldInput onSubmit={onSubmit} />
) : isFieldRating(fieldDefinition) ? (
<RatingFieldInput onSubmit={onSubmit} />
) : (
<></>
)}
@@ -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;
}
@@ -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);
@@ -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<number | null>(
const [fieldValue, setFieldValue] = useRecoilState<FieldRatingValue | null>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const probabilityIndex = Math.ceil((fieldValue ?? 0) / 25);
const rating = +(fieldValue ?? 0);
return {
fieldDefinition,
probabilityIndex,
rating,
setFieldValue,
hotkeyScope,
};
@@ -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 (
<ProbabilityInput
probabilityIndex={probabilityIndex}
onChange={handleChange}
/>
);
};
@@ -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 (
<RatingInput value={rating} onChange={handleChange} readonly={readonly} />
);
};
@@ -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 (
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'probability',
label: 'Probability',
type: 'PROBABILITY',
fieldMetadataId: 'rating',
label: 'Rating',
type: FieldMetadataType.Probability,
iconName: 'Icon123',
metadata: {
fieldName: 'Probability',
fieldName: 'Rating',
},
}}
entityId={entityId}
>
<ProbabilityFieldValueSetterEffect value={value} />
<ProbabilityFieldInput onSubmit={onSubmit} />
<RatingFieldValueSetterEffect value={value} />
<RatingFieldInput onSubmit={onSubmit} />
</FieldContextProvider>
);
};
@@ -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<typeof ProbabilityFieldInputWithContext>;
type Story = StoryObj<typeof RatingFieldInputWithContext>;
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);
},
@@ -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 (
<StyledContainer>
<StyledLabel>
{PROBABILITY_VALUES[probabilityIndexToShow].label}
</StyledLabel>
<StyledProgressBarContainer>
{PROBABILITY_VALUES.map((probability, probabilityIndexToSelect) => (
<StyledProgressBarItemContainer
key={probabilityIndexToSelect}
onClick={() => onChange(probability.value)}
onMouseEnter={() =>
setHoveredProbabilityIndex(probabilityIndexToSelect)
}
onMouseLeave={() => setHoveredProbabilityIndex(null)}
>
<StyledProgressBarItem
isActive={
hoveredProbabilityIndex || hoveredProbabilityIndex === 0
? probabilityIndexToSelect <= hoveredProbabilityIndex
: probabilityIndexToSelect <= probabilityIndexToShow
}
key={probability.label}
isFirst={probabilityIndexToSelect === 0}
isLast={
probabilityIndexToSelect === PROBABILITY_VALUES.length - 1
}
/>
</StyledProgressBarItemContainer>
))}
</StyledProgressBarContainer>
</StyledContainer>
);
};
@@ -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<number | null>(null);
const currentValue = hoveredValue ?? value;
return (
<StyledContainer
role="slider"
aria-label="Rating"
aria-valuemax={RATING_LEVELS_NB}
aria-valuemin={1}
aria-valuenow={value}
tabIndex={0}
>
{Array.from({ length: RATING_LEVELS_NB }, (_, index) => {
const rating = index + 1;
return (
<StyledRatingIconContainer
key={index}
isActive={rating <= currentValue}
onClick={readonly ? undefined : () => onChange(rating)}
onMouseEnter={readonly ? undefined : () => setHoveredValue(rating)}
onMouseLeave={readonly ? undefined : () => setHoveredValue(null)}
>
<IconTwentyStarFilled size={theme.icon.size.md} />
</StyledRatingIconContainer>
);
})}
</StyledContainer>
);
};
@@ -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)
@@ -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;
@@ -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'
@@ -1,7 +0,0 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldProbabilityMetadata } from '../FieldMetadata';
export const isFieldProbability = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldProbabilityMetadata> =>
field.type === 'PROBABILITY';
@@ -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);
@@ -0,0 +1,9 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldRatingMetadata } from '../FieldMetadata';
export const isFieldRating = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldRatingMetadata> =>
field.type === FieldMetadataType.Probability;
@@ -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;