Refacto board (#661)
* Refacto pipeline progress board to be entity agnostic * Abstract hooks as well * Move files * Pass specific components as props * Move board hook to the generic component * Make dnd and update logic part of the board * Remove useless call and getch pipelineProgress from hook * Minot * improve typing * Revert "improve typing" This reverts commit 49bf7929b6231747cc460cbb98f68c3c10424659. * wip * Get board from initial component * Move files again * Lint * Fix story * Lint * Mock pipeline progress * Fix storybook * WIP refactor recoil * Checkpoint: compilation * Fix dnd * Fix unselect card * Checkpoint: compilation * Checkpoint: New card OK * Checkpoint: feature complete * Fix latency for delete * Linter * Fix rebase * Move files * lint * Update Stories tests * lint * Fix test * Refactor hook for company progress indexing * Remove useless type * Move boardState * remove gardcoded Id * Nit * Fix * Rename state
This commit is contained in:
169
front/src/modules/companies/components/CompanyBoardCard.tsx
Normal file
169
front/src/modules/companies/components/CompanyBoardCard.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import { useCallback } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCurrencyDollar } from '@tabler/icons-react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { GET_PIPELINES } from '@/pipeline-progress/queries';
|
||||
import { BoardCardContext } from '@/pipeline-progress/states/BoardCardContext';
|
||||
import { pipelineProgressIdScopedState } from '@/pipeline-progress/states/pipelineProgressIdScopedState';
|
||||
import { selectedBoardCardsState } from '@/pipeline-progress/states/selectedBoardCardsState';
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { BoardCardEditableFieldDate } from '@/ui/board-card-field-inputs/components/BoardCardEditableFieldDate';
|
||||
import { BoardCardEditableFieldText } from '@/ui/board-card-field-inputs/components/BoardCardEditableFieldText';
|
||||
import { Checkbox } from '@/ui/components/form/Checkbox';
|
||||
import { IconCalendarEvent } from '@/ui/icons';
|
||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||
import {
|
||||
PipelineProgress,
|
||||
useUpdateOnePipelineProgressMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { companyProgressesFamilyState } from '~/pages/opportunities/companyProgressesFamilyState';
|
||||
|
||||
const StyledBoardCard = styled.div<{ selected: boolean }>`
|
||||
background-color: ${({ theme, selected }) =>
|
||||
selected ? theme.selectedCard : theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
&:hover {
|
||||
background-color: ${({ theme, selected }) =>
|
||||
selected ? theme.selectedCardHover : theme.background.tertiary};
|
||||
}
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const StyledBoardCardWrapper = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledBoardCardHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
height: 24px;
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
img {
|
||||
height: ${({ theme }) => theme.icon.size.md}px;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
object-fit: cover;
|
||||
width: ${({ theme }) => theme.icon.size.md}px;
|
||||
}
|
||||
`;
|
||||
const StyledBoardCardBody = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
span {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
svg {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function CompanyBoardCard() {
|
||||
const theme = useTheme();
|
||||
|
||||
const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation();
|
||||
|
||||
const [pipelineProgressId] = useRecoilScopedState(
|
||||
pipelineProgressIdScopedState,
|
||||
BoardCardContext,
|
||||
);
|
||||
const [companyProgress] = useRecoilState(
|
||||
companyProgressesFamilyState(pipelineProgressId || ''),
|
||||
);
|
||||
const { pipelineProgress, company } = companyProgress || {};
|
||||
const [selectedBoardCards, setSelectedBoardCards] = useRecoilState(
|
||||
selectedBoardCardsState,
|
||||
);
|
||||
|
||||
const selected = selectedBoardCards.includes(pipelineProgressId || '');
|
||||
function setSelected(isSelected: boolean) {
|
||||
if (isSelected) {
|
||||
setSelectedBoardCards([...selectedBoardCards, pipelineProgressId || '']);
|
||||
} else {
|
||||
setSelectedBoardCards(
|
||||
selectedBoardCards.filter((id) => id !== pipelineProgressId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const handleCardUpdate = useCallback(
|
||||
async (
|
||||
pipelineProgress: Pick<PipelineProgress, 'id' | 'amount' | 'closeDate'>,
|
||||
) => {
|
||||
await updatePipelineProgress({
|
||||
variables: {
|
||||
id: pipelineProgress.id,
|
||||
amount: pipelineProgress.amount,
|
||||
closeDate: pipelineProgress.closeDate || null,
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||
});
|
||||
},
|
||||
[updatePipelineProgress],
|
||||
);
|
||||
|
||||
const handleCheckboxChange = () => {
|
||||
setSelected(!selected);
|
||||
};
|
||||
|
||||
if (!company || !pipelineProgress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledBoardCardWrapper>
|
||||
<StyledBoardCard selected={selected}>
|
||||
<StyledBoardCardHeader>
|
||||
<img
|
||||
src={getLogoUrlFromDomainName(company.domainName).toString()}
|
||||
alt={`${company.name}-company-logo`}
|
||||
/>
|
||||
<span>{company.name}</span>
|
||||
<div style={{ display: 'flex', flex: 1 }} />
|
||||
<div onClick={handleCheckboxChange}>
|
||||
<Checkbox checked={selected} />
|
||||
</div>
|
||||
</StyledBoardCardHeader>
|
||||
<StyledBoardCardBody>
|
||||
<span>
|
||||
<IconCurrencyDollar size={theme.icon.size.md} />
|
||||
<BoardCardEditableFieldText
|
||||
value={pipelineProgress.amount?.toString() || ''}
|
||||
placeholder="Opportunity amount"
|
||||
onChange={(value) =>
|
||||
handleCardUpdate({
|
||||
...pipelineProgress,
|
||||
amount: parseInt(value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<IconCalendarEvent size={theme.icon.size.md} />
|
||||
<BoardCardEditableFieldDate
|
||||
value={new Date(pipelineProgress.closeDate || Date.now())}
|
||||
onChange={(value) => {
|
||||
handleCardUpdate({
|
||||
...pipelineProgress,
|
||||
closeDate: value.toISOString(),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</StyledBoardCardBody>
|
||||
</StyledBoardCard>
|
||||
</StyledBoardCardWrapper>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
|
||||
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
|
||||
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||
import { CommentableType, useSearchCompanyQuery } from '~/generated/graphql';
|
||||
|
||||
export function NewCompanyBoardCard() {
|
||||
const [searchFilter] = useRecoilScopedState(
|
||||
relationPickerSearchFilterScopedState,
|
||||
);
|
||||
|
||||
const companies = useFilteredSearchEntityQuery({
|
||||
queryHook: useSearchCompanyQuery,
|
||||
selectedIds: [],
|
||||
searchFilter: searchFilter,
|
||||
mappingFunction: (company) => ({
|
||||
entityType: CommentableType.Company,
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
domainName: company.domainName,
|
||||
avatarType: 'squared',
|
||||
avatarUrl: getLogoUrlFromDomainName(company.domainName),
|
||||
}),
|
||||
orderByField: 'name',
|
||||
searchOnFields: ['name'],
|
||||
});
|
||||
|
||||
const handleEntitySelect = useCallback(async (companyId: string) => {
|
||||
return;
|
||||
}, []);
|
||||
|
||||
function handleCancel() {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleEntitySelect
|
||||
onEntitySelected={(value) => handleEntitySelect(value.id)}
|
||||
onCancel={handleCancel}
|
||||
entities={{
|
||||
entitiesToSelect: companies.entitiesToSelect,
|
||||
selectedEntity: companies.selectedEntities[0],
|
||||
loading: companies.loading,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { usePreviousHotkeyScope } from '@/lib/hotkeys/hooks/usePreviousHotkeyScope';
|
||||
import { GET_PIPELINES } from '@/pipeline-progress/queries';
|
||||
import { BoardColumnContext } from '@/pipeline-progress/states/BoardColumnContext';
|
||||
import { boardState } from '@/pipeline-progress/states/boardState';
|
||||
import { pipelineStageIdScopedState } from '@/pipeline-progress/states/pipelineStageIdScopedState';
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
|
||||
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
|
||||
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||
import { RelationPickerHotkeyScope } from '@/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
|
||||
import { NewButton } from '@/ui/board/components/NewButton';
|
||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||
import {
|
||||
CommentableType,
|
||||
PipelineProgressableType,
|
||||
useCreateOnePipelineProgressMutation,
|
||||
useSearchCompanyQuery,
|
||||
} from '~/generated/graphql';
|
||||
import { currentPipelineState } from '~/pages/opportunities/currentPipelineState';
|
||||
|
||||
export function NewCompanyProgressButton() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isCreatingCard, setIsCreatingCard] = useState(false);
|
||||
const [board, setBoard] = useRecoilState(boardState);
|
||||
const [pipeline] = useRecoilState(currentPipelineState);
|
||||
const [pipelineStageId] = useRecoilScopedState(
|
||||
pipelineStageIdScopedState,
|
||||
BoardColumnContext,
|
||||
);
|
||||
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const [createOnePipelineProgress] = useCreateOnePipelineProgressMutation({
|
||||
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||
});
|
||||
|
||||
const handleEntitySelect = useCallback(
|
||||
async (company: any) => {
|
||||
if (!company) return;
|
||||
|
||||
setIsCreatingCard(false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
|
||||
const newUuid = uuidv4();
|
||||
const newBoard = JSON.parse(JSON.stringify(board));
|
||||
const destinationColumnIndex = newBoard.findIndex(
|
||||
(column: BoardPipelineStageColumn) =>
|
||||
column.pipelineStageId === pipelineStageId,
|
||||
);
|
||||
newBoard[destinationColumnIndex].pipelineProgressIds.push(newUuid);
|
||||
setBoard(newBoard);
|
||||
await createOnePipelineProgress({
|
||||
variables: {
|
||||
uuid: newUuid,
|
||||
pipelineStageId: pipelineStageId || '',
|
||||
pipelineId: pipeline?.id || '',
|
||||
entityId: company.id || '',
|
||||
entityType: PipelineProgressableType.Company,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
goBackToPreviousHotkeyScope,
|
||||
board,
|
||||
setBoard,
|
||||
createOnePipelineProgress,
|
||||
pipelineStageId,
|
||||
pipeline?.id,
|
||||
],
|
||||
);
|
||||
|
||||
const handleNewClick = useCallback(() => {
|
||||
setIsCreatingCard(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
);
|
||||
}, [setIsCreatingCard, setHotkeyScopeAndMemorizePreviousScope]);
|
||||
|
||||
function handleCancel() {
|
||||
goBackToPreviousHotkeyScope();
|
||||
setIsCreatingCard(false);
|
||||
}
|
||||
|
||||
const [searchFilter] = useRecoilScopedState(
|
||||
relationPickerSearchFilterScopedState,
|
||||
);
|
||||
const companies = useFilteredSearchEntityQuery({
|
||||
queryHook: useSearchCompanyQuery,
|
||||
selectedIds: [],
|
||||
searchFilter: searchFilter,
|
||||
mappingFunction: (company) => ({
|
||||
entityType: CommentableType.Company,
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
domainName: company.domainName,
|
||||
avatarType: 'squared',
|
||||
avatarUrl: getLogoUrlFromDomainName(company.domainName),
|
||||
}),
|
||||
orderByField: 'name',
|
||||
searchOnFields: ['name'],
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCreatingCard && (
|
||||
<RecoilScope>
|
||||
<div ref={containerRef}>
|
||||
<div ref={containerRef}>
|
||||
<SingleEntitySelect
|
||||
onEntitySelected={(value) => handleEntitySelect(value)}
|
||||
onCancel={handleCancel}
|
||||
entities={{
|
||||
entitiesToSelect: companies.entitiesToSelect,
|
||||
selectedEntity: companies.selectedEntities[0],
|
||||
loading: companies.loading,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RecoilScope>
|
||||
)}
|
||||
<NewButton onClick={handleNewClick} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { CompanyChip } from '../CompanyChip';
|
||||
|
||||
const meta: Meta<typeof CompanyChip> = {
|
||||
title: 'Modules/Companies/CompanyChip',
|
||||
component: CompanyChip,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CompanyChip>;
|
||||
|
||||
const TestCellContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
display: flex;
|
||||
|
||||
height: fit-content;
|
||||
justify-content: space-between;
|
||||
|
||||
max-width: 250px;
|
||||
|
||||
min-width: 250px;
|
||||
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
`;
|
||||
|
||||
export const SmallName: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<TestCellContainer>
|
||||
<BrowserRouter>
|
||||
<CompanyChip
|
||||
id="airbnb"
|
||||
name="Airbnb"
|
||||
picture="https://api.faviconkit.com/airbnb.com/144"
|
||||
/>
|
||||
</BrowserRouter>
|
||||
</TestCellContainer>,
|
||||
),
|
||||
};
|
||||
|
||||
export const BigName: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<TestCellContainer>
|
||||
<BrowserRouter>
|
||||
<CompanyChip
|
||||
id="google"
|
||||
name="Google with a real big name to overflow the cell"
|
||||
picture="https://api.faviconkit.com/google.com/144"
|
||||
/>
|
||||
</BrowserRouter>
|
||||
</TestCellContainer>,
|
||||
),
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { CompanyBoardCard } from '@/companies/components/CompanyBoardCard';
|
||||
import { NewCompanyProgressButton } from '@/companies/components/NewCompanyProgressButton';
|
||||
|
||||
import { BoardOptions } from '../../pipeline-progress/types/BoardOptions';
|
||||
|
||||
export const companyBoardOptions: BoardOptions = {
|
||||
newCardComponent: <NewCompanyProgressButton />,
|
||||
cardComponent: <CompanyBoardCard />,
|
||||
};
|
||||
Reference in New Issue
Block a user