Enable opportunity card deletion (#490)
* Add checkbox * Add state management for selected opportunities * Use recoil for selected items state, show action bar * Deduplicate code * Add delete action * Enable delete * Add color for selected cards * update board state on delete * Add stories * Enable empty board * Fix story * Handle dark mdoe * Nits * Rename module * Better naming * Fix naming confusion process<>progress
This commit is contained in:
167
front/src/modules/pipeline-progress/components/Board.tsx
Normal file
167
front/src/modules/pipeline-progress/components/Board.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { useCallback, useEffect, 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/components/board/BoardColumn';
|
||||
import { Company } from '~/generated/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
getOptimisticlyUpdatedBoard,
|
||||
StyledBoard,
|
||||
} from '../../ui/components/board/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 = Pick<
|
||||
Company,
|
||||
'id' | 'name' | 'domainName' | 'createdAt'
|
||||
>;
|
||||
export type CompanyProgressDict = {
|
||||
[key: string]: CompanyProgress;
|
||||
};
|
||||
|
||||
type BoardProps = {
|
||||
pipelineId: string;
|
||||
columns: Omit<Column, 'itemKeys'>[];
|
||||
initialBoard: Column[];
|
||||
initialItems: CompanyProgressDict;
|
||||
onUpdate?: (itemKey: string, columnId: Column['id']) => 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,
|
||||
onUpdate,
|
||||
pipelineId,
|
||||
}: BoardProps) {
|
||||
const [board, setBoard] = useRecoilState(boardColumnsState);
|
||||
const [boardItems, setBoardItems] = useRecoilState(boardItemsState);
|
||||
const [selectedBoardItems, setSelectedBoardItems] = useRecoilState(
|
||||
selectedBoardItemsState,
|
||||
);
|
||||
const [isInitialBoardLoaded, setIsInitialBoardLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setBoard(initialBoard);
|
||||
if (Object.keys(initialItems).length === 0 || isInitialBoardLoaded) return;
|
||||
setBoardItems(initialItems);
|
||||
setIsInitialBoardLoaded(true);
|
||||
}, [
|
||||
initialBoard,
|
||||
setBoard,
|
||||
initialItems,
|
||||
setBoardItems,
|
||||
setIsInitialBoardLoaded,
|
||||
isInitialBoardLoaded,
|
||||
]);
|
||||
|
||||
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 &&
|
||||
onUpdate &&
|
||||
(await onUpdate(draggedEntityId, destinationColumnId));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
[board, onUpdate, 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} 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]}
|
||||
selected={selectedBoardItems.includes(itemKey)}
|
||||
onSelect={() => handleSelect(itemKey)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
),
|
||||
)}
|
||||
</BoardColumnCardsContainer>
|
||||
<NewButton pipelineId={pipelineId} columnId={column.id} />
|
||||
</BoardColumn>
|
||||
)}
|
||||
</Droppable>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</StyledBoard>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
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';
|
||||
|
||||
export function BoardActionBarButtonDeletePipelineProgress() {
|
||||
const [selectedBoardItems, setSelectedBoardItems] = useRecoilState(
|
||||
selectedBoardItemsState,
|
||||
);
|
||||
const [boardItems, setBoardItems] = useRecoilState(boardItemsState);
|
||||
|
||||
const [deletePipelineProgress] = useDeleteManyPipelineProgressMutation({
|
||||
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||
});
|
||||
|
||||
async function handleDeleteClick() {
|
||||
await deletePipelineProgress({
|
||||
variables: {
|
||||
ids: selectedBoardItems,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('boardItems', boardItems);
|
||||
|
||||
setBoardItems(
|
||||
Object.fromEntries(
|
||||
Object.entries(boardItems).filter(
|
||||
([key]) => !selectedBoardItems.includes(key),
|
||||
),
|
||||
),
|
||||
);
|
||||
setSelectedBoardItems([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<EntityTableActionBarButton
|
||||
label="Delete"
|
||||
icon={<IconTrash size={16} />}
|
||||
type="warning"
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Company } from '../../../generated/graphql';
|
||||
import { PersonChip } from '../../people/components/PersonChip';
|
||||
import { Checkbox } from '../../ui/components/form/Checkbox';
|
||||
import { IconCalendarEvent, IconUser, IconUsers } from '../../ui/icons';
|
||||
import { getLogoUrlFromDomainName, humanReadableDate } from '../../utils/utils';
|
||||
|
||||
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: 4px;
|
||||
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;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
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)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type CompanyProp = Pick<
|
||||
Company,
|
||||
'id' | 'name' | 'domainName' | 'employees' | 'createdAt' | 'accountOwner'
|
||||
>;
|
||||
|
||||
export function CompanyBoardCard({
|
||||
company,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
company: CompanyProp;
|
||||
selected: boolean;
|
||||
onSelect: (company: CompanyProp) => void;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
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 }} />
|
||||
<Checkbox checked={selected} onChange={() => onSelect(company)} />
|
||||
</StyledBoardCardHeader>
|
||||
<StyledBoardCardBody>
|
||||
<span>
|
||||
<IconUser size={theme.icon.size.md} />
|
||||
<PersonChip name={company.accountOwner?.displayName || ''} />
|
||||
</span>
|
||||
<span>
|
||||
<IconUsers size={theme.icon.size.md} /> {company.employees}
|
||||
</span>
|
||||
<span>
|
||||
<IconCalendarEvent size={theme.icon.size.md} />
|
||||
{humanReadableDate(new Date(company.createdAt as string))}
|
||||
</span>
|
||||
</StyledBoardCardBody>
|
||||
</StyledBoardCard>
|
||||
</StyledBoardCardWrapper>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActionBar } from '@/ui/components/action-bar/ActionBar';
|
||||
|
||||
import { selectedBoardItemsState } from '../states/selectedBoardItemsState';
|
||||
|
||||
type OwnProps = {
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
};
|
||||
|
||||
export function EntityBoardActionBar({ children }: OwnProps) {
|
||||
const selectedBoardItems = useRecoilValue(selectedBoardItemsState);
|
||||
return <ActionBar selectedIds={selectedBoardItems}>{children}</ActionBar>;
|
||||
}
|
||||
83
front/src/modules/pipeline-progress/components/NewButton.tsx
Normal file
83
front/src/modules/pipeline-progress/components/NewButton.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { Column } from '@/ui/components/board/Board';
|
||||
import { NewButton as UINewButton } from '@/ui/components/board/NewButton';
|
||||
import {
|
||||
Company,
|
||||
PipelineProgressableType,
|
||||
useCreateOnePipelineProgressMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
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 [isCreatingCard, setIsCreatingCard] = useState(false);
|
||||
const [board, setBoard] = useRecoilState(boardColumnsState);
|
||||
const [boardItems, setBoardItems] = useRecoilState(boardItemsState);
|
||||
|
||||
const [createOnePipelineProgress] = useCreateOnePipelineProgressMutation();
|
||||
const onEntitySelect = useCallback(
|
||||
async (company: Pick<Company, 'id' | 'name' | 'domainName'>) => {
|
||||
setIsCreatingCard(false);
|
||||
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]: {
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
domainName: company.domainName,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
setBoard(newBoard);
|
||||
await createOnePipelineProgress({
|
||||
variables: {
|
||||
uuid: newUuid,
|
||||
pipelineStageId: columnId,
|
||||
pipelineId,
|
||||
entityId: company.id,
|
||||
entityType: PipelineProgressableType.Company,
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
createOnePipelineProgress,
|
||||
columnId,
|
||||
pipelineId,
|
||||
board,
|
||||
setBoard,
|
||||
boardItems,
|
||||
setBoardItems,
|
||||
],
|
||||
);
|
||||
|
||||
const onNewClick = useCallback(() => {
|
||||
setIsCreatingCard(true);
|
||||
}, [setIsCreatingCard]);
|
||||
return (
|
||||
<>
|
||||
{isCreatingCard && (
|
||||
<RecoilScope>
|
||||
<NewCompanyBoardCard onEntitySelect={onEntitySelect} />
|
||||
</RecoilScope>
|
||||
)}
|
||||
<UINewButton onClick={onNewClick} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
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';
|
||||
|
||||
type OwnProps = {
|
||||
onEntitySelect: (
|
||||
company: Pick<Company, 'id' | 'name' | 'domainName'>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export function NewCompanyBoardCard({ onEntitySelect }: OwnProps) {
|
||||
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 (
|
||||
<SingleEntitySelect
|
||||
onEntitySelected={(value) => onEntitySelect(value)}
|
||||
entities={{
|
||||
entitiesToSelect: companies.entitiesToSelect,
|
||||
selectedEntity: companies.selectedEntities[0],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
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}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { StrictMode, useState } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Company } from '../../../../generated/graphql';
|
||||
import { mockedCompaniesData } from '../../../../testing/mock-data/companies';
|
||||
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}
|
||||
selected={selected}
|
||||
onSelect={() => setSelected(!selected)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CompanyCompanyBoardCard: Story = {
|
||||
render: () => (
|
||||
<StrictMode>
|
||||
<FakeSelectableCompanyBoardCard />
|
||||
</StrictMode>
|
||||
),
|
||||
};
|
||||
@ -0,0 +1,60 @@
|
||||
import { Column } from '@/ui/components/board/Board';
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
|
||||
import { CompanyProgressDict } from '../Board';
|
||||
|
||||
export const items: CompanyProgressDict = {
|
||||
'item-1': mockedCompaniesData[0],
|
||||
'item-2': mockedCompaniesData[1],
|
||||
'item-3': mockedCompaniesData[2],
|
||||
'item-4': mockedCompaniesData[3],
|
||||
};
|
||||
|
||||
for (let i = 7; i <= 20; i++) {
|
||||
const key = `item-${i}`;
|
||||
items[key] = {
|
||||
...mockedCompaniesData[i % mockedCompaniesData.length],
|
||||
id: key,
|
||||
};
|
||||
}
|
||||
|
||||
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[];
|
||||
70
front/src/modules/pipeline-progress/hooks/useBoard.ts
Normal file
70
front/src/modules/pipeline-progress/hooks/useBoard.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import {
|
||||
Company,
|
||||
useGetCompaniesQuery,
|
||||
useGetPipelinesQuery,
|
||||
} from '../../../generated/graphql';
|
||||
import { Column } from '../../ui/components/board/Board';
|
||||
|
||||
type Item = Pick<Company, 'id' | 'name' | 'createdAt' | 'domainName'>;
|
||||
type Items = { [key: string]: Item };
|
||||
|
||||
export function useBoard(pipelineId: string) {
|
||||
const pipelines = useGetPipelinesQuery({
|
||||
variables: { where: { id: { equals: pipelineId } } },
|
||||
});
|
||||
const pipelineStages = pipelines.data?.findManyPipeline[0]?.pipelineStages;
|
||||
|
||||
const initialBoard: Column[] =
|
||||
pipelineStages?.map((pipelineStage) => ({
|
||||
id: pipelineStage.id,
|
||||
title: pipelineStage.name,
|
||||
colorCode: pipelineStage.color,
|
||||
itemKeys:
|
||||
pipelineStage.pipelineProgresses?.map((item) => item.id as string) ||
|
||||
[],
|
||||
})) || [];
|
||||
|
||||
const pipelineProgresses = pipelineStages?.reduce(
|
||||
(acc, pipelineStage) => [
|
||||
...acc,
|
||||
...(pipelineStage.pipelineProgresses?.map((item) => ({
|
||||
progressableId: item?.progressableId,
|
||||
pipelineProgressId: item?.id,
|
||||
})) || []),
|
||||
],
|
||||
[] as { progressableId: string; pipelineProgressId: string }[],
|
||||
);
|
||||
|
||||
const entitiesQueryResult = useGetCompaniesQuery({
|
||||
variables: {
|
||||
where: {
|
||||
id: { in: pipelineProgresses?.map((item) => item.progressableId) },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const indexByIdReducer = (acc: Items, entity: Item) => ({
|
||||
...acc,
|
||||
[entity.id]: entity,
|
||||
});
|
||||
|
||||
const companiesDict = entitiesQueryResult.data?.companies.reduce(
|
||||
indexByIdReducer,
|
||||
{} as Items,
|
||||
);
|
||||
|
||||
const items = pipelineProgresses?.reduce((acc, pipelineProgress) => {
|
||||
if (companiesDict?.[pipelineProgress.progressableId]) {
|
||||
acc[pipelineProgress.pipelineProgressId] =
|
||||
companiesDict[pipelineProgress.progressableId];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Items);
|
||||
|
||||
return {
|
||||
initialBoard,
|
||||
items,
|
||||
loading: pipelines.loading || entitiesQueryResult.loading,
|
||||
error: pipelines.error || entitiesQueryResult.error,
|
||||
};
|
||||
}
|
||||
54
front/src/modules/pipeline-progress/queries/index.ts
Normal file
54
front/src/modules/pipeline-progress/queries/index.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_PIPELINES = gql`
|
||||
query GetPipelines($where: PipelineWhereInput) {
|
||||
findManyPipeline(where: $where) {
|
||||
id
|
||||
name
|
||||
pipelineProgressableType
|
||||
pipelineStages {
|
||||
id
|
||||
name
|
||||
color
|
||||
pipelineProgresses {
|
||||
id
|
||||
progressableType
|
||||
progressableId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_PIPELINE_STAGE = gql`
|
||||
mutation UpdateOnePipelineProgress($id: String, $pipelineStageId: String) {
|
||||
updateOnePipelineProgress(
|
||||
where: { id: $id }
|
||||
data: { pipelineStage: { connect: { id: $pipelineStageId } } }
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_ENTITY_TO_PIPELINE = gql`
|
||||
mutation CreateOnePipelineProgress(
|
||||
$uuid: String!
|
||||
$entityType: PipelineProgressableType!
|
||||
$entityId: String!
|
||||
$pipelineId: String!
|
||||
$pipelineStageId: String!
|
||||
) {
|
||||
createOnePipelineProgress(
|
||||
data: {
|
||||
id: $uuid
|
||||
progressableType: $entityType
|
||||
progressableId: $entityId
|
||||
pipeline: { connect: { id: $pipelineId } }
|
||||
pipelineStage: { connect: { id: $pipelineStageId } }
|
||||
}
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
1
front/src/modules/pipeline-progress/services/index.ts
Normal file
1
front/src/modules/pipeline-progress/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './update';
|
||||
9
front/src/modules/pipeline-progress/services/update.ts
Normal file
9
front/src/modules/pipeline-progress/services/update.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_PIPELINE_PROGRESS = gql`
|
||||
mutation DeleteManyPipelineProgress($ids: [String!]) {
|
||||
deleteManyPipelineProgress(where: { id: { in: $ids } }) {
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { Column } from '@/ui/components/board/Board';
|
||||
|
||||
export const boardColumnsState = atom<Column[]>({
|
||||
key: 'boardColumnsState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { CompanyProgressDict } from '../components/Board';
|
||||
|
||||
export const boardItemsState = atom<CompanyProgressDict>({
|
||||
key: 'boardItemsState',
|
||||
default: {},
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const selectedBoardItemsState = atom<string[]>({
|
||||
key: 'selectedBoardItemsState',
|
||||
default: [],
|
||||
});
|
||||
Reference in New Issue
Block a user