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:
@ -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({
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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%;
|
||||
`;
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
Reference in New Issue
Block a user