mirror of
https://github.com/lingble/twenty.git
synced 2025-11-03 06:07:56 +00:00
Add ability to associate a new company to pipeline (#350)
* Add ability to associate a new company to pipeline * Fix tests
This commit is contained in:
@@ -1626,7 +1626,9 @@ export type DeleteCompaniesMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type DeleteCompaniesMutation = { __typename?: 'Mutation', deleteManyCompany: { __typename?: 'AffectedRows', count: number } };
|
export type DeleteCompaniesMutation = { __typename?: 'Mutation', deleteManyCompany: { __typename?: 'AffectedRows', count: number } };
|
||||||
|
|
||||||
export type GetPipelinesQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetPipelinesQueryVariables = Exact<{
|
||||||
|
where?: InputMaybe<PipelineWhereInput>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
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, pipelineProgresses?: Array<{ __typename?: 'PipelineProgress', id: string, progressableType: PipelineProgressableType, progressableId: string }> | 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, pipelineProgresses?: Array<{ __typename?: 'PipelineProgress', id: string, progressableType: PipelineProgressableType, progressableId: string }> | null }> | null }> };
|
||||||
@@ -1733,7 +1735,7 @@ export type GetCurrentUserQuery = { __typename?: 'Query', users: Array<{ __typen
|
|||||||
export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetUsersQuery = { __typename?: 'Query', findManyUser: Array<{ __typename?: 'User', id: string }> };
|
export type GetUsersQuery = { __typename?: 'Query', findManyUser: Array<{ __typename?: 'User', id: string, email: string, displayName: string }> };
|
||||||
|
|
||||||
|
|
||||||
export const CreateCommentDocument = gql`
|
export const CreateCommentDocument = gql`
|
||||||
@@ -2220,8 +2222,8 @@ export type DeleteCompaniesMutationHookResult = ReturnType<typeof useDeleteCompa
|
|||||||
export type DeleteCompaniesMutationResult = Apollo.MutationResult<DeleteCompaniesMutation>;
|
export type DeleteCompaniesMutationResult = Apollo.MutationResult<DeleteCompaniesMutation>;
|
||||||
export type DeleteCompaniesMutationOptions = Apollo.BaseMutationOptions<DeleteCompaniesMutation, DeleteCompaniesMutationVariables>;
|
export type DeleteCompaniesMutationOptions = Apollo.BaseMutationOptions<DeleteCompaniesMutation, DeleteCompaniesMutationVariables>;
|
||||||
export const GetPipelinesDocument = gql`
|
export const GetPipelinesDocument = gql`
|
||||||
query GetPipelines {
|
query GetPipelines($where: PipelineWhereInput) {
|
||||||
findManyPipeline {
|
findManyPipeline(where: $where) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
pipelineProgressableType
|
pipelineProgressableType
|
||||||
@@ -2251,6 +2253,7 @@ export const GetPipelinesDocument = gql`
|
|||||||
* @example
|
* @example
|
||||||
* const { data, loading, error } = useGetPipelinesQuery({
|
* const { data, loading, error } = useGetPipelinesQuery({
|
||||||
* variables: {
|
* variables: {
|
||||||
|
* where: // value for 'where'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
@@ -2729,9 +2732,11 @@ export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQ
|
|||||||
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
|
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
|
||||||
export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>;
|
export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>;
|
||||||
export const GetUsersDocument = gql`
|
export const GetUsersDocument = gql`
|
||||||
query getUsers {
|
query GetUsers {
|
||||||
findManyUser {
|
findManyUser {
|
||||||
id
|
id
|
||||||
|
email
|
||||||
|
displayName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,60 +1,90 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
import {
|
import {
|
||||||
DragDropContext,
|
DragDropContext,
|
||||||
Draggable,
|
Draggable,
|
||||||
Droppable,
|
Droppable,
|
||||||
|
DroppableProvided,
|
||||||
OnDragEndResponder,
|
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
|
} 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 {
|
import {
|
||||||
BoardItemKey,
|
|
||||||
Column,
|
Column,
|
||||||
getOptimisticlyUpdatedBoard,
|
getOptimisticlyUpdatedBoard,
|
||||||
Item,
|
|
||||||
Items,
|
|
||||||
StyledBoard,
|
StyledBoard,
|
||||||
} from '../../ui/components/board/Board';
|
} from '../../ui/components/board/Board';
|
||||||
import {
|
import { boardColumnsState } from '../states/boardColumnsState';
|
||||||
ItemsContainer,
|
import { boardItemsState } from '../states/boardItemsState';
|
||||||
ScrollableColumn,
|
|
||||||
StyledColumn,
|
|
||||||
StyledColumnTitle,
|
|
||||||
} from '../../ui/components/board/BoardColumn';
|
|
||||||
import { BoardItem } from '../../ui/components/board/BoardItem';
|
|
||||||
import { NewButton } from '../../ui/components/board/BoardNewButton';
|
|
||||||
|
|
||||||
import { BoardCard } from './BoardCard';
|
import { CompanyBoardCard } from './CompanyBoardCard';
|
||||||
|
import { NewButton } from './NewButton';
|
||||||
|
|
||||||
type BoardProps = {
|
export type CompanyProgress = Pick<
|
||||||
columns: Omit<Column, 'itemKeys'>[];
|
Company,
|
||||||
initialBoard: Column[];
|
'id' | 'name' | 'domainName' | 'createdAt'
|
||||||
items: Items;
|
>;
|
||||||
onUpdate?: (itemKey: BoardItemKey, columnId: Column['id']) => Promise<void>;
|
export type CompanyProgressDict = {
|
||||||
onClickNew?: (
|
[key: string]: CompanyProgress;
|
||||||
columnId: Column['id'],
|
|
||||||
newItem: Partial<Item> & { id: string },
|
|
||||||
) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Board = ({
|
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,
|
columns,
|
||||||
initialBoard,
|
initialBoard,
|
||||||
items,
|
initialItems,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onClickNew,
|
pipelineId,
|
||||||
}: BoardProps) => {
|
}: BoardProps) {
|
||||||
const [board, setBoard] = useState<Column[]>(initialBoard);
|
const [board, setBoard] = useRecoilState(boardColumnsState);
|
||||||
|
const [items, setItems] = useRecoilState(boardItemsState);
|
||||||
|
const [isInitialBoardLoaded, setIsInitialBoardLoaded] = useState(false);
|
||||||
|
|
||||||
const onClickFunctions = useMemo<
|
useEffect(() => {
|
||||||
Record<Column['id'], (newItem: Partial<Item> & { id: string }) => void>
|
if (Object.keys(initialItems).length === 0 || isInitialBoardLoaded) return;
|
||||||
>(() => {
|
setBoard(initialBoard);
|
||||||
return board.reduce((acc, column) => {
|
setItems(initialItems);
|
||||||
acc[column.id] = (newItem: Partial<Item> & { id: string }) => {
|
setIsInitialBoardLoaded(true);
|
||||||
onClickNew && onClickNew(column.id, newItem);
|
}, [
|
||||||
};
|
initialBoard,
|
||||||
return acc;
|
setBoard,
|
||||||
}, {} as Record<Column['id'], (newItem: Partial<Item> & { id: string }) => void>);
|
initialItems,
|
||||||
}, [board, onClickNew]);
|
setItems,
|
||||||
|
setIsInitialBoardLoaded,
|
||||||
|
isInitialBoardLoaded,
|
||||||
|
]);
|
||||||
|
|
||||||
const onDragEnd: OnDragEndResponder = useCallback(
|
const onDragEnd: OnDragEndResponder = useCallback(
|
||||||
async (result) => {
|
async (result) => {
|
||||||
@@ -72,42 +102,48 @@ export const Board = ({
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[board, onUpdate],
|
[board, onUpdate, setBoard],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return board.length > 0 ? (
|
||||||
<StyledBoard>
|
<StyledBoard>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
{columns.map((column, columnIndex) => (
|
{columns.map((column, columnIndex) => (
|
||||||
<Droppable key={column.id} droppableId={column.id}>
|
<Droppable key={column.id} droppableId={column.id}>
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
<StyledColumn>
|
<BoardColumn title={column.title} colorCode={column.colorCode}>
|
||||||
<StyledColumnTitle color={column.colorCode}>
|
<BoardColumnCardsContainer
|
||||||
• {column.title}
|
droppableProvided={droppableProvided}
|
||||||
</StyledColumnTitle>
|
>
|
||||||
<ScrollableColumn>
|
{board[columnIndex].itemKeys.map(
|
||||||
<ItemsContainer droppableProvided={droppableProvided}>
|
(itemKey, index) =>
|
||||||
{board[columnIndex].itemKeys.map((itemKey, index) => (
|
items[itemKey] && (
|
||||||
<Draggable
|
<Draggable
|
||||||
key={itemKey}
|
key={itemKey}
|
||||||
draggableId={itemKey}
|
draggableId={itemKey}
|
||||||
index={index}
|
index={index}
|
||||||
>
|
>
|
||||||
{(draggableProvided) => (
|
{(draggableProvided) => (
|
||||||
<BoardItem draggableProvided={draggableProvided}>
|
<div
|
||||||
<BoardCard item={items[itemKey]} />
|
ref={draggableProvided?.innerRef}
|
||||||
</BoardItem>
|
{...draggableProvided?.dragHandleProps}
|
||||||
)}
|
{...draggableProvided?.draggableProps}
|
||||||
</Draggable>
|
>
|
||||||
))}
|
<CompanyBoardCard company={items[itemKey]} />
|
||||||
</ItemsContainer>
|
</div>
|
||||||
<NewButton onClick={onClickFunctions[column.id]} />
|
)}
|
||||||
</ScrollableColumn>
|
</Draggable>
|
||||||
</StyledColumn>
|
),
|
||||||
|
)}
|
||||||
|
</BoardColumnCardsContainer>
|
||||||
|
<NewButton pipelineId={pipelineId} columnId={column.id} />
|
||||||
|
</BoardColumn>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
))}
|
))}
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</StyledBoard>
|
</StyledBoard>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { Company, Person } from '../../../generated/graphql';
|
|
||||||
import CompanyChip from '../../companies/components/CompanyChip';
|
|
||||||
import PersonPlaceholder from '../../people/components/person-placeholder.png';
|
|
||||||
import { PersonChip } from '../../people/components/PersonChip';
|
|
||||||
import {
|
|
||||||
IconBuildingSkyscraper,
|
|
||||||
IconCalendarEvent,
|
|
||||||
IconMail,
|
|
||||||
IconPhone,
|
|
||||||
IconUser,
|
|
||||||
IconUsers,
|
|
||||||
} from '../../ui/icons';
|
|
||||||
import { getLogoUrlFromDomainName, humanReadableDate } from '../../utils/utils';
|
|
||||||
|
|
||||||
const StyledBoardCard = styled.div`
|
|
||||||
color: ${(props) => props.theme.text80};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledBoardCardHeader = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
font-weight: ${(props) => props.theme.fontWeightBold};
|
|
||||||
height: 24px;
|
|
||||||
padding: ${(props) => props.theme.spacing(2)};
|
|
||||||
img {
|
|
||||||
height: ${(props) => props.theme.iconSizeMedium}px;
|
|
||||||
margin-right: ${(props) => props.theme.spacing(2)};
|
|
||||||
object-fit: cover;
|
|
||||||
width: ${(props) => props.theme.iconSizeMedium}px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const StyledBoardCardBody = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: ${(props) => props.theme.spacing(2)};
|
|
||||||
padding: ${(props) => props.theme.spacing(2)};
|
|
||||||
span {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
svg {
|
|
||||||
color: ${(props) => props.theme.text40};
|
|
||||||
margin-right: ${(props) => props.theme.spacing(2)};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const BoardCard = ({ item }: { item: Person | Company }) => {
|
|
||||||
if (item?.__typename === 'Person') return <PersonBoardCard person={item} />;
|
|
||||||
if (item?.__typename === 'Company')
|
|
||||||
return <CompanyBoardCard company={item} />;
|
|
||||||
// @todo return card skeleton
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PersonBoardCard = ({ person }: { person: Person }) => {
|
|
||||||
const fullname = `${person.firstname} ${person.lastname}`;
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<StyledBoardCard>
|
|
||||||
<StyledBoardCardHeader>
|
|
||||||
<img
|
|
||||||
data-testid="person-chip-image"
|
|
||||||
src={PersonPlaceholder.toString()}
|
|
||||||
alt="person"
|
|
||||||
/>
|
|
||||||
{fullname}
|
|
||||||
</StyledBoardCardHeader>
|
|
||||||
<StyledBoardCardBody>
|
|
||||||
<span>
|
|
||||||
<IconBuildingSkyscraper size={theme.iconSizeMedium} />
|
|
||||||
<CompanyChip
|
|
||||||
name={person.company?.name || ''}
|
|
||||||
picture={getLogoUrlFromDomainName(
|
|
||||||
person.company?.domainName,
|
|
||||||
).toString()}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<IconMail size={theme.iconSizeMedium} />
|
|
||||||
{person.email}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<IconPhone size={theme.iconSizeMedium} />
|
|
||||||
{person.phone}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<IconCalendarEvent size={theme.iconSizeMedium} />
|
|
||||||
{humanReadableDate(new Date(person.createdAt as string))}
|
|
||||||
</span>
|
|
||||||
</StyledBoardCardBody>
|
|
||||||
</StyledBoardCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CompanyBoardCard = ({ company }: { company: Company }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<StyledBoardCard>
|
|
||||||
<StyledBoardCardHeader>
|
|
||||||
<img
|
|
||||||
src={getLogoUrlFromDomainName(company.domainName).toString()}
|
|
||||||
alt={`${company.name}-company-logo`}
|
|
||||||
/>
|
|
||||||
<span>{company.name}</span>
|
|
||||||
</StyledBoardCardHeader>
|
|
||||||
<StyledBoardCardBody>
|
|
||||||
<span>
|
|
||||||
<IconUser size={theme.iconSizeMedium} />
|
|
||||||
<PersonChip name={company.accountOwner?.displayName || ''} />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<IconUsers size={theme.iconSizeMedium} /> {company.employees}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<IconCalendarEvent size={theme.iconSizeMedium} />
|
|
||||||
{humanReadableDate(new Date(company.createdAt as string))}
|
|
||||||
</span>
|
|
||||||
</StyledBoardCardBody>
|
|
||||||
</StyledBoardCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { Company } from '../../../generated/graphql';
|
||||||
|
import { PersonChip } from '../../people/components/PersonChip';
|
||||||
|
import { IconCalendarEvent, IconUser, IconUsers } from '../../ui/icons';
|
||||||
|
import { getLogoUrlFromDomainName, humanReadableDate } from '../../utils/utils';
|
||||||
|
|
||||||
|
const StyledBoardCard = styled.div`
|
||||||
|
background: ${({ theme }) => theme.secondaryBackground};
|
||||||
|
border: 1px solid ${({ theme }) => theme.mediumBorder};
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: ${({ theme }) => theme.lightBoxShadow};
|
||||||
|
color: ${({ theme }) => theme.text80};
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledBoardCardWrapper = styled.div`
|
||||||
|
padding-bottom: ${(props) => props.theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledBoardCardHeader = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
font-weight: ${(props) => props.theme.fontWeightBold};
|
||||||
|
height: 24px;
|
||||||
|
padding-left: ${(props) => props.theme.spacing(2)};
|
||||||
|
padding-right: ${(props) => props.theme.spacing(2)};
|
||||||
|
padding-top: ${(props) => props.theme.spacing(2)};
|
||||||
|
img {
|
||||||
|
height: ${(props) => props.theme.iconSizeMedium}px;
|
||||||
|
margin-right: ${(props) => props.theme.spacing(2)};
|
||||||
|
object-fit: cover;
|
||||||
|
width: ${(props) => props.theme.iconSizeMedium}px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const StyledBoardCardBody = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${(props) => props.theme.spacing(2)};
|
||||||
|
padding: ${(props) => props.theme.spacing(2)};
|
||||||
|
span {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
svg {
|
||||||
|
color: ${(props) => props.theme.text40};
|
||||||
|
margin-right: ${(props) => props.theme.spacing(2)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type CompanyProp = Pick<
|
||||||
|
Company,
|
||||||
|
'id' | 'name' | 'domainName' | 'employees' | 'createdAt' | 'accountOwner'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function CompanyBoardCard({ company }: { company: CompanyProp }) {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<StyledBoardCardWrapper>
|
||||||
|
<StyledBoardCard>
|
||||||
|
<StyledBoardCardHeader>
|
||||||
|
<img
|
||||||
|
src={getLogoUrlFromDomainName(company.domainName).toString()}
|
||||||
|
alt={`${company.name}-company-logo`}
|
||||||
|
/>
|
||||||
|
<span>{company.name}</span>
|
||||||
|
</StyledBoardCardHeader>
|
||||||
|
<StyledBoardCardBody>
|
||||||
|
<span>
|
||||||
|
<IconUser size={theme.iconSizeMedium} />
|
||||||
|
<PersonChip name={company.accountOwner?.displayName || ''} />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<IconUsers size={theme.iconSizeMedium} /> {company.employees}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<IconCalendarEvent size={theme.iconSizeMedium} />
|
||||||
|
{humanReadableDate(new Date(company.createdAt as string))}
|
||||||
|
</span>
|
||||||
|
</StyledBoardCardBody>
|
||||||
|
</StyledBoardCard>
|
||||||
|
</StyledBoardCardWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
front/src/modules/opportunities/components/NewButton.tsx
Normal file
82
front/src/modules/opportunities/components/NewButton.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { Column } from '@/ui/components/board/Board';
|
||||||
|
import { NewButton as UINewButton } from '@/ui/components/board/NewButton';
|
||||||
|
import { RecoilScope } from '@/ui/hooks/RecoilScope';
|
||||||
|
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 [items, setItems] = 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);
|
||||||
|
setItems({
|
||||||
|
...items,
|
||||||
|
[newUuid]: {
|
||||||
|
id: company.id,
|
||||||
|
name: company.name,
|
||||||
|
domainName: company.domainName,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setBoard(newBoard);
|
||||||
|
await createOnePipelineProgress({
|
||||||
|
variables: {
|
||||||
|
pipelineStageId: columnId,
|
||||||
|
pipelineId,
|
||||||
|
entityId: company.id,
|
||||||
|
entityType: PipelineProgressableType.Company,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
createOnePipelineProgress,
|
||||||
|
columnId,
|
||||||
|
pipelineId,
|
||||||
|
board,
|
||||||
|
setBoard,
|
||||||
|
items,
|
||||||
|
setItems,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNewClick = useCallback(() => {
|
||||||
|
setIsCreatingCard(true);
|
||||||
|
}, [setIsCreatingCard]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isCreatingCard && (
|
||||||
|
<RecoilScope>
|
||||||
|
<NewCompanyBoardCard onEntitySelect={onEntitySelect} />
|
||||||
|
</RecoilScope>
|
||||||
|
)}
|
||||||
|
<UINewButton onClick={onNewClick} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
|
||||||
|
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
|
||||||
|
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||||
|
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
|
||||||
|
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],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { StrictMode } from 'react';
|
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||||
|
|
||||||
import { Board } from '../Board';
|
import { Board } from '../Board';
|
||||||
|
|
||||||
import { initialBoard, items } from './mock-data';
|
import { initialBoard, items } from './mock-data';
|
||||||
@@ -14,9 +15,12 @@ export default meta;
|
|||||||
type Story = StoryObj<typeof Board>;
|
type Story = StoryObj<typeof Board>;
|
||||||
|
|
||||||
export const OneColumnBoard: Story = {
|
export const OneColumnBoard: Story = {
|
||||||
render: () => (
|
render: getRenderWrapperForComponent(
|
||||||
<StrictMode>
|
<Board
|
||||||
<Board columns={initialBoard} initialBoard={initialBoard} items={items} />
|
pipelineId={'xxx-test'}
|
||||||
</StrictMode>
|
columns={initialBoard}
|
||||||
|
initialBoard={initialBoard}
|
||||||
|
initialItems={items}
|
||||||
|
/>,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { StrictMode } from 'react';
|
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
|
|
||||||
import { Company, Person } from '../../../../generated/graphql';
|
|
||||||
import { mockedCompaniesData } from '../../../../testing/mock-data/companies';
|
|
||||||
import { mockedPeopleData } from '../../../../testing/mock-data/people';
|
|
||||||
import { BoardItem } from '../../../ui/components/board/BoardItem';
|
|
||||||
import { BoardCard } from '../BoardCard';
|
|
||||||
|
|
||||||
const meta: Meta<typeof BoardCard> = {
|
|
||||||
title: 'UI/Board/BoardCard',
|
|
||||||
component: BoardCard,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof BoardCard>;
|
|
||||||
|
|
||||||
export const CompanyBoardCard: Story = {
|
|
||||||
render: () => (
|
|
||||||
<StrictMode>
|
|
||||||
<BoardItem draggableProvided={undefined}>
|
|
||||||
<BoardCard item={mockedCompaniesData[0] as Company} />
|
|
||||||
</BoardItem>
|
|
||||||
</StrictMode>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PersonBoardCard: Story = {
|
|
||||||
render: () => (
|
|
||||||
<StrictMode>
|
|
||||||
<BoardItem draggableProvided={undefined}>
|
|
||||||
<BoardCard item={mockedPeopleData[0] as Person} />
|
|
||||||
</BoardItem>
|
|
||||||
</StrictMode>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { StrictMode } 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>;
|
||||||
|
|
||||||
|
export const CompanyCompanyBoardCard: Story = {
|
||||||
|
render: () => (
|
||||||
|
<StrictMode>
|
||||||
|
<CompanyBoardCard company={mockedCompaniesData[0] as Company} />
|
||||||
|
</StrictMode>
|
||||||
|
),
|
||||||
|
};
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { mockedCompaniesData } from '../../../../testing/mock-data/companies';
|
import { Column } from '@/ui/components/board/Board';
|
||||||
import { mockedPeopleData } from '../../../../testing/mock-data/people';
|
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||||
import { Column, Items } from '../../../ui/components/board/Board';
|
|
||||||
|
|
||||||
export const items: Items = {
|
import { CompanyProgressDict } from '../Board';
|
||||||
|
|
||||||
|
export const items: CompanyProgressDict = {
|
||||||
'item-1': mockedCompaniesData[0],
|
'item-1': mockedCompaniesData[0],
|
||||||
'item-2': mockedCompaniesData[1],
|
'item-2': mockedCompaniesData[1],
|
||||||
'item-3': mockedCompaniesData[2],
|
'item-3': mockedCompaniesData[2],
|
||||||
'item-4': mockedPeopleData[0],
|
'item-4': mockedCompaniesData[3],
|
||||||
'item-5': mockedPeopleData[1],
|
|
||||||
'item-6': mockedPeopleData[2],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 7; i <= 20; i++) {
|
for (let i = 7; i <= 20; i++) {
|
||||||
|
|||||||
@@ -1,86 +1,62 @@
|
|||||||
import {
|
import {
|
||||||
GetCompaniesQuery,
|
Company,
|
||||||
GetPeopleQuery,
|
|
||||||
useGetCompaniesQuery,
|
useGetCompaniesQuery,
|
||||||
useGetPeopleQuery,
|
|
||||||
useGetPipelinesQuery,
|
useGetPipelinesQuery,
|
||||||
} from '../../../generated/graphql';
|
} from '../../../generated/graphql';
|
||||||
import { BoardItemKey, Column, Items } from '../../ui/components/board/Board';
|
import { Column } from '../../ui/components/board/Board';
|
||||||
|
|
||||||
type Entities = GetCompaniesQuery | GetPeopleQuery;
|
type Item = Pick<Company, 'id' | 'name' | 'createdAt' | 'domainName'>;
|
||||||
|
type Items = { [key: string]: Item };
|
||||||
|
|
||||||
function isGetCompaniesQuery(
|
export function useBoard(pipelineId: string) {
|
||||||
entities: Entities,
|
const pipelines = useGetPipelinesQuery({
|
||||||
): entities is GetCompaniesQuery {
|
variables: { where: { id: { equals: pipelineId } } },
|
||||||
return (entities as GetCompaniesQuery).companies !== undefined;
|
});
|
||||||
}
|
const pipelineStages = pipelines.data?.findManyPipeline[0]?.pipelineStages;
|
||||||
|
|
||||||
function isGetPeopleQuery(entities: Entities): entities is GetPeopleQuery {
|
|
||||||
return (entities as GetPeopleQuery).people !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useBoard = () => {
|
|
||||||
const pipelines = useGetPipelinesQuery();
|
|
||||||
const pipelineStages = pipelines.data?.findManyPipeline[0].pipelineStages;
|
|
||||||
const initialBoard: Column[] =
|
const initialBoard: Column[] =
|
||||||
pipelineStages?.map((pipelineStage) => ({
|
pipelineStages?.map((pipelineStage) => ({
|
||||||
id: pipelineStage.id,
|
id: pipelineStage.id,
|
||||||
title: pipelineStage.name,
|
title: pipelineStage.name,
|
||||||
colorCode: pipelineStage.color,
|
colorCode: pipelineStage.color,
|
||||||
itemKeys:
|
itemKeys:
|
||||||
pipelineStage.pipelineProgresses?.map(
|
pipelineStage.pipelineProgresses?.map((item) => item.id as string) ||
|
||||||
(item) => item.id as BoardItemKey,
|
[],
|
||||||
) || [],
|
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const pipelineEntityIds = pipelineStages?.reduce(
|
const pipelineProgresses = pipelineStages?.reduce(
|
||||||
(acc, pipelineStage) => [
|
(acc, pipelineStage) => [
|
||||||
...acc,
|
...acc,
|
||||||
...(pipelineStage.pipelineProgresses?.map((item) => ({
|
...(pipelineStage.pipelineProgresses?.map((item) => ({
|
||||||
entityId: item?.progressableId,
|
progressableId: item?.progressableId,
|
||||||
pipelineProgressId: item?.id,
|
pipelineProgressId: item?.id,
|
||||||
})) || []),
|
})) || []),
|
||||||
],
|
],
|
||||||
[] as { entityId: string; pipelineProgressId: string }[],
|
[] as { progressableId: string; pipelineProgressId: string }[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const pipelineProgressableIdsMapper = (pipelineProgressId: string) => {
|
const entitiesQueryResult = useGetCompaniesQuery({
|
||||||
const entityId = pipelineEntityIds?.find(
|
|
||||||
(item) => item.pipelineProgressId === pipelineProgressId,
|
|
||||||
)?.entityId;
|
|
||||||
|
|
||||||
return entityId;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pipelineEntityType =
|
|
||||||
pipelines.data?.findManyPipeline[0].pipelineProgressableType;
|
|
||||||
|
|
||||||
const query =
|
|
||||||
pipelineEntityType === 'Person' ? useGetPeopleQuery : useGetCompaniesQuery;
|
|
||||||
|
|
||||||
const entitiesQueryResult = query({
|
|
||||||
variables: {
|
variables: {
|
||||||
where: { id: { in: pipelineEntityIds?.map((item) => item.entityId) } },
|
where: {
|
||||||
|
id: { in: pipelineProgresses?.map((item) => item.progressableId) },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const indexByIdReducer = (acc: Items, entity: { id: string }) => ({
|
const indexByIdReducer = (acc: Items, entity: Item) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[entity.id]: entity,
|
[entity.id]: entity,
|
||||||
});
|
});
|
||||||
|
|
||||||
const entityItems = entitiesQueryResult.data
|
const companiesDict = entitiesQueryResult.data?.companies.reduce(
|
||||||
? isGetCompaniesQuery(entitiesQueryResult.data)
|
indexByIdReducer,
|
||||||
? entitiesQueryResult.data.companies.reduce(indexByIdReducer, {} as Items)
|
{} as Items,
|
||||||
: isGetPeopleQuery(entitiesQueryResult.data)
|
);
|
||||||
? entitiesQueryResult.data.people.reduce(indexByIdReducer, {} as Items)
|
|
||||||
: undefined
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const items = pipelineEntityIds?.reduce((acc, item) => {
|
const items = pipelineProgresses?.reduce((acc, pipelineProgress) => {
|
||||||
const entityId = pipelineProgressableIdsMapper(item.pipelineProgressId);
|
if (companiesDict?.[pipelineProgress.progressableId]) {
|
||||||
if (entityId) {
|
acc[pipelineProgress.pipelineProgressId] =
|
||||||
acc[item.pipelineProgressId] = entityItems?.[entityId];
|
companiesDict[pipelineProgress.progressableId];
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Items);
|
}, {} as Items);
|
||||||
@@ -90,7 +66,5 @@ export const useBoard = () => {
|
|||||||
items,
|
items,
|
||||||
loading: pipelines.loading || entitiesQueryResult.loading,
|
loading: pipelines.loading || entitiesQueryResult.loading,
|
||||||
error: pipelines.error || entitiesQueryResult.error,
|
error: pipelines.error || entitiesQueryResult.error,
|
||||||
pipelineId: pipelines.data?.findManyPipeline[0].id,
|
|
||||||
pipelineEntityType,
|
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const GET_PIPELINES = gql`
|
export const GET_PIPELINES = gql`
|
||||||
query GetPipelines {
|
query GetPipelines($where: PipelineWhereInput) {
|
||||||
findManyPipeline {
|
findManyPipeline(where: $where) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
pipelineProgressableType
|
pipelineProgressableType
|
||||||
|
|||||||
@@ -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: {},
|
||||||
|
});
|
||||||
@@ -5,37 +5,33 @@ export const StyledBoard = styled.div`
|
|||||||
border-radius: ${({ theme }) => theme.spacing(2)};
|
border-radius: ${({ theme }) => theme.spacing(2)};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
height: 100%;
|
height: calc(100%);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type BoardItemKey = string;
|
|
||||||
export type Item = any & { id: string };
|
|
||||||
export interface Items {
|
|
||||||
[key: string]: Item;
|
|
||||||
}
|
|
||||||
export interface Column {
|
export interface Column {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
colorCode?: string;
|
colorCode?: string;
|
||||||
itemKeys: BoardItemKey[];
|
itemKeys: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOptimisticlyUpdatedBoard = (
|
export function getOptimisticlyUpdatedBoard(
|
||||||
board: Column[],
|
board: Column[],
|
||||||
result: DropResult,
|
result: DropResult,
|
||||||
) => {
|
) {
|
||||||
|
const newBoard = JSON.parse(JSON.stringify(board));
|
||||||
const { destination, source } = result;
|
const { destination, source } = result;
|
||||||
if (!destination) return;
|
if (!destination) return;
|
||||||
const sourceColumnIndex = board.findIndex(
|
const sourceColumnIndex = newBoard.findIndex(
|
||||||
(column) => column.id === source.droppableId,
|
(column: Column) => column.id === source.droppableId,
|
||||||
);
|
);
|
||||||
const sourceColumn = board[sourceColumnIndex];
|
const sourceColumn = newBoard[sourceColumnIndex];
|
||||||
const destinationColumnIndex = board.findIndex(
|
const destinationColumnIndex = newBoard.findIndex(
|
||||||
(column) => column.id === destination.droppableId,
|
(column: Column) => column.id === destination.droppableId,
|
||||||
);
|
);
|
||||||
const destinationColumn = board[destinationColumnIndex];
|
const destinationColumn = newBoard[destinationColumnIndex];
|
||||||
if (!destinationColumn || !sourceColumn) return;
|
if (!destinationColumn || !sourceColumn) return;
|
||||||
const sourceItems = sourceColumn.itemKeys;
|
const sourceItems = sourceColumn.itemKeys;
|
||||||
const destinationItems = destinationColumn.itemKeys;
|
const destinationItems = destinationColumn.itemKeys;
|
||||||
@@ -53,8 +49,7 @@ export const getOptimisticlyUpdatedBoard = (
|
|||||||
itemKeys: destinationItems,
|
itemKeys: destinationItems,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newBoard = [...board];
|
|
||||||
newBoard.splice(sourceColumnIndex, 1, newSourceColumn);
|
newBoard.splice(sourceColumnIndex, 1, newSourceColumn);
|
||||||
newBoard.splice(destinationColumnIndex, 1, newDestinationColumn);
|
newBoard.splice(destinationColumnIndex, 1, newDestinationColumn);
|
||||||
return newBoard;
|
return newBoard;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { DroppableProvided } 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 StyledColumn = styled.div`
|
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;
|
min-width: 200px;
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ScrollableColumn = styled.div`
|
|
||||||
max-height: calc(100vh - 120px);
|
|
||||||
overflow-y: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledColumnTitle = styled.h3`
|
export const StyledColumnTitle = styled.h3`
|
||||||
color: ${({ color }) => color};
|
color: ${({ color }) => color};
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
@@ -26,26 +20,17 @@ export const StyledColumnTitle = styled.h3`
|
|||||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledPlaceholder = styled.div`
|
type OwnProps = {
|
||||||
min-height: 1px;
|
colorCode?: string;
|
||||||
`;
|
title: string;
|
||||||
|
|
||||||
export const StyledItemContainer = styled.div``;
|
|
||||||
|
|
||||||
export const ItemsContainer = ({
|
|
||||||
children,
|
|
||||||
droppableProvided,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
droppableProvided: DroppableProvided;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<StyledItemContainer
|
|
||||||
ref={droppableProvided?.innerRef}
|
|
||||||
{...droppableProvided?.droppableProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<StyledPlaceholder>{droppableProvided?.placeholder}</StyledPlaceholder>
|
|
||||||
</StyledItemContainer>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function BoardColumn({ colorCode, title, children }: OwnProps) {
|
||||||
|
return (
|
||||||
|
<StyledColumn>
|
||||||
|
<StyledColumnTitle color={colorCode}>• {title}</StyledColumnTitle>
|
||||||
|
{children}
|
||||||
|
</StyledColumn>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { DraggableProvided } 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
|
|
||||||
|
|
||||||
const StyledCard = styled.div`
|
|
||||||
background-color: ${({ theme }) => theme.secondaryBackground};
|
|
||||||
border: 1px solid ${({ theme }) => theme.quaternaryBackground};
|
|
||||||
border-radius: ${({ theme }) => theme.borderRadius};
|
|
||||||
box-shadow: ${({ theme }) => theme.boxShadow};
|
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
|
||||||
max-width: 300px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type BoardCardProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
draggableProvided?: DraggableProvided;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BoardItem = ({ children, draggableProvided }: BoardCardProps) => {
|
|
||||||
return (
|
|
||||||
<StyledCard
|
|
||||||
ref={draggableProvided?.innerRef}
|
|
||||||
{...draggableProvided?.dragHandleProps}
|
|
||||||
{...draggableProvided?.draggableProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</StyledCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
@@ -22,19 +21,17 @@ const StyledButton = styled.button`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const NewButton = ({
|
type OwnProps = {
|
||||||
onClick,
|
onClick: () => void;
|
||||||
}: {
|
};
|
||||||
onClick?: (...args: any[]) => void;
|
|
||||||
}) => {
|
export function NewButton({ onClick }: OwnProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const onInnerClick = useCallback(() => {
|
|
||||||
onClick && onClick({ id: 'twenty-aaffcfbd-f86b-419f-b794-02319abe8637' });
|
|
||||||
}, [onClick]);
|
|
||||||
return (
|
return (
|
||||||
<StyledButton onClick={onInnerClick}>
|
<StyledButton onClick={onClick}>
|
||||||
<IconPlus size={theme.iconSizeMedium} />
|
<IconPlus size={theme.iconSizeMedium} />
|
||||||
New
|
New
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
@@ -27,7 +27,7 @@ export function WithTopBarContainer({
|
|||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<TopBar title={title} icon={icon} onAddButtonClick={onAddButtonClick} />
|
<TopBar title={title} icon={icon} onAddButtonClick={onAddButtonClick} />
|
||||||
<ContentContainer topMargin={TOP_BAR_MIN_HEIGHT}>
|
<ContentContainer topMargin={TOP_BAR_MIN_HEIGHT + 16 + 16}>
|
||||||
{children}
|
{children}
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ const lightThemeSpecific = {
|
|||||||
blueLowTransparency: 'rgba(25, 97, 237, 0.32)',
|
blueLowTransparency: 'rgba(25, 97, 237, 0.32)',
|
||||||
boxShadow: '0px 2px 4px 0px #0F0F0F0A',
|
boxShadow: '0px 2px 4px 0px #0F0F0F0A',
|
||||||
modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)',
|
modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)',
|
||||||
|
lightBoxShadow:
|
||||||
|
'0px 2px 4px 0px rgba(0, 0, 0, 0.04), 0px 0px 4px 0px rgba(0, 0, 0, 0.08)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const darkThemeSpecific: typeof lightThemeSpecific = {
|
const darkThemeSpecific: typeof lightThemeSpecific = {
|
||||||
@@ -48,6 +50,8 @@ const darkThemeSpecific: typeof lightThemeSpecific = {
|
|||||||
blueLowTransparency: 'rgba(104, 149, 236, 0.32)',
|
blueLowTransparency: 'rgba(104, 149, 236, 0.32)',
|
||||||
boxShadow: '0px 2px 4px 0px #0F0F0F0A', // TODO change color for dark theme
|
boxShadow: '0px 2px 4px 0px #0F0F0F0A', // TODO change color for dark theme
|
||||||
modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)', // TODO change color for dark theme
|
modalBoxShadow: '0px 3px 12px rgba(0, 0, 0, 0.09)', // TODO change color for dark theme
|
||||||
|
lightBoxShadow:
|
||||||
|
'0px 2px 4px 0px rgba(0, 0, 0, 0.04), 0px 0px 4px 0px rgba(0, 0, 0, 0.08)',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
|
export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ export const GET_CURRENT_USER = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_USERS = gql`
|
||||||
|
query GetUsers {
|
||||||
|
findManyUser {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export function useGetCurrentUserQuery(userId: string | null) {
|
export function useGetCurrentUserQuery(userId: string | null) {
|
||||||
return generatedUseGetCurrentUserQuery({
|
return generatedUseGetCurrentUserQuery({
|
||||||
variables: {
|
variables: {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
import { IconTargetArrow } from '@/ui/icons/index';
|
import { IconTargetArrow } from '@/ui/icons/index';
|
||||||
@@ -8,18 +7,19 @@ import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer'
|
|||||||
import {
|
import {
|
||||||
PipelineProgress,
|
PipelineProgress,
|
||||||
PipelineStage,
|
PipelineStage,
|
||||||
useCreateOnePipelineProgressMutation,
|
useGetPipelinesQuery,
|
||||||
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 theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { initialBoard, items, error, pipelineId, pipelineEntityType } =
|
const pipelines = useGetPipelinesQuery();
|
||||||
useBoard();
|
const pipelineId = pipelines.data?.findManyPipeline[0].id;
|
||||||
|
|
||||||
|
const { initialBoard, items } = useBoard(pipelineId || '');
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() =>
|
() =>
|
||||||
initialBoard?.map(({ id, colorCode, title }) => ({
|
initialBoard?.map(({ id, colorCode, title }) => ({
|
||||||
@@ -30,7 +30,6 @@ export function Opportunities() {
|
|||||||
[initialBoard],
|
[initialBoard],
|
||||||
);
|
);
|
||||||
const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation();
|
const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation();
|
||||||
const [createPipelineProgress] = useCreateOnePipelineProgressMutation();
|
|
||||||
|
|
||||||
const onUpdate = useCallback(
|
const onUpdate = useCallback(
|
||||||
async (
|
async (
|
||||||
@@ -44,43 +43,22 @@ export function Opportunities() {
|
|||||||
[updatePipelineProgress],
|
[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 (error) return <div>Error...</div>;
|
|
||||||
if (!initialBoard || !items) {
|
|
||||||
return <div>Initial board or items not found</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithTopBarContainer
|
<WithTopBarContainer
|
||||||
title="Opportunities"
|
title="Opportunities"
|
||||||
icon={<IconTargetArrow size={theme.iconSizeMedium} />}
|
icon={<IconTargetArrow size={theme.iconSizeMedium} />}
|
||||||
>
|
>
|
||||||
<Board
|
{items && pipelineId ? (
|
||||||
columns={columns || []}
|
<Board
|
||||||
initialBoard={initialBoard}
|
pipelineId={pipelineId}
|
||||||
items={items}
|
columns={columns || []}
|
||||||
onUpdate={onUpdate}
|
initialBoard={initialBoard}
|
||||||
onClickNew={onClickNew}
|
initialItems={items}
|
||||||
/>
|
onUpdate={onUpdate}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</WithTopBarContainer>
|
</WithTopBarContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export class AbilityFactory {
|
|||||||
|
|
||||||
// PipelineProgress
|
// PipelineProgress
|
||||||
can(AbilityAction.Read, 'PipelineProgress', { workspaceId: workspace.id });
|
can(AbilityAction.Read, 'PipelineProgress', { workspaceId: workspace.id });
|
||||||
|
can(AbilityAction.Create, 'PipelineProgress');
|
||||||
can(AbilityAction.Update, 'PipelineProgress', {
|
can(AbilityAction.Update, 'PipelineProgress', {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user