mirror of
				https://github.com/lingble/twenty.git
				synced 2025-11-03 22:27:57 +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}
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                              <CompanyBoardCard company={items[itemKey]} />
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
                          )}
 | 
					                          )}
 | 
				
			||||||
                        </Draggable>
 | 
					                        </Draggable>
 | 
				
			||||||
                    ))}
 | 
					                      ),
 | 
				
			||||||
                  </ItemsContainer>
 | 
					                  )}
 | 
				
			||||||
                  <NewButton onClick={onClickFunctions[column.id]} />
 | 
					                </BoardColumnCardsContainer>
 | 
				
			||||||
                </ScrollableColumn>
 | 
					                <NewButton pipelineId={pipelineId} columnId={column.id} />
 | 
				
			||||||
              </StyledColumn>
 | 
					              </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} />}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
 | 
					      {items && pipelineId ? (
 | 
				
			||||||
        <Board
 | 
					        <Board
 | 
				
			||||||
 | 
					          pipelineId={pipelineId}
 | 
				
			||||||
          columns={columns || []}
 | 
					          columns={columns || []}
 | 
				
			||||||
          initialBoard={initialBoard}
 | 
					          initialBoard={initialBoard}
 | 
				
			||||||
        items={items}
 | 
					          initialItems={items}
 | 
				
			||||||
          onUpdate={onUpdate}
 | 
					          onUpdate={onUpdate}
 | 
				
			||||||
        onClickNew={onClickNew}
 | 
					 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <></>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
    </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