338 on opportunities page when i associate a new company to a pipelinestage its persisted in db (#339)
* feature: add navigation for opportunities * chore: add companies in pipeline seed * feature: make the board scrollable * feature: make the board scrollable vertically * feature: remove board container * feature: fix newButton style * feature: add onClickNew method on board * feature: call backend with hardcoded id for new pipeline progressable * feature: refetch board on click on new * feature: use pipelineProgressId instead of entityId to ensure unicity of itemKey * feature: avoid rerender of columns when refetching
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
@ -10,11 +10,13 @@ import {
|
||||
BoardItemKey,
|
||||
Column,
|
||||
getOptimisticlyUpdatedBoard,
|
||||
Item,
|
||||
Items,
|
||||
StyledBoard,
|
||||
} from '../../ui/components/board/Board';
|
||||
import {
|
||||
ItemsContainer,
|
||||
ScrollableColumn,
|
||||
StyledColumn,
|
||||
StyledColumnTitle,
|
||||
} from '../../ui/components/board/BoardColumn';
|
||||
@ -24,21 +26,43 @@ import { NewButton } from '../../ui/components/board/BoardNewButton';
|
||||
import { BoardCard } from './BoardCard';
|
||||
|
||||
type BoardProps = {
|
||||
columns: Omit<Column, 'itemKeys'>[];
|
||||
initialBoard: Column[];
|
||||
items: Items;
|
||||
onUpdate?: (itemKey: BoardItemKey, columnId: Column['id']) => Promise<void>;
|
||||
onClickNew?: (
|
||||
columnId: Column['id'],
|
||||
newItem: Partial<Item> & { id: string },
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const Board = ({ initialBoard, items, onUpdate }: BoardProps) => {
|
||||
export const Board = ({
|
||||
columns,
|
||||
initialBoard,
|
||||
items,
|
||||
onUpdate,
|
||||
onClickNew,
|
||||
}: BoardProps) => {
|
||||
const [board, setBoard] = useState<Column[]>(initialBoard);
|
||||
|
||||
const onClickFunctions = useMemo<
|
||||
Record<Column['id'], (newItem: Partial<Item> & { id: string }) => void>
|
||||
>(() => {
|
||||
return board.reduce((acc, column) => {
|
||||
acc[column.id] = (newItem: Partial<Item> & { id: string }) => {
|
||||
onClickNew && onClickNew(column.id, newItem);
|
||||
};
|
||||
return acc;
|
||||
}, {} as Record<Column['id'], (newItem: Partial<Item> & { id: string }) => void>);
|
||||
}, [board, onClickNew]);
|
||||
|
||||
const onDragEnd: OnDragEndResponder = useCallback(
|
||||
async (result) => {
|
||||
const newBoard = getOptimisticlyUpdatedBoard(board, result);
|
||||
if (!newBoard) return;
|
||||
setBoard(newBoard);
|
||||
try {
|
||||
const draggedEntityId = items[result.draggableId]?.id;
|
||||
const draggedEntityId = result.draggableId;
|
||||
const destinationColumnId = result.destination?.droppableId;
|
||||
draggedEntityId &&
|
||||
destinationColumnId &&
|
||||
@ -48,35 +72,38 @@ export const Board = ({ initialBoard, items, onUpdate }: BoardProps) => {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
[board, onUpdate, items],
|
||||
[board, onUpdate],
|
||||
);
|
||||
|
||||
console.log('board', board);
|
||||
return (
|
||||
<StyledBoard>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{board.map((column) => (
|
||||
{columns.map((column, columnIndex) => (
|
||||
<Droppable key={column.id} droppableId={column.id}>
|
||||
{(droppableProvided) => (
|
||||
<StyledColumn>
|
||||
<StyledColumnTitle color={column.colorCode}>
|
||||
• {column.title}
|
||||
</StyledColumnTitle>
|
||||
<ItemsContainer droppableProvided={droppableProvided}>
|
||||
{column.itemKeys.map((itemKey, index) => (
|
||||
<Draggable
|
||||
key={itemKey}
|
||||
draggableId={itemKey}
|
||||
index={index}
|
||||
>
|
||||
{(draggableProvided) => (
|
||||
<BoardItem draggableProvided={draggableProvided}>
|
||||
<BoardCard item={items[itemKey]} />
|
||||
</BoardItem>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</ItemsContainer>
|
||||
<NewButton />
|
||||
<ScrollableColumn>
|
||||
<ItemsContainer droppableProvided={droppableProvided}>
|
||||
{board[columnIndex].itemKeys.map((itemKey, index) => (
|
||||
<Draggable
|
||||
key={itemKey}
|
||||
draggableId={itemKey}
|
||||
index={index}
|
||||
>
|
||||
{(draggableProvided) => (
|
||||
<BoardItem draggableProvided={draggableProvided}>
|
||||
<BoardCard item={items[itemKey]} />
|
||||
</BoardItem>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</ItemsContainer>
|
||||
<NewButton onClick={onClickFunctions[column.id]} />
|
||||
</ScrollableColumn>
|
||||
</StyledColumn>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
@ -16,7 +16,7 @@ type Story = StoryObj<typeof Board>;
|
||||
export const OneColumnBoard: Story = {
|
||||
render: () => (
|
||||
<StrictMode>
|
||||
<Board initialBoard={initialBoard} items={items} />
|
||||
<Board columns={initialBoard} initialBoard={initialBoard} items={items} />
|
||||
</StrictMode>
|
||||
),
|
||||
};
|
||||
|
||||
@ -29,7 +29,7 @@ export const useBoard = () => {
|
||||
colorCode: pipelineStage.color,
|
||||
itemKeys:
|
||||
pipelineStage.pipelineProgresses?.map(
|
||||
(item) => item.progressableId as BoardItemKey,
|
||||
(item) => item.id as BoardItemKey,
|
||||
) || [],
|
||||
})) || [];
|
||||
|
||||
@ -44,15 +44,15 @@ export const useBoard = () => {
|
||||
[] as { entityId: string; pipelineProgressId: string }[],
|
||||
);
|
||||
|
||||
const pipelineEntityIdsMapper = (entityId: string) => {
|
||||
const pipelineProgressId = pipelineEntityIds?.find(
|
||||
(item) => item.entityId === entityId,
|
||||
)?.pipelineProgressId;
|
||||
const pipelineProgressableIdsMapper = (pipelineProgressId: string) => {
|
||||
const entityId = pipelineEntityIds?.find(
|
||||
(item) => item.pipelineProgressId === pipelineProgressId,
|
||||
)?.entityId;
|
||||
|
||||
return pipelineProgressId;
|
||||
return entityId;
|
||||
};
|
||||
|
||||
const pipelineEntityType: 'Person' | 'Company' | undefined =
|
||||
const pipelineEntityType =
|
||||
pipelines.data?.findManyPipeline[0].pipelineProgressableType;
|
||||
|
||||
const query =
|
||||
@ -69,7 +69,7 @@ export const useBoard = () => {
|
||||
[entity.id]: entity,
|
||||
});
|
||||
|
||||
const items: Items | undefined = entitiesQueryResult.data
|
||||
const entityItems = entitiesQueryResult.data
|
||||
? isGetCompaniesQuery(entitiesQueryResult.data)
|
||||
? entitiesQueryResult.data.companies.reduce(indexByIdReducer, {} as Items)
|
||||
: isGetPeopleQuery(entitiesQueryResult.data)
|
||||
@ -77,11 +77,20 @@ export const useBoard = () => {
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
const items = pipelineEntityIds?.reduce((acc, item) => {
|
||||
const entityId = pipelineProgressableIdsMapper(item.pipelineProgressId);
|
||||
if (entityId) {
|
||||
acc[item.pipelineProgressId] = entityItems?.[entityId];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Items);
|
||||
|
||||
return {
|
||||
initialBoard,
|
||||
items,
|
||||
loading: pipelines.loading || entitiesQueryResult.loading,
|
||||
error: pipelines.error || entitiesQueryResult.error,
|
||||
pipelineEntityIdsMapper,
|
||||
pipelineId: pipelines.data?.findManyPipeline[0].id,
|
||||
pipelineEntityType,
|
||||
};
|
||||
};
|
||||
|
||||
@ -30,3 +30,23 @@ export const UPDATE_PIPELINE_STAGE = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ADD_ENTITY_TO_PIPELINE = gql`
|
||||
mutation CreateOnePipelineProgress(
|
||||
$entityType: PipelineProgressableType!
|
||||
$entityId: String!
|
||||
$pipelineId: String!
|
||||
$pipelineStageId: String!
|
||||
) {
|
||||
createOnePipelineProgress(
|
||||
data: {
|
||||
progressableType: $entityType
|
||||
progressableId: $entityId
|
||||
pipeline: { connect: { id: $pipelineId } }
|
||||
pipelineStage: { connect: { id: $pipelineStageId } }
|
||||
}
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -2,9 +2,12 @@ import styled from '@emotion/styled';
|
||||
import { DropResult } 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
|
||||
|
||||
export const StyledBoard = styled.div`
|
||||
border-radius: ${({ theme }) => theme.spacing(2)};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type BoardItemKey = string;
|
||||
|
||||
@ -6,8 +6,13 @@ export const StyledColumn = styled.div`
|
||||
background-color: ${({ theme }) => theme.primaryBackground};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 300px;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
width: 300px;
|
||||
`;
|
||||
|
||||
export const ScrollableColumn = styled.div`
|
||||
max-height: calc(100vh - 120px);
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
export const StyledColumnTitle = styled.h3`
|
||||
@ -21,6 +26,10 @@ export const StyledColumnTitle = styled.h3`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledPlaceholder = styled.div`
|
||||
min-height: 1px;
|
||||
`;
|
||||
|
||||
export const StyledItemContainer = styled.div``;
|
||||
|
||||
export const ItemsContainer = ({
|
||||
@ -36,7 +45,7 @@ export const ItemsContainer = ({
|
||||
{...droppableProvided?.droppableProps}
|
||||
>
|
||||
{children}
|
||||
{droppableProvided?.placeholder}
|
||||
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
|
||||
</StyledItemContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
@ -14,17 +15,24 @@ const StyledButton = styled.button`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
padding: ${(props) => props.theme.spacing(1)};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.secondaryBackground};
|
||||
}
|
||||
`;
|
||||
|
||||
export const NewButton = () => {
|
||||
export const NewButton = ({
|
||||
onClick,
|
||||
}: {
|
||||
onClick?: (...args: any[]) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const onInnerClick = useCallback(() => {
|
||||
onClick && onClick({ id: 'twenty-aaffcfbd-f86b-419f-b794-02319abe8637' });
|
||||
}, [onClick]);
|
||||
return (
|
||||
<StyledButton>
|
||||
<StyledButton onClick={onInnerClick}>
|
||||
<IconPlus size={theme.iconSizeMedium} />
|
||||
New
|
||||
</StyledButton>
|
||||
|
||||
Reference in New Issue
Block a user