Add probability picker on Opportunity card (#747)

* Fix padding

* Update date input component

* Add Probability picker component on opportunity card

* lint
This commit is contained in:
Emilien Chauvet
2023-07-18 23:54:34 -07:00
committed by GitHub
parent 8a23a65c17
commit c2fb8fd040
6 changed files with 214 additions and 22 deletions

View File

@ -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() {
}
/>
<CompanyAccountOwnerEditableField company={company} />
<span>
<IconCalendarEvent size={theme.icon.size.md} />
<BoardCardEditableFieldDate
value={new Date(pipelineProgress.closeDate || Date.now())}
onChange={(value) => {
handleCardUpdate({
...pipelineProgress,
closeDate: value.toISOString(),
});
}}
/>
</span>
<NumberEditableField
<DateEditableField
icon={<IconCalendarEvent />}
value={pipelineProgress.closeDate || new Date().toISOString()}
onSubmit={(value) =>
handleCardUpdate({
...pipelineProgress,
closeDate: value,
})
}
/>
<ProbabilityEditableField
icon={<IconCheck />}
placeholder="Opportunity probability for closing"
value={pipelineProgress.probability}
onSubmit={(value) =>
handleCardUpdate({

View File

@ -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 (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={icon}
displayModeContentOnly
disableHoverEffect
displayModeContent={
<ProbabilityFieldEditMode
value={internalValue ?? 0}
onChange={(newValue: number) => {
handleChange(newValue);
}}
/>
}
/>
</RecoilScope>
);
}

View File

@ -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 (
<StyledContainer>
<StyledLabel>
{
PROBABILITY_VALUES[
nextProbabilityIndex || nextProbabilityIndex === 0
? nextProbabilityIndex
: probabilityIndex
].label
}
</StyledLabel>
<StyledProgressBarContainer>
{PROBABILITY_VALUES.map((probability, i) => (
<StyledProgressBarItemContainer
onClick={() => handleChange(probability.value)}
onMouseEnter={() => setNextProbabilityIndex(i)}
onMouseLeave={() => setNextProbabilityIndex(null)}
>
<StyledProgressBarItem
isActive={
nextProbabilityIndex || nextProbabilityIndex === 0
? i <= nextProbabilityIndex
: i <= probabilityIndex
}
key={probability.label}
isFirst={i === 0}
isLast={i === PROBABILITY_VALUES.length - 1}
/>
</StyledProgressBarItemContainer>
))}
</StyledProgressBarContainer>
</StyledContainer>
);
}

View File

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

View File

@ -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({
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
)}
</StyledLabelAndIconContainer>
{isFieldInEditMode ? (
{isFieldInEditMode && !displayModeContentOnly ? (
<EditableFieldEditMode onSubmit={onSubmit} onCancel={onCancel}>
{editModeContent}
</EditableFieldEditMode>
) : (
<EditableFieldDisplayMode
disableHoverEffect={disableHoverEffect}
disableClick={useEditButton}
onClick={handleDisplayModeClick}
isDisplayModeContentEmpty={isDisplayModeContentEmpty}

View File

@ -2,7 +2,10 @@ import { css } from '@emotion/react';
import styled from '@emotion/styled';
export const EditableFieldNormalModeOuterContainer = styled.div<
Pick<OwnProps, 'disableClick' | 'isDisplayModeContentEmpty'>
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<OwnProps>) {
return (
<EditableFieldNormalModeOuterContainer
onClick={disableClick ? undefined : onClick}
disableClick={disableClick}
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
disableHoverEffect={disableHoverEffect}
>
<EditableFieldNormalModeInnerContainer>
{children}