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:
@ -3366,7 +3366,14 @@ export type GetPipelinesQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetPipelinesQuery = { __typename?: 'Query', findManyPipeline: Array<{ __typename?: 'Pipeline', id: string, name: string, pipelineProgressableType: PipelineProgressableType, pipelineStages?: Array<{ __typename?: 'PipelineStage', id: string, name: string, color: string, index?: number | null, pipelineProgresses?: Array<{ __typename?: 'PipelineProgress', id: string, progressableType: PipelineProgressableType, progressableId: string, amount?: number | null, closeDate?: string | null }> | null }> | null }> };
|
||||
export type GetPipelinesQuery = { __typename?: 'Query', findManyPipeline: Array<{ __typename?: 'Pipeline', id: string, name: string, pipelineProgressableType: PipelineProgressableType, pipelineStages?: Array<{ __typename?: 'PipelineStage', id: string, name: string, color: string, index?: number | null, pipelineProgresses?: Array<{ __typename?: 'PipelineProgress', id: string }> | null }> | null }> };
|
||||
|
||||
export type GetPipelineProgressQueryVariables = Exact<{
|
||||
where?: InputMaybe<PipelineProgressWhereInput>;
|
||||
}>;
|
||||
|
||||
|
||||
export type GetPipelineProgressQuery = { __typename?: 'Query', findManyPipelineProgress: Array<{ __typename?: 'PipelineProgress', id: string, progressableType: PipelineProgressableType, progressableId: string, amount?: number | null, closeDate?: string | null }> };
|
||||
|
||||
export type UpdateOnePipelineProgressMutationVariables = Exact<{
|
||||
id?: InputMaybe<Scalars['String']>;
|
||||
@ -4959,10 +4966,6 @@ export const GetPipelinesDocument = gql`
|
||||
index
|
||||
pipelineProgresses {
|
||||
id
|
||||
progressableType
|
||||
progressableId
|
||||
amount
|
||||
closeDate
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4996,6 +4999,45 @@ export function useGetPipelinesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
|
||||
export type GetPipelinesQueryHookResult = ReturnType<typeof useGetPipelinesQuery>;
|
||||
export type GetPipelinesLazyQueryHookResult = ReturnType<typeof useGetPipelinesLazyQuery>;
|
||||
export type GetPipelinesQueryResult = Apollo.QueryResult<GetPipelinesQuery, GetPipelinesQueryVariables>;
|
||||
export const GetPipelineProgressDocument = gql`
|
||||
query GetPipelineProgress($where: PipelineProgressWhereInput) {
|
||||
findManyPipelineProgress(where: $where) {
|
||||
id
|
||||
progressableType
|
||||
progressableId
|
||||
amount
|
||||
closeDate
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetPipelineProgressQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetPipelineProgressQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetPipelineProgressQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetPipelineProgressQuery({
|
||||
* variables: {
|
||||
* where: // value for 'where'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetPipelineProgressQuery(baseOptions?: Apollo.QueryHookOptions<GetPipelineProgressQuery, GetPipelineProgressQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetPipelineProgressQuery, GetPipelineProgressQueryVariables>(GetPipelineProgressDocument, options);
|
||||
}
|
||||
export function useGetPipelineProgressLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetPipelineProgressQuery, GetPipelineProgressQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetPipelineProgressQuery, GetPipelineProgressQueryVariables>(GetPipelineProgressDocument, options);
|
||||
}
|
||||
export type GetPipelineProgressQueryHookResult = ReturnType<typeof useGetPipelineProgressQuery>;
|
||||
export type GetPipelineProgressLazyQueryHookResult = ReturnType<typeof useGetPipelineProgressLazyQuery>;
|
||||
export type GetPipelineProgressQueryResult = Apollo.QueryResult<GetPipelineProgressQuery, GetPipelineProgressQueryVariables>;
|
||||
export const UpdateOnePipelineProgressDocument = gql`
|
||||
mutation UpdateOnePipelineProgress($id: String, $amount: Int, $closeDate: DateTime) {
|
||||
updateOnePipelineProgress(
|
||||
|
||||
25
front/src/modules/companies/__stories__/Board.stories.tsx
Normal file
25
front/src/modules/companies/__stories__/Board.stories.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { companyBoardOptions } from '@/companies/components/companyBoardOptions';
|
||||
import { EntityBoard } from '@/pipeline-progress/components/EntityBoard';
|
||||
import { BoardDecorator } from '~/testing/decorators';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
const meta: Meta<typeof EntityBoard> = {
|
||||
title: 'UI/Board/Board',
|
||||
component: EntityBoard,
|
||||
decorators: [BoardDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EntityBoard>;
|
||||
|
||||
export const OneColumnBoard: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<EntityBoard boardOptions={companyBoardOptions} />,
|
||||
),
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { CompanyBoardCard } from '@/companies/components/CompanyBoardCard';
|
||||
import { BoardCardDecorator } from '~/testing/decorators';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
const meta: Meta<typeof CompanyBoardCard> = {
|
||||
title: 'UI/Board/CompanyBoardCard',
|
||||
component: CompanyBoardCard,
|
||||
decorators: [BoardCardDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CompanyBoardCard>;
|
||||
|
||||
const FakeSelectableCompanyBoardCard = () => {
|
||||
return <CompanyBoardCard />;
|
||||
};
|
||||
|
||||
export const CompanyCompanyBoardCard: Story = {
|
||||
render: getRenderWrapperForComponent(<FakeSelectableCompanyBoardCard />),
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
@ -4,7 +4,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { CompanyChip } from '../CompanyChip';
|
||||
import { CompanyChip } from '../components/CompanyChip';
|
||||
|
||||
const meta: Meta<typeof CompanyChip> = {
|
||||
title: 'Modules/Companies/CompanyChip',
|
||||
42
front/src/modules/companies/__stories__/mock-data.ts
Normal file
42
front/src/modules/companies/__stories__/mock-data.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Pipeline } from '~/generated/graphql';
|
||||
|
||||
export const pipeline = {
|
||||
id: 'pipeline-1',
|
||||
name: 'pipeline-1',
|
||||
pipelineStages: [
|
||||
{
|
||||
id: 'pipeline-stage-1',
|
||||
name: 'New',
|
||||
index: 0,
|
||||
color: '#B76796',
|
||||
pipelineProgresses: [
|
||||
{ id: 'fe256b39-3ec3-4fe7-8998-b76aa0bfb600' },
|
||||
{ id: '4a886c90-f4f2-4984-8222-882ebbb905d6' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pipeline-stage-2',
|
||||
name: 'Screening',
|
||||
index: 1,
|
||||
color: '#CB912F',
|
||||
},
|
||||
{
|
||||
id: 'pipeline-stage-3',
|
||||
name: 'Meeting',
|
||||
index: 2,
|
||||
color: '#9065B0',
|
||||
},
|
||||
{
|
||||
id: 'pipeline-stage-4',
|
||||
name: 'Proposal',
|
||||
index: 3,
|
||||
color: '#337EA9',
|
||||
},
|
||||
{
|
||||
id: 'pipeline-stage-5',
|
||||
name: 'Customer',
|
||||
index: 4,
|
||||
color: '#079039',
|
||||
},
|
||||
],
|
||||
} as Pipeline;
|
||||
@ -1,14 +1,25 @@
|
||||
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 { Company, PipelineProgress } from '../../../generated/graphql';
|
||||
import { Checkbox } from '../../ui/components/form/Checkbox';
|
||||
import { IconCalendarEvent } from '../../ui/icons';
|
||||
import { getLogoUrlFromDomainName } from '../../utils/utils';
|
||||
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 }) =>
|
||||
@ -59,30 +70,58 @@ const StyledBoardCardBody = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
type CompanyProp = Pick<
|
||||
Company,
|
||||
'id' | 'name' | 'domainName' | 'employees' | 'accountOwner'
|
||||
>;
|
||||
|
||||
type PipelineProgressProp = Pick<
|
||||
PipelineProgress,
|
||||
'id' | 'amount' | 'closeDate'
|
||||
>;
|
||||
|
||||
export function CompanyBoardCard({
|
||||
company,
|
||||
pipelineProgress,
|
||||
selected,
|
||||
onSelect,
|
||||
onCardUpdate,
|
||||
}: {
|
||||
company: CompanyProp;
|
||||
pipelineProgress: PipelineProgressProp;
|
||||
selected: boolean;
|
||||
onSelect: (company: CompanyProp) => void;
|
||||
onCardUpdate: (pipelineProgress: PipelineProgressProp) => Promise<void>;
|
||||
}) {
|
||||
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}>
|
||||
@ -93,7 +132,9 @@ export function CompanyBoardCard({
|
||||
/>
|
||||
<span>{company.name}</span>
|
||||
<div style={{ display: 'flex', flex: 1 }} />
|
||||
<Checkbox checked={selected} onChange={() => onSelect(company)} />
|
||||
<div onClick={handleCheckboxChange}>
|
||||
<Checkbox checked={selected} />
|
||||
</div>
|
||||
</StyledBoardCardHeader>
|
||||
<StyledBoardCardBody>
|
||||
<span>
|
||||
@ -102,7 +143,7 @@ export function CompanyBoardCard({
|
||||
value={pipelineProgress.amount?.toString() || ''}
|
||||
placeholder="Opportunity amount"
|
||||
onChange={(value) =>
|
||||
onCardUpdate({
|
||||
handleCardUpdate({
|
||||
...pipelineProgress,
|
||||
amount: parseInt(value),
|
||||
})
|
||||
@ -114,7 +155,7 @@ export function CompanyBoardCard({
|
||||
<BoardCardEditableFieldDate
|
||||
value={new Date(pipelineProgress.closeDate || Date.now())}
|
||||
onChange={(value) => {
|
||||
onCardUpdate({
|
||||
handleCardUpdate({
|
||||
...pipelineProgress,
|
||||
closeDate: value.toISOString(),
|
||||
});
|
||||
@ -1,22 +1,13 @@
|
||||
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,
|
||||
Company,
|
||||
useSearchCompanyQuery,
|
||||
} from '~/generated/graphql';
|
||||
import { CommentableType, useSearchCompanyQuery } from '~/generated/graphql';
|
||||
|
||||
type OwnProps = {
|
||||
onEntitySelect: (
|
||||
company: Pick<Company, 'id' | 'name' | 'domainName'>,
|
||||
) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
export function NewCompanyBoardCard({ onEntitySelect, onCancel }: OwnProps) {
|
||||
export function NewCompanyBoardCard() {
|
||||
const [searchFilter] = useRecoilScopedState(
|
||||
relationPickerSearchFilterScopedState,
|
||||
);
|
||||
@ -37,10 +28,18 @@ export function NewCompanyBoardCard({ onEntitySelect, onCancel }: OwnProps) {
|
||||
searchOnFields: ['name'],
|
||||
});
|
||||
|
||||
const handleEntitySelect = useCallback(async (companyId: string) => {
|
||||
return;
|
||||
}, []);
|
||||
|
||||
function handleCancel() {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleEntitySelect
|
||||
onEntitySelected={(value) => onEntitySelect(value)}
|
||||
onCancel={onCancel}
|
||||
onEntitySelected={(value) => handleEntitySelect(value.id)}
|
||||
onCancel={handleCancel}
|
||||
entities={{
|
||||
entitiesToSelect: companies.entitiesToSelect,
|
||||
selectedEntity: companies.selectedEntities[0],
|
||||
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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 />,
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const CompanyBoardContext = createContext<string | null>(null);
|
||||
11
front/src/modules/companies/states/companyBoardIndexState.ts
Normal file
11
front/src/modules/companies/states/companyBoardIndexState.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { CompanyProgress } from '@/companies/types/CompanyProgress';
|
||||
|
||||
export const companyBoardIndexState = atomFamily<
|
||||
CompanyProgress | undefined,
|
||||
string
|
||||
>({
|
||||
key: 'currentPipelineState',
|
||||
default: undefined,
|
||||
});
|
||||
16
front/src/modules/companies/types/CompanyProgress.ts
Normal file
16
front/src/modules/companies/types/CompanyProgress.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Company, PipelineProgress } from '~/generated/graphql';
|
||||
|
||||
export type CompanyForBoard = Pick<Company, 'id' | 'name' | 'domainName'>;
|
||||
export type PipelineProgressForBoard = Pick<
|
||||
PipelineProgress,
|
||||
'id' | 'amount' | 'closeDate' | 'progressableId'
|
||||
>;
|
||||
|
||||
export type CompanyProgress = {
|
||||
company: CompanyForBoard;
|
||||
pipelineProgress: PipelineProgressForBoard;
|
||||
};
|
||||
|
||||
export type CompanyProgressDict = {
|
||||
[key: string]: CompanyProgress;
|
||||
};
|
||||
@ -1,205 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
DroppableProvided,
|
||||
OnDragEndResponder,
|
||||
} from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { BoardColumn } from '@/ui/board/components/BoardColumn';
|
||||
import { Company, PipelineProgress } from '~/generated/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
getOptimisticlyUpdatedBoard,
|
||||
StyledBoard,
|
||||
} from '../../ui/board/components/Board';
|
||||
import { boardColumnsState } from '../states/boardColumnsState';
|
||||
import { boardItemsState } from '../states/boardItemsState';
|
||||
import { selectedBoardItemsState } from '../states/selectedBoardItemsState';
|
||||
|
||||
import { CompanyBoardCard } from './CompanyBoardCard';
|
||||
import { NewButton } from './NewButton';
|
||||
|
||||
export type CompanyProgress = {
|
||||
company: Pick<Company, 'id' | 'name' | 'domainName'>;
|
||||
pipelineProgress: Pick<PipelineProgress, 'id' | 'amount' | 'closeDate'>;
|
||||
};
|
||||
|
||||
export type CompanyProgressDict = {
|
||||
[key: string]: CompanyProgress;
|
||||
};
|
||||
|
||||
type BoardProps = {
|
||||
pipelineId: string;
|
||||
columns: Omit<Column, 'itemKeys'>[];
|
||||
initialBoard: Column[];
|
||||
initialItems: CompanyProgressDict;
|
||||
onCardMove?: (itemKey: string, columnId: Column['id']) => Promise<void>;
|
||||
onCardUpdate: (
|
||||
pipelineProgress: Pick<PipelineProgress, 'id' | 'amount' | 'closeDate'>,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
const StyledPlaceholder = styled.div`
|
||||
min-height: 1px;
|
||||
`;
|
||||
|
||||
const BoardColumnCardsContainer = ({
|
||||
children,
|
||||
droppableProvided,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
droppableProvided: DroppableProvided;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
ref={droppableProvided?.innerRef}
|
||||
{...droppableProvided?.droppableProps}
|
||||
>
|
||||
{children}
|
||||
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function Board({
|
||||
columns,
|
||||
initialBoard,
|
||||
initialItems,
|
||||
onCardMove,
|
||||
onCardUpdate,
|
||||
pipelineId,
|
||||
}: BoardProps) {
|
||||
const [board, setBoard] = useRecoilState(boardColumnsState);
|
||||
const [boardItems, setBoardItems] = useRecoilState(boardItemsState);
|
||||
const [selectedBoardItems, setSelectedBoardItems] = useRecoilState(
|
||||
selectedBoardItemsState,
|
||||
);
|
||||
const [isInitialBoardLoaded, setIsInitialBoardLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialBoardLoaded) {
|
||||
setBoard(initialBoard);
|
||||
}
|
||||
if (Object.keys(initialItems).length > 0) {
|
||||
setBoardItems(initialItems);
|
||||
setIsInitialBoardLoaded(true);
|
||||
}
|
||||
}, [
|
||||
initialBoard,
|
||||
setBoard,
|
||||
initialItems,
|
||||
setBoardItems,
|
||||
setIsInitialBoardLoaded,
|
||||
isInitialBoardLoaded,
|
||||
]);
|
||||
|
||||
const calculateColumnTotals = (
|
||||
columns: Column[],
|
||||
items: {
|
||||
[key: string]: CompanyProgress;
|
||||
},
|
||||
): { [key: string]: number } => {
|
||||
return columns.reduce<{ [key: string]: number }>((acc, column) => {
|
||||
acc[column.id] = column.itemKeys.reduce(
|
||||
(total: number, itemKey: string) => {
|
||||
return total + (items[itemKey]?.pipelineProgress?.amount || 0);
|
||||
},
|
||||
0,
|
||||
);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const columnTotals = useMemo(
|
||||
() => calculateColumnTotals(board, boardItems),
|
||||
[board, boardItems],
|
||||
);
|
||||
|
||||
const onDragEnd: OnDragEndResponder = useCallback(
|
||||
async (result) => {
|
||||
const newBoard = getOptimisticlyUpdatedBoard(board, result);
|
||||
if (!newBoard) return;
|
||||
setBoard(newBoard);
|
||||
try {
|
||||
const draggedEntityId = result.draggableId;
|
||||
const destinationColumnId = result.destination?.droppableId;
|
||||
draggedEntityId &&
|
||||
destinationColumnId &&
|
||||
onCardMove &&
|
||||
(await onCardMove(draggedEntityId, destinationColumnId));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
[board, onCardMove, setBoard],
|
||||
);
|
||||
|
||||
function handleSelect(itemKey: string) {
|
||||
if (selectedBoardItems.includes(itemKey)) {
|
||||
setSelectedBoardItems(
|
||||
selectedBoardItems.filter((key) => key !== itemKey),
|
||||
);
|
||||
} else {
|
||||
setSelectedBoardItems([...selectedBoardItems, itemKey]);
|
||||
}
|
||||
}
|
||||
|
||||
return board.length > 0 ? (
|
||||
<StyledBoard>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{columns.map((column, columnIndex) => (
|
||||
<Droppable key={column.id} droppableId={column.id}>
|
||||
{(droppableProvided) => (
|
||||
<BoardColumn
|
||||
title={`${column.title} `}
|
||||
amount={columnTotals[column.id]}
|
||||
colorCode={column.colorCode}
|
||||
>
|
||||
<BoardColumnCardsContainer
|
||||
droppableProvided={droppableProvided}
|
||||
>
|
||||
{board[columnIndex].itemKeys.map(
|
||||
(itemKey, index) =>
|
||||
boardItems[itemKey] && (
|
||||
<Draggable
|
||||
key={itemKey}
|
||||
draggableId={itemKey}
|
||||
index={index}
|
||||
>
|
||||
{(draggableProvided) => (
|
||||
<div
|
||||
ref={draggableProvided?.innerRef}
|
||||
{...draggableProvided?.dragHandleProps}
|
||||
{...draggableProvided?.draggableProps}
|
||||
>
|
||||
<CompanyBoardCard
|
||||
company={boardItems[itemKey].company}
|
||||
pipelineProgress={
|
||||
boardItems[itemKey].pipelineProgress
|
||||
}
|
||||
selected={selectedBoardItems.includes(itemKey)}
|
||||
onCardUpdate={onCardUpdate}
|
||||
onSelect={() => handleSelect(itemKey)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
),
|
||||
)}
|
||||
</BoardColumnCardsContainer>
|
||||
<NewButton pipelineId={pipelineId} columnId={column.id} />
|
||||
</BoardColumn>
|
||||
)}
|
||||
</Droppable>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</StyledBoard>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
@ -1,39 +1,41 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { boardState } from '@/pipeline-progress/states/boardState';
|
||||
import { EntityTableActionBarButton } from '@/ui/components/table/action-bar/EntityTableActionBarButton';
|
||||
import { IconTrash } from '@/ui/icons/index';
|
||||
import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql';
|
||||
|
||||
import { GET_PIPELINES } from '../queries';
|
||||
import { boardItemsState } from '../states/boardItemsState';
|
||||
import { selectedBoardItemsState } from '../states/selectedBoardItemsState';
|
||||
import { selectedBoardCardsState } from '../states/selectedBoardCardsState';
|
||||
|
||||
export function BoardActionBarButtonDeletePipelineProgress() {
|
||||
const [selectedBoardItems, setSelectedBoardItems] = useRecoilState(
|
||||
selectedBoardItemsState,
|
||||
selectedBoardCardsState,
|
||||
);
|
||||
const [boardItems, setBoardItems] = useRecoilState(boardItemsState);
|
||||
const [board, setBoard] = useRecoilState(boardState);
|
||||
|
||||
const [deletePipelineProgress] = useDeleteManyPipelineProgressMutation({
|
||||
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||
});
|
||||
|
||||
async function handleDeleteClick() {
|
||||
setBoard(
|
||||
board?.map((pipelineStage) => ({
|
||||
...pipelineStage,
|
||||
pipelineProgressIds: pipelineStage.pipelineProgressIds.filter(
|
||||
(pipelineProgressId) =>
|
||||
!selectedBoardItems.includes(pipelineProgressId),
|
||||
),
|
||||
})),
|
||||
);
|
||||
|
||||
setSelectedBoardItems([]);
|
||||
await deletePipelineProgress({
|
||||
variables: {
|
||||
ids: selectedBoardItems,
|
||||
},
|
||||
});
|
||||
|
||||
setBoardItems(
|
||||
Object.fromEntries(
|
||||
Object.entries(boardItems).filter(
|
||||
([key]) => !selectedBoardItems.includes(key),
|
||||
),
|
||||
),
|
||||
);
|
||||
setSelectedBoardItems([]);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
import { useCallback } from 'react';
|
||||
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { boardState } from '@/pipeline-progress/states/boardState';
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import {
|
||||
PipelineProgress,
|
||||
PipelineStage,
|
||||
useUpdateOnePipelineProgressStageMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import {
|
||||
getOptimisticlyUpdatedBoard,
|
||||
StyledBoard,
|
||||
} from '../../ui/board/components/Board';
|
||||
import { BoardColumnContext } from '../states/BoardColumnContext';
|
||||
import { BoardOptions } from '../types/BoardOptions';
|
||||
|
||||
import { EntityBoardColumn } from './EntityBoardColumn';
|
||||
|
||||
export function EntityBoard({ boardOptions }: { boardOptions: BoardOptions }) {
|
||||
const [board, setBoard] = useRecoilState(boardState);
|
||||
const [updatePipelineProgressStage] =
|
||||
useUpdateOnePipelineProgressStageMutation();
|
||||
|
||||
const updatePipelineProgressStageInDB = useCallback(
|
||||
async (
|
||||
pipelineProgressId: NonNullable<PipelineProgress['id']>,
|
||||
pipelineStageId: NonNullable<PipelineStage['id']>,
|
||||
) => {
|
||||
updatePipelineProgressStage({
|
||||
variables: {
|
||||
id: pipelineProgressId,
|
||||
pipelineStageId,
|
||||
},
|
||||
});
|
||||
},
|
||||
[updatePipelineProgressStage],
|
||||
);
|
||||
|
||||
const onDragEnd: OnDragEndResponder = useCallback(
|
||||
async (result) => {
|
||||
if (!board) return;
|
||||
const newBoard = getOptimisticlyUpdatedBoard(board, result);
|
||||
if (!newBoard) return;
|
||||
setBoard(newBoard);
|
||||
try {
|
||||
const draggedEntityId = result.draggableId;
|
||||
const destinationColumnId = result.destination?.droppableId;
|
||||
draggedEntityId &&
|
||||
destinationColumnId &&
|
||||
updatePipelineProgressStageInDB &&
|
||||
(await updatePipelineProgressStageInDB(
|
||||
draggedEntityId,
|
||||
destinationColumnId,
|
||||
));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
[board, updatePipelineProgressStageInDB, setBoard],
|
||||
);
|
||||
|
||||
return (board?.length ?? 0) > 0 ? (
|
||||
<StyledBoard>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{board?.map((column) => (
|
||||
<RecoilScope
|
||||
SpecificContext={BoardColumnContext}
|
||||
key={column.pipelineStageId}
|
||||
>
|
||||
<EntityBoardColumn boardOptions={boardOptions} column={column} />
|
||||
</RecoilScope>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</StyledBoard>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
@ -3,13 +3,13 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActionBar } from '@/ui/components/action-bar/ActionBar';
|
||||
|
||||
import { selectedBoardItemsState } from '../states/selectedBoardItemsState';
|
||||
import { selectedBoardCardsState } from '../states/selectedBoardCardsState';
|
||||
|
||||
type OwnProps = {
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
};
|
||||
|
||||
export function EntityBoardActionBar({ children }: OwnProps) {
|
||||
const selectedBoardItems = useRecoilValue(selectedBoardItemsState);
|
||||
return <ActionBar selectedIds={selectedBoardItems}>{children}</ActionBar>;
|
||||
const selectedBoardCards = useRecoilValue(selectedBoardCardsState);
|
||||
return <ActionBar selectedIds={selectedBoardCards}>{children}</ActionBar>;
|
||||
}
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
|
||||
import { BoardCardContext } from '@/pipeline-progress/states/BoardCardContext';
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { pipelineProgressIdScopedState } from '../states/pipelineProgressIdScopedState';
|
||||
import { BoardOptions } from '../types/BoardOptions';
|
||||
|
||||
export function EntityBoardCard({
|
||||
boardOptions,
|
||||
pipelineProgressId,
|
||||
index,
|
||||
}: {
|
||||
boardOptions: BoardOptions;
|
||||
pipelineProgressId: string;
|
||||
index: number;
|
||||
}) {
|
||||
const [pipelineProgressIdFromRecoil, setPipelineProgressId] =
|
||||
useRecoilScopedState(pipelineProgressIdScopedState, BoardCardContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (pipelineProgressIdFromRecoil !== pipelineProgressId) {
|
||||
setPipelineProgressId(pipelineProgressId);
|
||||
}
|
||||
}, [pipelineProgressId, setPipelineProgressId, pipelineProgressIdFromRecoil]);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={pipelineProgressId}
|
||||
draggableId={pipelineProgressId}
|
||||
index={index}
|
||||
>
|
||||
{(draggableProvided) => (
|
||||
<div
|
||||
ref={draggableProvided?.innerRef}
|
||||
{...draggableProvided?.dragHandleProps}
|
||||
{...draggableProvided?.draggableProps}
|
||||
>
|
||||
{boardOptions.cardComponent}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
import { useEffect } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Droppable, DroppableProvided } from '@hello-pangea/dnd';
|
||||
|
||||
import { BoardCardContext } from '@/pipeline-progress/states/BoardCardContext';
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
|
||||
import { BoardColumn } from '@/ui/board/components/BoardColumn';
|
||||
|
||||
import { BoardColumnContext } from '../states/BoardColumnContext';
|
||||
import { pipelineStageIdScopedState } from '../states/pipelineStageIdScopedState';
|
||||
import { BoardOptions } from '../types/BoardOptions';
|
||||
|
||||
import { EntityBoardCard } from './EntityBoardCard';
|
||||
|
||||
const StyledPlaceholder = styled.div`
|
||||
min-height: 1px;
|
||||
`;
|
||||
|
||||
const BoardColumnCardsContainer = ({
|
||||
children,
|
||||
droppableProvided,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
droppableProvided: DroppableProvided;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
ref={droppableProvided?.innerRef}
|
||||
{...droppableProvided?.droppableProps}
|
||||
>
|
||||
{children}
|
||||
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function EntityBoardColumn({
|
||||
column,
|
||||
boardOptions,
|
||||
}: {
|
||||
column: BoardPipelineStageColumn;
|
||||
boardOptions: BoardOptions;
|
||||
}) {
|
||||
const [pipelineStageId, setPipelineStageId] = useRecoilScopedState(
|
||||
pipelineStageIdScopedState,
|
||||
BoardColumnContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (pipelineStageId !== column.pipelineStageId) {
|
||||
setPipelineStageId(column.pipelineStageId);
|
||||
}
|
||||
}, [column, setPipelineStageId, pipelineStageId]);
|
||||
|
||||
return (
|
||||
<Droppable droppableId={column.pipelineStageId}>
|
||||
{(droppableProvided) => (
|
||||
<BoardColumn title={`${column.title} `} colorCode={column.colorCode}>
|
||||
<BoardColumnCardsContainer droppableProvided={droppableProvided}>
|
||||
{column.pipelineProgressIds.map((pipelineProgressId, index) => (
|
||||
<RecoilScope
|
||||
SpecificContext={BoardCardContext}
|
||||
key={pipelineProgressId}
|
||||
>
|
||||
<EntityBoardCard
|
||||
index={index}
|
||||
pipelineProgressId={pipelineProgressId}
|
||||
boardOptions={boardOptions}
|
||||
/>
|
||||
</RecoilScope>
|
||||
))}
|
||||
</BoardColumnCardsContainer>
|
||||
<RecoilScope>{boardOptions.newCardComponent}</RecoilScope>
|
||||
</BoardColumn>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
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 { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { RelationPickerHotkeyScope } from '@/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { Column } from '@/ui/board/components/Board';
|
||||
import { NewButton as UINewButton } from '@/ui/board/components/NewButton';
|
||||
import {
|
||||
Company,
|
||||
PipelineProgressableType,
|
||||
useCreateOnePipelineProgressMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { GET_PIPELINES } from '../queries';
|
||||
import { boardColumnsState } from '../states/boardColumnsState';
|
||||
import { boardItemsState } from '../states/boardItemsState';
|
||||
|
||||
import { NewCompanyBoardCard } from './NewCompanyBoardCard';
|
||||
|
||||
type OwnProps = {
|
||||
pipelineId: string;
|
||||
columnId: string;
|
||||
};
|
||||
|
||||
export function NewButton({ pipelineId, columnId }: OwnProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isCreatingCard, setIsCreatingCard] = useState(false);
|
||||
const [board, setBoard] = useRecoilState(boardColumnsState);
|
||||
const [boardItems, setBoardItems] = useRecoilState(boardItemsState);
|
||||
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const [createOnePipelineProgress] = useCreateOnePipelineProgressMutation({
|
||||
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||
});
|
||||
|
||||
const handleEntitySelect = useCallback(
|
||||
async (company: Pick<Company, 'id' | 'name' | 'domainName'>) => {
|
||||
if (!company) return;
|
||||
|
||||
setIsCreatingCard(false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
|
||||
const newUuid = uuidv4();
|
||||
const newBoard = JSON.parse(JSON.stringify(board));
|
||||
const destinationColumnIndex = newBoard.findIndex(
|
||||
(column: Column) => column.id === columnId,
|
||||
);
|
||||
newBoard[destinationColumnIndex].itemKeys.push(newUuid);
|
||||
setBoardItems({
|
||||
...boardItems,
|
||||
[newUuid]: {
|
||||
company,
|
||||
pipelineProgress: {
|
||||
id: newUuid,
|
||||
amount: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
setBoard(newBoard);
|
||||
await createOnePipelineProgress({
|
||||
variables: {
|
||||
uuid: newUuid,
|
||||
pipelineStageId: columnId,
|
||||
pipelineId,
|
||||
entityId: company.id,
|
||||
entityType: PipelineProgressableType.Company,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
createOnePipelineProgress,
|
||||
columnId,
|
||||
pipelineId,
|
||||
board,
|
||||
setBoard,
|
||||
boardItems,
|
||||
setBoardItems,
|
||||
goBackToPreviousHotkeyScope,
|
||||
],
|
||||
);
|
||||
|
||||
const handleNewClick = useCallback(() => {
|
||||
setIsCreatingCard(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
);
|
||||
}, [setIsCreatingCard, setHotkeyScopeAndMemorizePreviousScope]);
|
||||
|
||||
function handleCancel() {
|
||||
goBackToPreviousHotkeyScope();
|
||||
setIsCreatingCard(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCreatingCard && (
|
||||
<RecoilScope>
|
||||
<div ref={containerRef}>
|
||||
<NewCompanyBoardCard
|
||||
onEntitySelect={handleEntitySelect}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
</RecoilScope>
|
||||
)}
|
||||
<UINewButton onClick={handleNewClick} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { Board } from '../Board';
|
||||
|
||||
import { initialBoard, items } from './mock-data';
|
||||
|
||||
const meta: Meta<typeof Board> = {
|
||||
title: 'UI/Board/Board',
|
||||
component: Board,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Board>;
|
||||
|
||||
export const OneColumnBoard: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<Board
|
||||
pipelineId={'xxx-test'}
|
||||
columns={initialBoard}
|
||||
initialBoard={initialBoard}
|
||||
initialItems={items}
|
||||
onCardUpdate={async (_) => {}} // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
/>,
|
||||
),
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { Company } from '../../../../generated/graphql';
|
||||
import { mockedCompaniesData } from '../../../../testing/mock-data/companies';
|
||||
import { mockedPipelineProgressData } from '../../../../testing/mock-data/pipeline-progress';
|
||||
import { CompanyBoardCard } from '../CompanyBoardCard';
|
||||
|
||||
const meta: Meta<typeof CompanyBoardCard> = {
|
||||
title: 'UI/Board/CompanyBoardCard',
|
||||
component: CompanyBoardCard,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CompanyBoardCard>;
|
||||
|
||||
const FakeSelectableCompanyBoardCard = () => {
|
||||
const [selected, setSelected] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<CompanyBoardCard
|
||||
company={mockedCompaniesData[0] as Company}
|
||||
pipelineProgress={mockedPipelineProgressData[0]}
|
||||
selected={selected}
|
||||
onSelect={() => setSelected(!selected)}
|
||||
onCardUpdate={async (_) => {}} // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CompanyCompanyBoardCard: Story = {
|
||||
render: getRenderWrapperForComponent(<FakeSelectableCompanyBoardCard />),
|
||||
};
|
||||
@ -1,64 +0,0 @@
|
||||
import { Column } from '@/ui/board/components/Board';
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
|
||||
import { CompanyProgressDict } from '../Board';
|
||||
|
||||
export const items: CompanyProgressDict = {
|
||||
'item-1': {
|
||||
company: mockedCompaniesData[0],
|
||||
pipelineProgress: { id: '0', amount: 1 },
|
||||
},
|
||||
'item-2': {
|
||||
company: mockedCompaniesData[1],
|
||||
pipelineProgress: { id: '1', amount: 1 },
|
||||
},
|
||||
'item-3': {
|
||||
company: mockedCompaniesData[2],
|
||||
pipelineProgress: { id: '2', amount: 1 },
|
||||
},
|
||||
'item-4': {
|
||||
company: mockedCompaniesData[3],
|
||||
pipelineProgress: { id: '3', amount: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
export const initialBoard = [
|
||||
{
|
||||
id: 'column-1',
|
||||
title: 'New',
|
||||
colorCode: '#B76796',
|
||||
itemKeys: [
|
||||
'item-1',
|
||||
'item-2',
|
||||
'item-3',
|
||||
'item-4',
|
||||
'item-7',
|
||||
'item-8',
|
||||
'item-9',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'column-2',
|
||||
title: 'Screening',
|
||||
colorCode: '#CB912F',
|
||||
itemKeys: ['item-5', 'item-6'],
|
||||
},
|
||||
{
|
||||
id: 'column-3',
|
||||
colorCode: '#9065B0',
|
||||
title: 'Meeting',
|
||||
itemKeys: [],
|
||||
},
|
||||
{
|
||||
id: 'column-4',
|
||||
title: 'Proposal',
|
||||
colorCode: '#337EA9',
|
||||
itemKeys: [],
|
||||
},
|
||||
{
|
||||
id: 'column-5',
|
||||
colorCode: '#079039',
|
||||
title: 'Customer',
|
||||
itemKeys: [],
|
||||
},
|
||||
] satisfies Column[];
|
||||
@ -1,89 +0,0 @@
|
||||
import {
|
||||
Company,
|
||||
PipelineProgress,
|
||||
useGetCompaniesQuery,
|
||||
useGetPipelinesQuery,
|
||||
} from '../../../generated/graphql';
|
||||
import { Column } from '../../ui/board/components/Board';
|
||||
|
||||
type ItemCompany = Pick<Company, 'id' | 'name' | 'domainName'>;
|
||||
type ItemPipelineProgress = Pick<
|
||||
PipelineProgress,
|
||||
'id' | 'amount' | 'progressableId'
|
||||
>;
|
||||
|
||||
type Item = {
|
||||
company: ItemCompany;
|
||||
pipelineProgress: ItemPipelineProgress;
|
||||
};
|
||||
type Items = { [key: string]: Item };
|
||||
|
||||
export function useBoard(pipelineId: string) {
|
||||
const pipelines = useGetPipelinesQuery({
|
||||
variables: { where: { id: { equals: pipelineId } } },
|
||||
skip: pipelineId === '',
|
||||
});
|
||||
const pipelineStages = pipelines.data?.findManyPipeline[0]?.pipelineStages;
|
||||
const orderedPipelineStages = pipelineStages
|
||||
? [...pipelineStages].sort((a, b) => {
|
||||
if (!a.index || !b.index) return 0;
|
||||
return a.index - b.index;
|
||||
})
|
||||
: [];
|
||||
|
||||
const initialBoard: Column[] =
|
||||
orderedPipelineStages?.map((pipelineStage) => ({
|
||||
id: pipelineStage.id,
|
||||
title: pipelineStage.name,
|
||||
colorCode: pipelineStage.color,
|
||||
itemKeys:
|
||||
pipelineStage.pipelineProgresses?.map((item) => item.id as string) ||
|
||||
[],
|
||||
})) || [];
|
||||
|
||||
const pipelineProgresses = orderedPipelineStages?.reduce(
|
||||
(acc, pipelineStage) => [
|
||||
...acc,
|
||||
...(pipelineStage.pipelineProgresses || []),
|
||||
],
|
||||
[] as ItemPipelineProgress[],
|
||||
);
|
||||
|
||||
const entitiesQueryResult = useGetCompaniesQuery({
|
||||
variables: {
|
||||
where: {
|
||||
id: { in: pipelineProgresses?.map((item) => item.progressableId) },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const indexCompanyByIdReducer = (
|
||||
acc: { [key: string]: ItemCompany },
|
||||
entity: ItemCompany,
|
||||
) => ({
|
||||
...acc,
|
||||
[entity.id]: entity,
|
||||
});
|
||||
|
||||
const companiesDict = entitiesQueryResult.data?.companies.reduce(
|
||||
indexCompanyByIdReducer,
|
||||
{} as { [key: string]: ItemCompany },
|
||||
);
|
||||
|
||||
const items = pipelineProgresses?.reduce((acc, pipelineProgress) => {
|
||||
if (companiesDict?.[pipelineProgress.progressableId]) {
|
||||
acc[pipelineProgress.id] = {
|
||||
pipelineProgress,
|
||||
company: companiesDict[pipelineProgress.progressableId],
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {} as Items);
|
||||
|
||||
return {
|
||||
initialBoard,
|
||||
items,
|
||||
loading: pipelines.loading || entitiesQueryResult.loading,
|
||||
error: pipelines.error || entitiesQueryResult.error,
|
||||
};
|
||||
}
|
||||
@ -13,16 +13,24 @@ export const GET_PIPELINES = gql`
|
||||
index
|
||||
pipelineProgresses {
|
||||
id
|
||||
progressableType
|
||||
progressableId
|
||||
amount
|
||||
closeDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_PIPELINE_PROGRESS = gql`
|
||||
query GetPipelineProgress($where: PipelineProgressWhereInput) {
|
||||
findManyPipelineProgress(where: $where) {
|
||||
id
|
||||
progressableType
|
||||
progressableId
|
||||
amount
|
||||
closeDate
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_PIPELINE_PROGRESS = gql`
|
||||
mutation UpdateOnePipelineProgress(
|
||||
$id: String
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const BoardCardContext = createContext<string | null>(null);
|
||||
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const BoardColumnContext = createContext<string | null>(null);
|
||||
@ -1,8 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { Column } from '@/ui/board/components/Board';
|
||||
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
|
||||
|
||||
export const boardColumnsState = atom<Column[]>({
|
||||
export const boardColumnsState = atom<BoardPipelineStageColumn[]>({
|
||||
key: 'boardColumnsState',
|
||||
default: [],
|
||||
});
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { CompanyProgressDict } from '../components/Board';
|
||||
|
||||
export const boardItemsState = atom<CompanyProgressDict>({
|
||||
key: 'boardItemsState',
|
||||
default: {},
|
||||
});
|
||||
8
front/src/modules/pipeline-progress/states/boardState.ts
Normal file
8
front/src/modules/pipeline-progress/states/boardState.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
|
||||
|
||||
export const boardState = atom<BoardPipelineStageColumn[] | undefined>({
|
||||
key: 'boardState',
|
||||
default: undefined,
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { Pipeline } from '~/generated/graphql';
|
||||
|
||||
export const currentPipelineState = atom<Pipeline | undefined>({
|
||||
key: 'currentPipelineState',
|
||||
default: undefined,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isBoardLoadedState = atom<boolean>({
|
||||
key: 'isBoardLoadedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const pipelineProgressIdScopedState = atomFamily<string | null, string>({
|
||||
key: 'pipelineProgressIdScopedState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const pipelineStageIdScopedState = atomFamily<string | null, string>({
|
||||
key: 'pipelineStageIdScopedState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const selectedBoardCardsState = atom<string[]>({
|
||||
key: 'isBoardCardSelectedFamilyState',
|
||||
default: [],
|
||||
});
|
||||
@ -1,6 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const selectedBoardItemsState = atom<string[]>({
|
||||
key: 'selectedBoardItemsState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,4 @@
|
||||
export type BoardOptions = {
|
||||
newCardComponent: React.ReactNode;
|
||||
cardComponent: React.ReactNode;
|
||||
};
|
||||
@ -10,43 +10,46 @@ export const StyledBoard = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type Column = {
|
||||
id: string;
|
||||
export type BoardPipelineStageColumn = {
|
||||
pipelineStageId: string;
|
||||
title: string;
|
||||
colorCode?: string;
|
||||
itemKeys: string[];
|
||||
pipelineProgressIds: string[];
|
||||
};
|
||||
|
||||
export function getOptimisticlyUpdatedBoard(
|
||||
board: Column[],
|
||||
board: BoardPipelineStageColumn[],
|
||||
result: DropResult,
|
||||
) {
|
||||
// TODO: review any types
|
||||
const newBoard = JSON.parse(JSON.stringify(board));
|
||||
const { destination, source } = result;
|
||||
if (!destination) return;
|
||||
const sourceColumnIndex = newBoard.findIndex(
|
||||
(column: Column) => column.id === source.droppableId,
|
||||
(column: BoardPipelineStageColumn) =>
|
||||
column.pipelineStageId === source.droppableId,
|
||||
);
|
||||
const sourceColumn = newBoard[sourceColumnIndex];
|
||||
const destinationColumnIndex = newBoard.findIndex(
|
||||
(column: Column) => column.id === destination.droppableId,
|
||||
(column: BoardPipelineStageColumn) =>
|
||||
column.pipelineStageId === destination.droppableId,
|
||||
);
|
||||
const destinationColumn = newBoard[destinationColumnIndex];
|
||||
if (!destinationColumn || !sourceColumn) return;
|
||||
const sourceItems = sourceColumn.itemKeys;
|
||||
const destinationItems = destinationColumn.itemKeys;
|
||||
const sourceItems = sourceColumn.pipelineProgressIds;
|
||||
const destinationItems = destinationColumn.pipelineProgressIds;
|
||||
|
||||
const [removed] = sourceItems.splice(source.index, 1);
|
||||
destinationItems.splice(destination.index, 0, removed);
|
||||
|
||||
const newSourceColumn = {
|
||||
const newSourceColumn: BoardPipelineStageColumn = {
|
||||
...sourceColumn,
|
||||
itemKeys: sourceItems,
|
||||
pipelineProgressIds: sourceItems,
|
||||
};
|
||||
|
||||
const newDestinationColumn = {
|
||||
...destinationColumn,
|
||||
itemKeys: destinationItems,
|
||||
pipelineProgressIds: destinationItems,
|
||||
};
|
||||
|
||||
newBoard.splice(sourceColumnIndex, 1, newSourceColumn);
|
||||
|
||||
@ -33,16 +33,14 @@ export const StyledAmount = styled.div`
|
||||
type OwnProps = {
|
||||
colorCode?: string;
|
||||
title: string;
|
||||
amount: number;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function BoardColumn({ colorCode, title, amount, children }: OwnProps) {
|
||||
export function BoardColumn({ colorCode, title, children }: OwnProps) {
|
||||
return (
|
||||
<StyledColumn>
|
||||
<StyledHeader>
|
||||
<StyledColumnTitle color={colorCode}>• {title}</StyledColumnTitle>
|
||||
{!!amount && <StyledAmount>${amount}</StyledAmount>}
|
||||
</StyledHeader>
|
||||
{children}
|
||||
</StyledColumn>
|
||||
|
||||
@ -25,12 +25,14 @@ describe('getOptimisticlyUpdatedBoard', () => {
|
||||
{
|
||||
id: 'column-1',
|
||||
title: 'My Column',
|
||||
itemKeys: initialColumn1,
|
||||
pipelineStageId: 'column-1',
|
||||
pipelineProgressIds: initialColumn1,
|
||||
},
|
||||
{
|
||||
id: 'column-2',
|
||||
title: 'My Column',
|
||||
itemKeys: initialColumn2,
|
||||
pipelineStageId: 'column-2',
|
||||
pipelineProgressIds: initialColumn2,
|
||||
},
|
||||
];
|
||||
|
||||
@ -40,12 +42,14 @@ describe('getOptimisticlyUpdatedBoard', () => {
|
||||
{
|
||||
id: 'column-1',
|
||||
title: 'My Column',
|
||||
itemKeys: finalColumn1,
|
||||
pipelineStageId: 'column-1',
|
||||
pipelineProgressIds: finalColumn1,
|
||||
},
|
||||
{
|
||||
id: 'column-2',
|
||||
title: 'My Column',
|
||||
itemKeys: finalColumn2,
|
||||
pipelineStageId: 'column-2',
|
||||
pipelineProgressIds: finalColumn2,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
138
front/src/pages/opportunities/HookCompanyBoard.tsx
Normal file
138
front/src/pages/opportunities/HookCompanyBoard.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
CompanyForBoard,
|
||||
CompanyProgress,
|
||||
PipelineProgressForBoard,
|
||||
} from '@/companies/types/CompanyProgress';
|
||||
import { BoardPipelineStageColumn } from '@/ui/board/components/Board';
|
||||
import {
|
||||
Pipeline,
|
||||
PipelineStage,
|
||||
useGetCompaniesQuery,
|
||||
useGetPipelineProgressQuery,
|
||||
useGetPipelinesQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { boardState } from '../../modules/pipeline-progress/states/boardState';
|
||||
|
||||
import { companyProgressesFamilyState } from './companyProgressesFamilyState';
|
||||
import { currentPipelineState } from './currentPipelineState';
|
||||
import { isBoardLoadedState } from './isBoardLoadedState';
|
||||
export function HookCompanyBoard() {
|
||||
const [currentPipeline, setCurrentPipeline] =
|
||||
useRecoilState(currentPipelineState);
|
||||
|
||||
const [, setBoard] = useRecoilState(boardState);
|
||||
|
||||
const [, setIsBoardLoaded] = useRecoilState(isBoardLoadedState);
|
||||
|
||||
useGetPipelinesQuery({
|
||||
onCompleted: async (data) => {
|
||||
const pipeline = data?.findManyPipeline[0] as Pipeline;
|
||||
setCurrentPipeline(pipeline);
|
||||
const pipelineStages = pipeline?.pipelineStages;
|
||||
const orderedPipelineStages = pipelineStages
|
||||
? [...pipelineStages].sort((a, b) => {
|
||||
if (!a.index || !b.index) return 0;
|
||||
return a.index - b.index;
|
||||
})
|
||||
: [];
|
||||
const initialBoard: BoardPipelineStageColumn[] =
|
||||
orderedPipelineStages?.map((pipelineStage) => ({
|
||||
pipelineStageId: pipelineStage.id,
|
||||
title: pipelineStage.name,
|
||||
colorCode: pipelineStage.color,
|
||||
pipelineProgressIds:
|
||||
pipelineStage.pipelineProgresses?.map(
|
||||
(item) => item.id as string,
|
||||
) || [],
|
||||
})) || [];
|
||||
setBoard(initialBoard);
|
||||
setIsBoardLoaded(true);
|
||||
},
|
||||
});
|
||||
|
||||
const pipelineProgressIds = currentPipeline?.pipelineStages
|
||||
?.map((pipelineStage: PipelineStage) =>
|
||||
(
|
||||
pipelineStage.pipelineProgresses?.map((item) => item.id as string) || []
|
||||
).flat(),
|
||||
)
|
||||
.flat();
|
||||
|
||||
const pipelineProgressesQuery = useGetPipelineProgressQuery({
|
||||
variables: {
|
||||
where: {
|
||||
id: { in: pipelineProgressIds },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pipelineProgresses =
|
||||
pipelineProgressesQuery.data?.findManyPipelineProgress || [];
|
||||
|
||||
const entitiesQueryResult = useGetCompaniesQuery({
|
||||
variables: {
|
||||
where: {
|
||||
id: {
|
||||
in: pipelineProgresses.map((item) => item.progressableId),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const indexCompanyByIdReducer = (
|
||||
acc: { [key: string]: CompanyForBoard },
|
||||
company: CompanyForBoard,
|
||||
) => ({
|
||||
...acc,
|
||||
[company.id]: company,
|
||||
});
|
||||
|
||||
const companiesDict =
|
||||
entitiesQueryResult.data?.companies.reduce(
|
||||
indexCompanyByIdReducer,
|
||||
{} as { [key: string]: CompanyForBoard },
|
||||
) || {};
|
||||
|
||||
const indexPipelineProgressByIdReducer = (
|
||||
acc: {
|
||||
[key: string]: CompanyProgress;
|
||||
},
|
||||
pipelineProgress: PipelineProgressForBoard,
|
||||
) => {
|
||||
const company = companiesDict[pipelineProgress.progressableId];
|
||||
return {
|
||||
...acc,
|
||||
[pipelineProgress.id]: {
|
||||
pipelineProgress,
|
||||
company,
|
||||
},
|
||||
};
|
||||
};
|
||||
const companyBoardIndex = pipelineProgresses.reduce(
|
||||
indexPipelineProgressByIdReducer,
|
||||
{} as { [key: string]: CompanyProgress },
|
||||
);
|
||||
|
||||
const synchronizeCompanyProgresses = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(companyBoardIndex: { [key: string]: CompanyProgress }) => {
|
||||
Object.entries(companyBoardIndex).forEach(([id, companyProgress]) => {
|
||||
set(companyProgressesFamilyState(id), companyProgress);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const loading =
|
||||
entitiesQueryResult.loading || pipelineProgressesQuery.loading;
|
||||
|
||||
useEffect(() => {
|
||||
!loading && synchronizeCompanyProgresses(companyBoardIndex);
|
||||
}, [loading, companyBoardIndex, synchronizeCompanyProgresses]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
@ -1,97 +1,31 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { companyBoardOptions } from '@/companies/components/companyBoardOptions';
|
||||
import { CompanyBoardContext } from '@/companies/states/CompanyBoardContext';
|
||||
import { BoardActionBarButtonDeletePipelineProgress } from '@/pipeline-progress/components/BoardActionBarButtonDeletePipelineProgress';
|
||||
import { EntityBoard } from '@/pipeline-progress/components/EntityBoard';
|
||||
import { EntityBoardActionBar } from '@/pipeline-progress/components/EntityBoardActionBar';
|
||||
import { GET_PIPELINES } from '@/pipeline-progress/queries';
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { IconTargetArrow } from '@/ui/icons/index';
|
||||
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
|
||||
|
||||
import {
|
||||
PipelineProgress,
|
||||
PipelineStage,
|
||||
useGetPipelinesQuery,
|
||||
useUpdateOnePipelineProgressMutation,
|
||||
useUpdateOnePipelineProgressStageMutation,
|
||||
} from '../../generated/graphql';
|
||||
import { Board } from '../../modules/pipeline-progress/components/Board';
|
||||
import { useBoard } from '../../modules/pipeline-progress/hooks/useBoard';
|
||||
import { HookCompanyBoard } from './HookCompanyBoard';
|
||||
|
||||
export function Opportunities() {
|
||||
const theme = useTheme();
|
||||
|
||||
const pipelines = useGetPipelinesQuery();
|
||||
const pipelineId = pipelines.data?.findManyPipeline[0]?.id;
|
||||
|
||||
const { initialBoard, items } = useBoard(pipelineId || '');
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
initialBoard?.map(({ id, colorCode, title }) => ({
|
||||
id,
|
||||
colorCode,
|
||||
title,
|
||||
})),
|
||||
[initialBoard],
|
||||
);
|
||||
const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation();
|
||||
const [updatePipelineProgressStage] =
|
||||
useUpdateOnePipelineProgressStageMutation();
|
||||
|
||||
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 handleCardMove = useCallback(
|
||||
async (
|
||||
pipelineProgressId: NonNullable<PipelineProgress['id']>,
|
||||
pipelineStageId: NonNullable<PipelineStage['id']>,
|
||||
) => {
|
||||
updatePipelineProgressStage({
|
||||
variables: {
|
||||
id: pipelineProgressId,
|
||||
pipelineStageId,
|
||||
},
|
||||
});
|
||||
},
|
||||
[updatePipelineProgressStage],
|
||||
);
|
||||
|
||||
return (
|
||||
<WithTopBarContainer
|
||||
title="Opportunities"
|
||||
icon={<IconTargetArrow size={theme.icon.size.md} />}
|
||||
>
|
||||
{items && pipelineId ? (
|
||||
<>
|
||||
<Board
|
||||
pipelineId={pipelineId}
|
||||
columns={columns || []}
|
||||
initialBoard={initialBoard}
|
||||
initialItems={items}
|
||||
onCardMove={handleCardMove}
|
||||
onCardUpdate={handleCardUpdate}
|
||||
/>
|
||||
<EntityBoardActionBar>
|
||||
<BoardActionBarButtonDeletePipelineProgress />
|
||||
</EntityBoardActionBar>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<HookCompanyBoard />
|
||||
<RecoilScope SpecificContext={CompanyBoardContext}>
|
||||
<EntityBoard boardOptions={companyBoardOptions} />
|
||||
<EntityBoardActionBar>
|
||||
<BoardActionBarButtonDeletePipelineProgress />
|
||||
</EntityBoardActionBar>
|
||||
</RecoilScope>
|
||||
</WithTopBarContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { CompanyProgress } from '@/companies/types/CompanyProgress';
|
||||
|
||||
export const companyProgressesFamilyState = atomFamily<
|
||||
CompanyProgress | undefined,
|
||||
string
|
||||
>({
|
||||
key: 'companyProgressesFamilyState',
|
||||
default: undefined,
|
||||
});
|
||||
8
front/src/pages/opportunities/currentPipelineState.ts
Normal file
8
front/src/pages/opportunities/currentPipelineState.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { Pipeline } from '~/generated/graphql';
|
||||
|
||||
export const currentPipelineState = atom<Pipeline | undefined>({
|
||||
key: 'currentPipelineState',
|
||||
default: undefined,
|
||||
});
|
||||
6
front/src/pages/opportunities/isBoardLoadedState.ts
Normal file
6
front/src/pages/opportunities/isBoardLoadedState.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const isBoardLoadedState = atom<boolean>({
|
||||
key: 'isBoardLoadedState',
|
||||
default: false,
|
||||
});
|
||||
@ -1,7 +1,16 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { Decorator } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { pipeline } from '@/companies/__stories__/mock-data';
|
||||
import { CompanyBoardContext } from '@/companies/states/CompanyBoardContext';
|
||||
import { BoardCardContext } from '@/pipeline-progress/states/BoardCardContext';
|
||||
import { BoardColumnContext } from '@/pipeline-progress/states/BoardColumnContext';
|
||||
import { pipelineProgressIdScopedState } from '@/pipeline-progress/states/pipelineProgressIdScopedState';
|
||||
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { HookCompanyBoard } from '~/pages/opportunities/HookCompanyBoard';
|
||||
|
||||
import { RecoilScope } from '../modules/recoil-scope/components/RecoilScope';
|
||||
import { CellContext } from '../modules/ui/tables/states/CellContext';
|
||||
import { RowContext } from '../modules/ui/tables/states/RowContext';
|
||||
@ -30,3 +39,41 @@ export const CellPositionDecorator: Decorator = (Story) => (
|
||||
</RecoilScope>
|
||||
</RecoilScope>
|
||||
);
|
||||
|
||||
export const BoardDecorator: Decorator = (Story) => (
|
||||
<>
|
||||
<HookCompanyBoard />
|
||||
<RecoilScope SpecificContext={CompanyBoardContext}>
|
||||
<Story />
|
||||
</RecoilScope>
|
||||
</>
|
||||
);
|
||||
|
||||
function HookLoadFakeBoardContextState() {
|
||||
const [, setPipelineProgressId] = useRecoilScopedState(
|
||||
pipelineProgressIdScopedState,
|
||||
BoardCardContext,
|
||||
);
|
||||
const pipelineProgress =
|
||||
pipeline?.pipelineStages?.[0]?.pipelineProgresses?.[0];
|
||||
useEffect(() => {
|
||||
setPipelineProgressId(pipelineProgress?.id || '');
|
||||
}, [pipelineProgress?.id, setPipelineProgressId]);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export const BoardCardDecorator: Decorator = (Story) => {
|
||||
return (
|
||||
<>
|
||||
<HookCompanyBoard />
|
||||
<RecoilScope SpecificContext={CompanyBoardContext}>
|
||||
<RecoilScope SpecificContext={BoardColumnContext}>
|
||||
<RecoilScope SpecificContext={BoardCardContext}>
|
||||
<HookLoadFakeBoardContextState />
|
||||
<Story />
|
||||
</RecoilScope>
|
||||
</RecoilScope>
|
||||
</RecoilScope>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,7 +5,10 @@ import { CREATE_EVENT } from '@/analytics/services';
|
||||
import { GET_CLIENT_CONFIG } from '@/client-config/queries';
|
||||
import { GET_COMPANIES } from '@/companies/services';
|
||||
import { GET_PEOPLE, UPDATE_PERSON } from '@/people/services';
|
||||
import { GET_PIPELINES } from '@/pipeline-progress/queries';
|
||||
import {
|
||||
GET_PIPELINE_PROGRESS,
|
||||
GET_PIPELINES,
|
||||
} from '@/pipeline-progress/queries';
|
||||
import {
|
||||
SEARCH_COMPANY_QUERY,
|
||||
SEARCH_USER_QUERY,
|
||||
@ -20,6 +23,7 @@ import {
|
||||
|
||||
import { mockedCompaniesData } from './mock-data/companies';
|
||||
import { mockedPeopleData } from './mock-data/people';
|
||||
import { mockedPipelineProgressData } from './mock-data/pipeline-progress';
|
||||
import { mockedPipelinesData } from './mock-data/pipelines';
|
||||
import { mockedUsersData } from './mock-data/users';
|
||||
import { filterAndSortData, updateOneFromData } from './mock-data';
|
||||
@ -113,6 +117,16 @@ export const graphqlMocks = [
|
||||
}),
|
||||
);
|
||||
}),
|
||||
graphql.query(
|
||||
getOperationName(GET_PIPELINE_PROGRESS) ?? '',
|
||||
(req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
findManyPipelineProgress: mockedPipelineProgressData,
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
graphql.mutation(getOperationName(CREATE_EVENT) ?? '', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
|
||||
@ -2,7 +2,7 @@ import { PipelineProgress, User } from '../../generated/graphql';
|
||||
|
||||
type MockedPipelineProgress = Pick<
|
||||
PipelineProgress,
|
||||
'id' | 'amount' | 'closeDate'
|
||||
'id' | 'amount' | 'closeDate' | 'progressableId'
|
||||
> & {
|
||||
accountOwner: Pick<
|
||||
User,
|
||||
@ -10,17 +10,34 @@ type MockedPipelineProgress = Pick<
|
||||
> | null;
|
||||
};
|
||||
|
||||
const accountOwner = {
|
||||
email: 'charles@test.com',
|
||||
displayName: 'Charles Test',
|
||||
firstName: 'Charles',
|
||||
lastName: 'Test',
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
};
|
||||
|
||||
export const mockedPipelineProgressData: Array<MockedPipelineProgress> = [
|
||||
{
|
||||
id: '0ac8761c-1ad6-11ee-be56-0242ac120002',
|
||||
amount: 78,
|
||||
accountOwner: {
|
||||
email: 'charles@test.com',
|
||||
displayName: 'Charles Test',
|
||||
firstName: 'Charles',
|
||||
lastName: 'Test',
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
__typename: 'User',
|
||||
},
|
||||
closeDate: '2021-10-01T00:00:00.000Z',
|
||||
progressableId: '0',
|
||||
accountOwner: accountOwner,
|
||||
},
|
||||
{
|
||||
id: 'fe256b39-3ec3-4fe7-8998-b76aa0bfb600',
|
||||
progressableId: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||
amount: 7,
|
||||
closeDate: '2021-10-01T00:00:00.000Z',
|
||||
accountOwner,
|
||||
},
|
||||
{
|
||||
id: '4a886c90-f4f2-4984-8222-882ebbb905d6',
|
||||
progressableId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
|
||||
amount: 100,
|
||||
closeDate: '2021-10-01T00:00:00.000Z',
|
||||
accountOwner,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user