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

View File

@ -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 ? (
<BooleanFieldInput readonly />
) : fieldMetadata.type === FieldMetadataType.Probability ? (
<RatingFieldInput readonly />
) : (
<FieldDisplay />
)}

View File

@ -91,6 +91,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
FieldMetadataType.Enum,
FieldMetadataType.Link,
FieldMetadataType.Number,
FieldMetadataType.Probability,
FieldMetadataType.Relation,
FieldMetadataType.Text,
].includes(values.type) && (

View File

@ -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) => (

View File

@ -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 },
};

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M6.06216 1.53416C6.38434 0.663593 7.61566 0.663591 7.93784 1.53416L9.00134 4.40789C9.10263 4.68158 9.31842 4.89737 9.59211 4.99866L12.4658 6.06216C13.3364 6.38434 13.3364 7.61566 12.4658 7.93784L9.59211 9.00134C9.31842 9.10263 9.10263 9.31842 9.00134 9.59211L7.93784 12.4658C7.61566 13.3364 6.38434 13.3364 6.06216 12.4658L4.99866 9.59211C4.89737 9.31842 4.68158 9.10263 4.40789 9.00134L1.53416 7.93784C0.663593 7.61566 0.663591 6.38434 1.53416 6.06216L4.40789 4.99866C4.68158 4.89737 4.89737 4.68158 4.99866 4.40789L6.06216 1.53416Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 668 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4673 3.06709C10.9938 1.64431 13.0062 1.6443 13.5327 3.06709L15.2708 7.76367C15.4364 8.21097 15.789 8.56364 16.2363 8.72917L20.9329 10.4673C22.3557 10.9938 22.3557 13.0062 20.9329 13.5327L16.2363 15.2708C15.789 15.4364 15.4364 15.789 15.2708 16.2363L13.5327 20.9329C13.0062 22.3557 10.9938 22.3557 10.4673 20.9329L8.72917 16.2363C8.56364 15.789 8.21097 15.4364 7.76367 15.2708L3.06709 13.5327C1.64431 13.0062 1.6443 10.9938 3.06709 10.4673L7.76367 8.72917C8.21097 8.56364 8.56364 8.21097 8.72917 7.76367L10.4673 3.06709Z" stroke="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 663 B

View File

@ -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 <IconTwentyStarRaw height={size} width={size} strokeWidth={stroke} />;
};

View File

@ -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 (
<IconTwentyStarFilledRaw height={size} width={size} strokeWidth={stroke} />
);
};

View File

@ -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} />
) : (
<></>
)}

View File

@ -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;
}

View File

@ -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);

View File

@ -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,
};

View File

@ -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}
/>
);
};

View File

@ -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} />
);
};

View File

@ -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);
},

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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)

View File

@ -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;

View File

@ -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'

View File

@ -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';

View File

@ -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);

View File

@ -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;

View File

@ -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;