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:
@ -7,6 +7,7 @@ import {
|
|||||||
IconInbox,
|
IconInbox,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
|
IconTargetArrow,
|
||||||
IconUser,
|
IconUser,
|
||||||
} from '@/ui/icons/index';
|
} from '@/ui/icons/index';
|
||||||
import NavItemsContainer from '@/ui/layout/navbar/NavItemsContainer';
|
import NavItemsContainer from '@/ui/layout/navbar/NavItemsContainer';
|
||||||
@ -57,6 +58,12 @@ export function AppNavbar() {
|
|||||||
icon={<IconBuildingSkyscraper size={theme.iconSizeMedium} />}
|
icon={<IconBuildingSkyscraper size={theme.iconSizeMedium} />}
|
||||||
active={currentPath === '/companies'}
|
active={currentPath === '/companies'}
|
||||||
/>
|
/>
|
||||||
|
<NavItem
|
||||||
|
label="Opportunities"
|
||||||
|
to="/opportunities"
|
||||||
|
icon={<IconTargetArrow size={theme.iconSizeMedium} />}
|
||||||
|
active={currentPath === '/opportunities'}
|
||||||
|
/>
|
||||||
</NavItemsContainer>
|
</NavItemsContainer>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1639,6 +1639,16 @@ export type UpdateOnePipelineProgressMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UpdateOnePipelineProgressMutation = { __typename?: 'Mutation', updateOnePipelineProgress?: { __typename?: 'PipelineProgress', id: string } | null };
|
export type UpdateOnePipelineProgressMutation = { __typename?: 'Mutation', updateOnePipelineProgress?: { __typename?: 'PipelineProgress', id: string } | null };
|
||||||
|
|
||||||
|
export type CreateOnePipelineProgressMutationVariables = Exact<{
|
||||||
|
entityType: PipelineProgressableType;
|
||||||
|
entityId: Scalars['String'];
|
||||||
|
pipelineId: Scalars['String'];
|
||||||
|
pipelineStageId: Scalars['String'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateOnePipelineProgressMutation = { __typename?: 'Mutation', createOnePipelineProgress: { __typename?: 'PipelineProgress', id: string } };
|
||||||
|
|
||||||
export type GetPeopleQueryVariables = Exact<{
|
export type GetPeopleQueryVariables = Exact<{
|
||||||
orderBy?: InputMaybe<Array<PersonOrderByWithRelationInput> | PersonOrderByWithRelationInput>;
|
orderBy?: InputMaybe<Array<PersonOrderByWithRelationInput> | PersonOrderByWithRelationInput>;
|
||||||
where?: InputMaybe<PersonWhereInput>;
|
where?: InputMaybe<PersonWhereInput>;
|
||||||
@ -2292,6 +2302,44 @@ export function useUpdateOnePipelineProgressMutation(baseOptions?: Apollo.Mutati
|
|||||||
export type UpdateOnePipelineProgressMutationHookResult = ReturnType<typeof useUpdateOnePipelineProgressMutation>;
|
export type UpdateOnePipelineProgressMutationHookResult = ReturnType<typeof useUpdateOnePipelineProgressMutation>;
|
||||||
export type UpdateOnePipelineProgressMutationResult = Apollo.MutationResult<UpdateOnePipelineProgressMutation>;
|
export type UpdateOnePipelineProgressMutationResult = Apollo.MutationResult<UpdateOnePipelineProgressMutation>;
|
||||||
export type UpdateOnePipelineProgressMutationOptions = Apollo.BaseMutationOptions<UpdateOnePipelineProgressMutation, UpdateOnePipelineProgressMutationVariables>;
|
export type UpdateOnePipelineProgressMutationOptions = Apollo.BaseMutationOptions<UpdateOnePipelineProgressMutation, UpdateOnePipelineProgressMutationVariables>;
|
||||||
|
export const CreateOnePipelineProgressDocument = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type CreateOnePipelineProgressMutationFn = Apollo.MutationFunction<CreateOnePipelineProgressMutation, CreateOnePipelineProgressMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useCreateOnePipelineProgressMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useCreateOnePipelineProgressMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useCreateOnePipelineProgressMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [createOnePipelineProgressMutation, { data, loading, error }] = useCreateOnePipelineProgressMutation({
|
||||||
|
* variables: {
|
||||||
|
* entityType: // value for 'entityType'
|
||||||
|
* entityId: // value for 'entityId'
|
||||||
|
* pipelineId: // value for 'pipelineId'
|
||||||
|
* pipelineStageId: // value for 'pipelineStageId'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useCreateOnePipelineProgressMutation(baseOptions?: Apollo.MutationHookOptions<CreateOnePipelineProgressMutation, CreateOnePipelineProgressMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<CreateOnePipelineProgressMutation, CreateOnePipelineProgressMutationVariables>(CreateOnePipelineProgressDocument, options);
|
||||||
|
}
|
||||||
|
export type CreateOnePipelineProgressMutationHookResult = ReturnType<typeof useCreateOnePipelineProgressMutation>;
|
||||||
|
export type CreateOnePipelineProgressMutationResult = Apollo.MutationResult<CreateOnePipelineProgressMutation>;
|
||||||
|
export type CreateOnePipelineProgressMutationOptions = Apollo.BaseMutationOptions<CreateOnePipelineProgressMutation, CreateOnePipelineProgressMutationVariables>;
|
||||||
export const GetPeopleDocument = gql`
|
export const GetPeopleDocument = gql`
|
||||||
query GetPeople($orderBy: [PersonOrderByWithRelationInput!], $where: PersonWhereInput, $limit: Int) {
|
query GetPeople($orderBy: [PersonOrderByWithRelationInput!], $where: PersonWhereInput, $limit: Int) {
|
||||||
people: findManyPerson(orderBy: $orderBy, where: $where, take: $limit) {
|
people: findManyPerson(orderBy: $orderBy, where: $where, take: $limit) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
DragDropContext,
|
DragDropContext,
|
||||||
Draggable,
|
Draggable,
|
||||||
@ -10,11 +10,13 @@ import {
|
|||||||
BoardItemKey,
|
BoardItemKey,
|
||||||
Column,
|
Column,
|
||||||
getOptimisticlyUpdatedBoard,
|
getOptimisticlyUpdatedBoard,
|
||||||
|
Item,
|
||||||
Items,
|
Items,
|
||||||
StyledBoard,
|
StyledBoard,
|
||||||
} from '../../ui/components/board/Board';
|
} from '../../ui/components/board/Board';
|
||||||
import {
|
import {
|
||||||
ItemsContainer,
|
ItemsContainer,
|
||||||
|
ScrollableColumn,
|
||||||
StyledColumn,
|
StyledColumn,
|
||||||
StyledColumnTitle,
|
StyledColumnTitle,
|
||||||
} from '../../ui/components/board/BoardColumn';
|
} from '../../ui/components/board/BoardColumn';
|
||||||
@ -24,21 +26,43 @@ import { NewButton } from '../../ui/components/board/BoardNewButton';
|
|||||||
import { BoardCard } from './BoardCard';
|
import { BoardCard } from './BoardCard';
|
||||||
|
|
||||||
type BoardProps = {
|
type BoardProps = {
|
||||||
|
columns: Omit<Column, 'itemKeys'>[];
|
||||||
initialBoard: Column[];
|
initialBoard: Column[];
|
||||||
items: Items;
|
items: Items;
|
||||||
onUpdate?: (itemKey: BoardItemKey, columnId: Column['id']) => Promise<void>;
|
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 [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(
|
const onDragEnd: OnDragEndResponder = useCallback(
|
||||||
async (result) => {
|
async (result) => {
|
||||||
const newBoard = getOptimisticlyUpdatedBoard(board, result);
|
const newBoard = getOptimisticlyUpdatedBoard(board, result);
|
||||||
if (!newBoard) return;
|
if (!newBoard) return;
|
||||||
setBoard(newBoard);
|
setBoard(newBoard);
|
||||||
try {
|
try {
|
||||||
const draggedEntityId = items[result.draggableId]?.id;
|
const draggedEntityId = result.draggableId;
|
||||||
const destinationColumnId = result.destination?.droppableId;
|
const destinationColumnId = result.destination?.droppableId;
|
||||||
draggedEntityId &&
|
draggedEntityId &&
|
||||||
destinationColumnId &&
|
destinationColumnId &&
|
||||||
@ -48,35 +72,38 @@ export const Board = ({ initialBoard, items, onUpdate }: BoardProps) => {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[board, onUpdate, items],
|
[board, onUpdate],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('board', board);
|
||||||
return (
|
return (
|
||||||
<StyledBoard>
|
<StyledBoard>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
{board.map((column) => (
|
{columns.map((column, columnIndex) => (
|
||||||
<Droppable key={column.id} droppableId={column.id}>
|
<Droppable key={column.id} droppableId={column.id}>
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
<StyledColumn>
|
<StyledColumn>
|
||||||
<StyledColumnTitle color={column.colorCode}>
|
<StyledColumnTitle color={column.colorCode}>
|
||||||
• {column.title}
|
• {column.title}
|
||||||
</StyledColumnTitle>
|
</StyledColumnTitle>
|
||||||
<ItemsContainer droppableProvided={droppableProvided}>
|
<ScrollableColumn>
|
||||||
{column.itemKeys.map((itemKey, index) => (
|
<ItemsContainer droppableProvided={droppableProvided}>
|
||||||
<Draggable
|
{board[columnIndex].itemKeys.map((itemKey, index) => (
|
||||||
key={itemKey}
|
<Draggable
|
||||||
draggableId={itemKey}
|
key={itemKey}
|
||||||
index={index}
|
draggableId={itemKey}
|
||||||
>
|
index={index}
|
||||||
{(draggableProvided) => (
|
>
|
||||||
<BoardItem draggableProvided={draggableProvided}>
|
{(draggableProvided) => (
|
||||||
<BoardCard item={items[itemKey]} />
|
<BoardItem draggableProvided={draggableProvided}>
|
||||||
</BoardItem>
|
<BoardCard item={items[itemKey]} />
|
||||||
)}
|
</BoardItem>
|
||||||
</Draggable>
|
)}
|
||||||
))}
|
</Draggable>
|
||||||
</ItemsContainer>
|
))}
|
||||||
<NewButton />
|
</ItemsContainer>
|
||||||
|
<NewButton onClick={onClickFunctions[column.id]} />
|
||||||
|
</ScrollableColumn>
|
||||||
</StyledColumn>
|
</StyledColumn>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
|
|||||||
@ -16,7 +16,7 @@ type Story = StoryObj<typeof Board>;
|
|||||||
export const OneColumnBoard: Story = {
|
export const OneColumnBoard: Story = {
|
||||||
render: () => (
|
render: () => (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Board initialBoard={initialBoard} items={items} />
|
<Board columns={initialBoard} initialBoard={initialBoard} items={items} />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const useBoard = () => {
|
|||||||
colorCode: pipelineStage.color,
|
colorCode: pipelineStage.color,
|
||||||
itemKeys:
|
itemKeys:
|
||||||
pipelineStage.pipelineProgresses?.map(
|
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 }[],
|
[] as { entityId: string; pipelineProgressId: string }[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const pipelineEntityIdsMapper = (entityId: string) => {
|
const pipelineProgressableIdsMapper = (pipelineProgressId: string) => {
|
||||||
const pipelineProgressId = pipelineEntityIds?.find(
|
const entityId = pipelineEntityIds?.find(
|
||||||
(item) => item.entityId === entityId,
|
(item) => item.pipelineProgressId === pipelineProgressId,
|
||||||
)?.pipelineProgressId;
|
)?.entityId;
|
||||||
|
|
||||||
return pipelineProgressId;
|
return entityId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pipelineEntityType: 'Person' | 'Company' | undefined =
|
const pipelineEntityType =
|
||||||
pipelines.data?.findManyPipeline[0].pipelineProgressableType;
|
pipelines.data?.findManyPipeline[0].pipelineProgressableType;
|
||||||
|
|
||||||
const query =
|
const query =
|
||||||
@ -69,7 +69,7 @@ export const useBoard = () => {
|
|||||||
[entity.id]: entity,
|
[entity.id]: entity,
|
||||||
});
|
});
|
||||||
|
|
||||||
const items: Items | undefined = entitiesQueryResult.data
|
const entityItems = entitiesQueryResult.data
|
||||||
? isGetCompaniesQuery(entitiesQueryResult.data)
|
? isGetCompaniesQuery(entitiesQueryResult.data)
|
||||||
? entitiesQueryResult.data.companies.reduce(indexByIdReducer, {} as Items)
|
? entitiesQueryResult.data.companies.reduce(indexByIdReducer, {} as Items)
|
||||||
: isGetPeopleQuery(entitiesQueryResult.data)
|
: isGetPeopleQuery(entitiesQueryResult.data)
|
||||||
@ -77,11 +77,20 @@ export const useBoard = () => {
|
|||||||
: undefined
|
: undefined
|
||||||
: 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 {
|
return {
|
||||||
initialBoard,
|
initialBoard,
|
||||||
items,
|
items,
|
||||||
loading: pipelines.loading || entitiesQueryResult.loading,
|
loading: pipelines.loading || entitiesQueryResult.loading,
|
||||||
error: pipelines.error || entitiesQueryResult.error,
|
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
|
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`
|
export const StyledBoard = styled.div`
|
||||||
|
border-radius: ${({ theme }) => theme.spacing(2)};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type BoardItemKey = string;
|
export type BoardItemKey = string;
|
||||||
|
|||||||
@ -6,8 +6,13 @@ export const StyledColumn = styled.div`
|
|||||||
background-color: ${({ theme }) => theme.primaryBackground};
|
background-color: ${({ theme }) => theme.primaryBackground};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-width: 300px;
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
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`
|
export const StyledColumnTitle = styled.h3`
|
||||||
@ -21,6 +26,10 @@ export const StyledColumnTitle = styled.h3`
|
|||||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledPlaceholder = styled.div`
|
||||||
|
min-height: 1px;
|
||||||
|
`;
|
||||||
|
|
||||||
export const StyledItemContainer = styled.div``;
|
export const StyledItemContainer = styled.div``;
|
||||||
|
|
||||||
export const ItemsContainer = ({
|
export const ItemsContainer = ({
|
||||||
@ -36,7 +45,7 @@ export const ItemsContainer = ({
|
|||||||
{...droppableProvided?.droppableProps}
|
{...droppableProvided?.droppableProps}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{droppableProvided?.placeholder}
|
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
|
||||||
</StyledItemContainer>
|
</StyledItemContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
@ -14,17 +15,24 @@ const StyledButton = styled.button`
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: background-color 0.2s ease-in-out;
|
padding: ${(props) => props.theme.spacing(1)};
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${({ theme }) => theme.secondaryBackground};
|
background-color: ${({ theme }) => theme.secondaryBackground};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const NewButton = () => {
|
export const NewButton = ({
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
onClick?: (...args: any[]) => void;
|
||||||
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const onInnerClick = useCallback(() => {
|
||||||
|
onClick && onClick({ id: 'twenty-aaffcfbd-f86b-419f-b794-02319abe8637' });
|
||||||
|
}, [onClick]);
|
||||||
return (
|
return (
|
||||||
<StyledButton>
|
<StyledButton onClick={onInnerClick}>
|
||||||
<IconPlus size={theme.iconSizeMedium} />
|
<IconPlus size={theme.iconSizeMedium} />
|
||||||
New
|
New
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
import { IconTargetArrow } from '@/ui/icons/index';
|
import { IconTargetArrow } from '@/ui/icons/index';
|
||||||
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
|
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
|
||||||
@ -6,36 +8,79 @@ import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer'
|
|||||||
import {
|
import {
|
||||||
PipelineProgress,
|
PipelineProgress,
|
||||||
PipelineStage,
|
PipelineStage,
|
||||||
|
useCreateOnePipelineProgressMutation,
|
||||||
useUpdateOnePipelineProgressMutation,
|
useUpdateOnePipelineProgressMutation,
|
||||||
} from '../../generated/graphql';
|
} from '../../generated/graphql';
|
||||||
import { Board } from '../../modules/opportunities/components/Board';
|
import { Board } from '../../modules/opportunities/components/Board';
|
||||||
import { useBoard } from '../../modules/opportunities/hooks/useBoard';
|
import { useBoard } from '../../modules/opportunities/hooks/useBoard';
|
||||||
|
import { GET_PIPELINES } from '../../modules/opportunities/queries';
|
||||||
|
|
||||||
export function Opportunities() {
|
export function Opportunities() {
|
||||||
const { initialBoard, items, loading, error, pipelineEntityIdsMapper } =
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { initialBoard, items, error, pipelineId, pipelineEntityType } =
|
||||||
useBoard();
|
useBoard();
|
||||||
|
const columns = useMemo(
|
||||||
|
() =>
|
||||||
|
initialBoard?.map(({ id, colorCode, title }) => ({
|
||||||
|
id,
|
||||||
|
colorCode,
|
||||||
|
title,
|
||||||
|
})),
|
||||||
|
[initialBoard],
|
||||||
|
);
|
||||||
const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation();
|
const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation();
|
||||||
|
const [createPipelineProgress] = useCreateOnePipelineProgressMutation();
|
||||||
|
|
||||||
const onUpdate = useCallback(
|
const onUpdate = useCallback(
|
||||||
async (
|
async (
|
||||||
entityId: NonNullable<PipelineProgress['progressableId']>,
|
pipelineProgressId: NonNullable<PipelineProgress['id']>,
|
||||||
pipelineStageId: NonNullable<PipelineStage['id']>,
|
pipelineStageId: NonNullable<PipelineStage['id']>,
|
||||||
) => {
|
) => {
|
||||||
const pipelineProgressId = pipelineEntityIdsMapper(entityId);
|
|
||||||
updatePipelineProgress({
|
updatePipelineProgress({
|
||||||
variables: { id: pipelineProgressId, pipelineStageId },
|
variables: { id: pipelineProgressId, pipelineStageId },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[updatePipelineProgress, pipelineEntityIdsMapper],
|
[updatePipelineProgress],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickNew = useCallback(
|
||||||
|
(
|
||||||
|
columnId: PipelineStage['id'],
|
||||||
|
newItem: Partial<PipelineProgress> & { id: string },
|
||||||
|
) => {
|
||||||
|
if (!pipelineId || !pipelineEntityType) return;
|
||||||
|
const variables = {
|
||||||
|
pipelineStageId: columnId,
|
||||||
|
pipelineId,
|
||||||
|
entityId: newItem.id,
|
||||||
|
entityType: pipelineEntityType,
|
||||||
|
};
|
||||||
|
createPipelineProgress({
|
||||||
|
variables,
|
||||||
|
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[pipelineId, pipelineEntityType, createPipelineProgress],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading) return <div>Loading...</div>;
|
|
||||||
if (error) return <div>Error...</div>;
|
if (error) return <div>Error...</div>;
|
||||||
if (!initialBoard || !items)
|
if (!initialBoard || !items) {
|
||||||
return <div>Initial board or items not found</div>;
|
return <div>Initial board or items not found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithTopBarContainer title="Opportunities" icon={<IconTargetArrow />}>
|
<WithTopBarContainer
|
||||||
<Board initialBoard={initialBoard} items={items} onUpdate={onUpdate} />
|
title="Opportunities"
|
||||||
|
icon={<IconTargetArrow size={theme.iconSizeMedium} />}
|
||||||
|
>
|
||||||
|
<Board
|
||||||
|
columns={columns || []}
|
||||||
|
initialBoard={initialBoard}
|
||||||
|
items={items}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
onClickNew={onClickNew}
|
||||||
|
/>
|
||||||
</WithTopBarContainer>
|
</WithTopBarContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export const seedPipelines = async (prisma: PrismaClient) => {
|
|||||||
name: 'Sales pipeline',
|
name: 'Sales pipeline',
|
||||||
icon: '💰',
|
icon: '💰',
|
||||||
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
|
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
|
||||||
pipelineProgressableType: 'Person',
|
pipelineProgressableType: 'Company',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -84,8 +84,47 @@ export const seedPipelines = async (prisma: PrismaClient) => {
|
|||||||
id: 'twenty-fe256b39-3ec3-4fe7-8998-b76aa0bfb600',
|
id: 'twenty-fe256b39-3ec3-4fe7-8998-b76aa0bfb600',
|
||||||
pipelineId: 'twenty-fe256b39-3ec3-4fe3-8997-b75aa0bfb400',
|
pipelineId: 'twenty-fe256b39-3ec3-4fe3-8997-b75aa0bfb400',
|
||||||
pipelineStageId: 'twenty-fe256b39-3ec3-4fe3-8998-b76aa0bfb600',
|
pipelineStageId: 'twenty-fe256b39-3ec3-4fe3-8998-b76aa0bfb600',
|
||||||
progressableType: 'Person',
|
progressableType: 'Company',
|
||||||
progressableId: 'twenty-755035db-623d-41fe-92e7-dd45b7c568e1',
|
progressableId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
|
||||||
|
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.pipelineProgress.upsert({
|
||||||
|
where: { id: 'twenty-4a886c90-f4f2-4984-8222-882ebbb905d6' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: 'twenty-4a886c90-f4f2-4984-8222-882ebbb905d6',
|
||||||
|
pipelineId: 'twenty-fe256b39-3ec3-4fe3-8997-b75aa0bfb400',
|
||||||
|
pipelineStageId: 'twenty-fe256b39-3ec3-4fe4-8998-b76aa0bfb600',
|
||||||
|
progressableType: 'Company',
|
||||||
|
progressableId: 'twenty-118995f3-5d81-46d6-bf83-f7fd33ea6102',
|
||||||
|
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.pipelineProgress.upsert({
|
||||||
|
where: { id: 'twenty-af92f3eb-d51d-4528-9b97-b8f132865b00' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: 'twenty-af92f3eb-d51d-4528-9b97-b8f132865b00',
|
||||||
|
pipelineId: 'twenty-fe256b39-3ec3-4fe3-8997-b75aa0bfb400',
|
||||||
|
pipelineStageId: 'twenty-fe256b39-3ec3-4fe5-8998-b76aa0bfb600',
|
||||||
|
progressableType: 'Company',
|
||||||
|
progressableId: 'twenty-04b2e9f5-0713-40a5-8216-82802401d33e',
|
||||||
|
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.pipelineProgress.upsert({
|
||||||
|
where: { id: 'twenty-08369b1a-acdb-43d6-95f9-67ac7436941a' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: 'twenty-08369b1a-acdb-43d6-95f9-67ac7436941a',
|
||||||
|
pipelineId: 'twenty-fe256b39-3ec3-4fe3-8997-b75aa0bfb400',
|
||||||
|
pipelineStageId: 'twenty-fe256b39-3ec3-4fe5-8998-b76aa0bfb600',
|
||||||
|
progressableType: 'Company',
|
||||||
|
progressableId: 'twenty-460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||||
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
|
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user