From c2fb8fd0402d053880e55e681d3de671b6882bbb Mon Sep 17 00:00:00 2001 From: Emilien Chauvet Date: Tue, 18 Jul 2023 23:54:34 -0700 Subject: [PATCH] Add probability picker on Opportunity card (#747) * Fix padding * Update date input component * Add Probability picker component on opportunity card * lint --- .../companies/components/CompanyBoardCard.tsx | 32 +++-- .../components/ProbabilityEditableField.tsx | 71 +++++++++++ .../components/ProbabilityFieldEditMode.tsx | 112 ++++++++++++++++++ .../src/modules/ui/board/components/Board.tsx | 1 + .../components/EditableField.tsx | 9 +- .../components/EditableFieldDisplayMode.tsx | 11 +- 6 files changed, 214 insertions(+), 22 deletions(-) create mode 100644 front/src/modules/pipeline/editable-field/components/ProbabilityEditableField.tsx create mode 100644 front/src/modules/pipeline/editable-field/components/ProbabilityFieldEditMode.tsx diff --git a/front/src/modules/companies/components/CompanyBoardCard.tsx b/front/src/modules/companies/components/CompanyBoardCard.tsx index 7aebeccac..b9941a304 100644 --- a/front/src/modules/companies/components/CompanyBoardCard.tsx +++ b/front/src/modules/companies/components/CompanyBoardCard.tsx @@ -1,16 +1,16 @@ import { useCallback } from 'react'; import { getOperationName } from '@apollo/client/utilities'; -import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useRecoilState } from 'recoil'; import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState'; +import { ProbabilityEditableField } from '@/pipeline/editable-field/components/ProbabilityEditableField'; import { GET_PIPELINE_PROGRESS, GET_PIPELINES } from '@/pipeline/queries'; import { BoardCardContext } from '@/pipeline/states/BoardCardContext'; import { pipelineProgressIdScopedState } from '@/pipeline/states/pipelineProgressIdScopedState'; import { selectedBoardCardsState } from '@/pipeline/states/selectedBoardCardsState'; -import { BoardCardEditableFieldDate } from '@/ui/board/card-field/components/BoardCardEditableFieldDate'; import { ChipVariant } from '@/ui/chip/components/EntityChip'; +import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField'; import { NumberEditableField } from '@/ui/editable-field/variants/components/NumberEditableField'; import { IconCheck, IconCurrencyDollar } from '@/ui/icon'; import { IconCalendarEvent } from '@/ui/icon'; @@ -74,8 +74,6 @@ const StyledBoardCardBody = styled.div` `; export function CompanyBoardCard() { - const theme = useTheme(); - const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation(); const [pipelineProgressId] = useRecoilScopedState( @@ -155,21 +153,19 @@ export function CompanyBoardCard() { } /> - - - { - handleCardUpdate({ - ...pipelineProgress, - closeDate: value.toISOString(), - }); - }} - /> - - } + value={pipelineProgress.closeDate || new Date().toISOString()} + onSubmit={(value) => + handleCardUpdate({ + ...pipelineProgress, + closeDate: value, + }) + } + /> + + } - placeholder="Opportunity probability for closing" value={pipelineProgress.probability} onSubmit={(value) => handleCardUpdate({ diff --git a/front/src/modules/pipeline/editable-field/components/ProbabilityEditableField.tsx b/front/src/modules/pipeline/editable-field/components/ProbabilityEditableField.tsx new file mode 100644 index 000000000..9c021943a --- /dev/null +++ b/front/src/modules/pipeline/editable-field/components/ProbabilityEditableField.tsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from 'react'; + +import { EditableField } from '@/ui/editable-field/components/EditableField'; +import { FieldContext } from '@/ui/editable-field/states/FieldContext'; +import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; + +import { ProbabilityFieldEditMode } from './ProbabilityFieldEditMode'; + +type OwnProps = { + icon?: React.ReactNode; + value: number | null | undefined; + onSubmit?: (newValue: number) => void; +}; + +export function ProbabilityEditableField({ icon, value, onSubmit }: OwnProps) { + const [internalValue, setInternalValue] = useState(value); + + useEffect(() => { + setInternalValue(value); + }, [value]); + + async function handleChange(newValue: number) { + setInternalValue(newValue); + } + + async function handleSubmit() { + if (!internalValue) return; + + try { + const numberValue = internalValue; + + if (isNaN(numberValue)) { + throw new Error('Not a number'); + } + + if (numberValue < 0 || numberValue > 100) { + throw new Error('Not a probability'); + } + + onSubmit?.(numberValue); + + setInternalValue(numberValue); + } catch { + handleCancel(); + } + } + + async function handleCancel() { + setInternalValue(value); + } + + return ( + + { + handleChange(newValue); + }} + /> + } + /> + + ); +} diff --git a/front/src/modules/pipeline/editable-field/components/ProbabilityFieldEditMode.tsx b/front/src/modules/pipeline/editable-field/components/ProbabilityFieldEditMode.tsx new file mode 100644 index 000000000..5c47c1b27 --- /dev/null +++ b/front/src/modules/pipeline/editable-field/components/ProbabilityFieldEditMode.tsx @@ -0,0 +1,112 @@ +import { useState } from 'react'; +import styled from '@emotion/styled'; + +import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; +import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope'; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + justify-content: flex-start; + width: 100%; +`; + +const StyledProgressBarItemContainer = styled.div` + 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)}; +`; + +type OwnProps = { + value: number; + onChange?: (newValue: number) => void; + parentHotkeyScope?: HotkeyScope; +}; + +const PROBABILITY_VALUES = [ + { label: 'Lost', value: 0 }, + { label: '25%', value: 25 }, + { label: '50%', value: 50 }, + { label: '75%', value: 75 }, + { label: '100%', value: 100 }, +]; + +export function ProbabilityFieldEditMode({ value, onChange }: OwnProps) { + const [nextProbabilityIndex, setNextProbabilityIndex] = useState< + number | null + >(null); + + const probabilityIndex = Math.ceil(value / 25); + const { closeEditableField } = useEditableField(); + + function handleChange(newValue: number) { + onChange?.(newValue); + closeEditableField(); + } + + return ( + + + { + PROBABILITY_VALUES[ + nextProbabilityIndex || nextProbabilityIndex === 0 + ? nextProbabilityIndex + : probabilityIndex + ].label + } + + + {PROBABILITY_VALUES.map((probability, i) => ( + handleChange(probability.value)} + onMouseEnter={() => setNextProbabilityIndex(i)} + onMouseLeave={() => setNextProbabilityIndex(null)} + > + + + ))} + + + ); +} diff --git a/front/src/modules/ui/board/components/Board.tsx b/front/src/modules/ui/board/components/Board.tsx index da30f45bf..93ad98ac3 100644 --- a/front/src/modules/ui/board/components/Board.tsx +++ b/front/src/modules/ui/board/components/Board.tsx @@ -6,6 +6,7 @@ export const StyledBoard = styled.div` display: flex; flex-direction: row; overflow-x: auto; + padding-left: ${({ theme }) => theme.spacing(2)}; width: 100%; `; diff --git a/front/src/modules/ui/editable-field/components/EditableField.tsx b/front/src/modules/ui/editable-field/components/EditableField.tsx index ef001550b..ae5a33347 100644 --- a/front/src/modules/ui/editable-field/components/EditableField.tsx +++ b/front/src/modules/ui/editable-field/components/EditableField.tsx @@ -59,7 +59,9 @@ type OwnProps = { label?: string; labelFixedWidth?: number; useEditButton?: boolean; - editModeContent: React.ReactNode; + editModeContent?: React.ReactNode; + displayModeContentOnly?: boolean; + disableHoverEffect?: boolean; displayModeContent: React.ReactNode; parentHotkeyScope?: HotkeyScope; customEditHotkeyScope?: HotkeyScope; @@ -77,7 +79,9 @@ export function EditableField({ displayModeContent, parentHotkeyScope, customEditHotkeyScope, + disableHoverEffect, isDisplayModeContentEmpty, + displayModeContentOnly, onSubmit, onCancel, }: OwnProps) { @@ -115,12 +119,13 @@ export function EditableField({ {label} )} - {isFieldInEditMode ? ( + {isFieldInEditMode && !displayModeContentOnly ? ( {editModeContent} ) : ( + Pick< + OwnProps, + 'disableClick' | 'isDisplayModeContentEmpty' | 'disableHoverEffect' + > >` align-items: center; border-radius: ${({ theme }) => theme.border.radius.sm}; @@ -37,7 +40,8 @@ export const EditableFieldNormalModeOuterContainer = styled.div< cursor: pointer; &:hover { - background-color: ${props.theme.background.transparent.light}; + background-color: ${!props.disableHoverEffect && + props.theme.background.transparent.light}; } `; } @@ -62,6 +66,7 @@ type OwnProps = { disableClick?: boolean; onClick?: () => void; isDisplayModeContentEmpty?: boolean; + disableHoverEffect?: boolean; }; export function EditableFieldDisplayMode({ @@ -69,12 +74,14 @@ export function EditableFieldDisplayMode({ disableClick, onClick, isDisplayModeContentEmpty, + disableHoverEffect, }: React.PropsWithChildren) { return ( {children}