feat: rename Probability field type to Rating and update preview (#2770)
Closes #2593
This commit is contained in:
@ -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 />
|
||||
)}
|
||||
|
||||
@ -91,6 +91,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
|
||||
FieldMetadataType.Enum,
|
||||
FieldMetadataType.Link,
|
||||
FieldMetadataType.Number,
|
||||
FieldMetadataType.Probability,
|
||||
FieldMetadataType.Relation,
|
||||
FieldMetadataType.Text,
|
||||
].includes(values.type) && (
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -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 },
|
||||
};
|
||||
|
||||
@ -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 |
3
front/src/modules/ui/display/icon/assets/twenty-star.svg
Normal file
3
front/src/modules/ui/display/icon/assets/twenty-star.svg
Normal 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 |
@ -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} />;
|
||||
};
|
||||
@ -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} />
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
Reference in New Issue
Block a user