289 on opportunities page i see person and company card layout read only (#293)
* feature: create boardCard component * test: add snapshot for BoardCards * feature: fix typename of company * feature: add max width on BoardItem * feature: design CompanyBoardCard * feature: Add People picture and name * feature: design PeopleCard * feature: fix font weight for card header * test: fix storybook for board * test: add unit test for column optimistic renderer
This commit is contained in:
@ -33,12 +33,12 @@ describe('Company mappers', () => {
|
||||
__typename: 'Pipe',
|
||||
},
|
||||
],
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
} satisfies GraphqlQueryCompany;
|
||||
|
||||
const company = mapToCompany(graphQLCompany);
|
||||
expect(company).toStrictEqual({
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
id: graphQLCompany.id,
|
||||
name: graphQLCompany.name,
|
||||
domainName: graphQLCompany.domainName,
|
||||
@ -77,7 +77,7 @@ describe('Company mappers', () => {
|
||||
__typename: 'users',
|
||||
},
|
||||
createdAt: now,
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
} satisfies Company;
|
||||
const graphQLCompany = mapToGqlCompany(company);
|
||||
expect(graphQLCompany).toStrictEqual({
|
||||
@ -88,7 +88,7 @@ describe('Company mappers', () => {
|
||||
employees: company.employees,
|
||||
address: company.address,
|
||||
accountOwnerId: '522d4ec4-c46b-4360-a0a7-df8df170be81',
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
} satisfies GraphqlMutationCompany);
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from '../../users/interfaces/user.interface';
|
||||
|
||||
export type Company = {
|
||||
__typename: 'companies';
|
||||
__typename: 'Company';
|
||||
id: string;
|
||||
name?: string;
|
||||
domainName?: string;
|
||||
@ -54,7 +54,7 @@ export type GraphqlMutationCompany = {
|
||||
};
|
||||
|
||||
export const mapToCompany = (company: GraphqlQueryCompany): Company => ({
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
id: company.id,
|
||||
employees: company.employees,
|
||||
name: company.name,
|
||||
@ -80,5 +80,5 @@ export const mapToGqlCompany = (company: Company): GraphqlMutationCompany => ({
|
||||
createdAt: company.createdAt ? company.createdAt.toUTCString() : undefined,
|
||||
|
||||
accountOwnerId: company.accountOwner?.id,
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
});
|
||||
|
||||
@ -61,9 +61,7 @@ export const Board = ({ initialBoard, items }: BoardProps) => {
|
||||
>
|
||||
{(draggableProvided) => (
|
||||
<BoardItem draggableProvided={draggableProvided}>
|
||||
<BoardCard>
|
||||
{items[itemKey]?.id || 'Item not found'}
|
||||
</BoardCard>
|
||||
<BoardCard item={items[itemKey]} />
|
||||
</BoardItem>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
@ -1,5 +1,122 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const BoardCard = styled.p`
|
||||
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 {
|
||||
IconBuilding,
|
||||
IconCalendar,
|
||||
IconMail,
|
||||
IconPhone,
|
||||
IconSum,
|
||||
IconUser,
|
||||
} 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: 16px;
|
||||
margin-right: ${(props) => props.theme.spacing(2)};
|
||||
object-fit: cover;
|
||||
width: 16px;
|
||||
}
|
||||
`;
|
||||
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}`;
|
||||
return (
|
||||
<StyledBoardCard>
|
||||
<StyledBoardCardHeader>
|
||||
<img
|
||||
data-testid="person-chip-image"
|
||||
src={PersonPlaceholder.toString()}
|
||||
alt="person"
|
||||
/>
|
||||
{fullname}
|
||||
</StyledBoardCardHeader>
|
||||
<StyledBoardCardBody>
|
||||
<span>
|
||||
<IconBuilding size={16} />
|
||||
<CompanyChip
|
||||
name={person.company?.name || ''}
|
||||
picture={getLogoUrlFromDomainName(
|
||||
person.company?.domainName,
|
||||
).toString()}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<IconMail size={16} />
|
||||
{person.email}
|
||||
</span>
|
||||
<span>
|
||||
<IconPhone size={16} />
|
||||
{person.phone}
|
||||
</span>
|
||||
<span>
|
||||
<IconCalendar size={16} />
|
||||
{humanReadableDate(new Date(person.createdAt as string))}
|
||||
</span>
|
||||
</StyledBoardCardBody>
|
||||
</StyledBoardCard>
|
||||
);
|
||||
};
|
||||
|
||||
const CompanyBoardCard = ({ company }: { company: Company }) => {
|
||||
return (
|
||||
<StyledBoardCard>
|
||||
<StyledBoardCardHeader>
|
||||
<img
|
||||
src={getLogoUrlFromDomainName(company.domainName).toString()}
|
||||
alt={`${company.name}-company-logo`}
|
||||
/>
|
||||
<span>{company.name}</span>
|
||||
</StyledBoardCardHeader>
|
||||
<StyledBoardCardBody>
|
||||
<span>
|
||||
<IconUser size={16} />
|
||||
<PersonChip name={company.accountOwner?.displayName || ''} />
|
||||
</span>
|
||||
<span>
|
||||
<IconSum size={16} /> {company.employees}
|
||||
</span>
|
||||
<span>
|
||||
<IconCalendar size={16} />
|
||||
{humanReadableDate(new Date(company.createdAt as string))}
|
||||
</span>
|
||||
</StyledBoardCardBody>
|
||||
</StyledBoardCard>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
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>
|
||||
),
|
||||
};
|
||||
@ -1,16 +1,22 @@
|
||||
import { mockedCompaniesData } from '../../../../testing/mock-data/companies';
|
||||
import { mockedPeopleData } from '../../../../testing/mock-data/people';
|
||||
import { Column, Items } from '../../../ui/components/board/Board';
|
||||
|
||||
export const items: Items = {
|
||||
'item-1': { id: 'item-1', content: 'Item 1' },
|
||||
'item-2': { id: 'item-2', content: 'Item 2' },
|
||||
'item-3': { id: 'item-3', content: 'Item 3' },
|
||||
'item-4': { id: 'item-4', content: 'Item 4' },
|
||||
'item-5': { id: 'item-5', content: 'Item 5' },
|
||||
'item-6': { id: 'item-6', content: 'Item 6' },
|
||||
'item-1': mockedCompaniesData[0],
|
||||
'item-2': mockedCompaniesData[1],
|
||||
'item-3': mockedCompaniesData[2],
|
||||
'item-4': mockedPeopleData[0],
|
||||
'item-5': mockedPeopleData[1],
|
||||
'item-6': mockedPeopleData[2],
|
||||
};
|
||||
|
||||
for (let i = 7; i <= 20; i++) {
|
||||
const key = `item-${i}`;
|
||||
items[key] = { id: key, content: `Item ${i}` };
|
||||
items[key] = {
|
||||
...mockedCompaniesData[i % mockedCompaniesData.length],
|
||||
id: key,
|
||||
};
|
||||
}
|
||||
|
||||
export const initialBoard = [
|
||||
|
||||
@ -39,7 +39,7 @@ describe('Person mappers', () => {
|
||||
phone: graphQLPerson.phone,
|
||||
_commentCount: 1,
|
||||
company: {
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87',
|
||||
accountOwner: undefined,
|
||||
address: undefined,
|
||||
|
||||
@ -31,7 +31,7 @@ it('updates a person', async () => {
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
name: 'ACME',
|
||||
domainName: 'example.com',
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
},
|
||||
phone: '+1 (555) 123-4567',
|
||||
pipes: [
|
||||
|
||||
@ -8,10 +8,7 @@ export const StyledBoard = styled.div`
|
||||
`;
|
||||
|
||||
export type BoardItemKey = `item-${number | string}`;
|
||||
export interface Item {
|
||||
id: string;
|
||||
content?: string;
|
||||
}
|
||||
export type Item = any & { id: string };
|
||||
export interface Items {
|
||||
[key: string]: Item;
|
||||
}
|
||||
|
||||
@ -7,12 +7,12 @@ const StyledCard = styled.div`
|
||||
border-radius: ${({ theme }) => theme.borderRadius};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
max-width: 300px;
|
||||
`;
|
||||
|
||||
type BoardCardProps = {
|
||||
children: React.ReactNode;
|
||||
draggableProvided: DraggableProvided;
|
||||
draggableProvided?: DraggableProvided;
|
||||
};
|
||||
|
||||
export const BoardItem = ({ children, draggableProvided }: BoardCardProps) => {
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
import { DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
import { BoardItemKey, getOptimisticlyUpdatedBoard } from '../Board';
|
||||
|
||||
describe('getOptimisticlyUpdatedBoard', () => {
|
||||
it('should return a new board with the updated cell', () => {
|
||||
const initialColumn1: BoardItemKey[] = ['item-1', 'item-2', 'item-3'];
|
||||
const initialColumn2: BoardItemKey[] = ['item-4', 'item-5'];
|
||||
|
||||
const finalColumn1: BoardItemKey[] = ['item-2', 'item-3'];
|
||||
const finalColumn2: BoardItemKey[] = ['item-4', 'item-1', 'item-5'];
|
||||
|
||||
const dropResult = {
|
||||
source: {
|
||||
droppableId: 'column-1',
|
||||
index: 0,
|
||||
},
|
||||
destination: {
|
||||
droppableId: 'column-2',
|
||||
index: 1,
|
||||
},
|
||||
} as DropResult;
|
||||
|
||||
const initialBoard = [
|
||||
{
|
||||
id: 'column-1',
|
||||
title: 'My Column',
|
||||
itemKeys: initialColumn1,
|
||||
},
|
||||
{
|
||||
id: 'column-2',
|
||||
title: 'My Column',
|
||||
itemKeys: initialColumn2,
|
||||
},
|
||||
];
|
||||
|
||||
const updatedBoard = getOptimisticlyUpdatedBoard(initialBoard, dropResult);
|
||||
|
||||
const finalBoard = [
|
||||
{
|
||||
id: 'column-1',
|
||||
title: 'My Column',
|
||||
itemKeys: finalColumn1,
|
||||
},
|
||||
{
|
||||
id: 'column-2',
|
||||
title: 'My Column',
|
||||
itemKeys: finalColumn2,
|
||||
},
|
||||
];
|
||||
|
||||
expect(updatedBoard).toEqual(finalBoard);
|
||||
expect(updatedBoard).not.toBe(initialBoard);
|
||||
});
|
||||
});
|
||||
@ -24,7 +24,7 @@ import { currentRowSelectionState } from '../../tables/states/rowSelectionState'
|
||||
import { TableHeader } from './table-header/TableHeader';
|
||||
|
||||
type OwnProps<
|
||||
TData extends { id: string; __typename: 'companies' | 'people' },
|
||||
TData extends { id: string; __typename: 'Company' | 'people' },
|
||||
SortField,
|
||||
> = {
|
||||
data: Array<TData>;
|
||||
@ -109,7 +109,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
|
||||
`;
|
||||
|
||||
export function EntityTable<
|
||||
TData extends { id: string; __typename: 'companies' | 'people' },
|
||||
TData extends { id: string; __typename: 'Company' | 'people' },
|
||||
SortField,
|
||||
>({
|
||||
data,
|
||||
|
||||
@ -66,7 +66,7 @@ export function Companies() {
|
||||
pipes: [],
|
||||
createdAt: new Date(),
|
||||
accountOwner: null,
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
};
|
||||
|
||||
await insertCompany(newCompany);
|
||||
|
||||
@ -7,7 +7,7 @@ describe('PeopleFilter', () => {
|
||||
id: 'test-id',
|
||||
name: 'test-name',
|
||||
domainName: 'test-domain-name',
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@ -15,7 +15,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
__typename: 'users',
|
||||
},
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
},
|
||||
{
|
||||
id: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
|
||||
@ -26,7 +26,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
|
||||
employees: 1,
|
||||
_commentCount: 1,
|
||||
accountOwner: null,
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
},
|
||||
{
|
||||
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
|
||||
@ -37,7 +37,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
|
||||
employees: 1,
|
||||
_commentCount: 1,
|
||||
accountOwner: null,
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
},
|
||||
{
|
||||
id: 'b1cfd51b-a831-455f-ba07-4e30671e1dc3',
|
||||
@ -48,7 +48,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
|
||||
employees: 10,
|
||||
_commentCount: 0,
|
||||
accountOwner: null,
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
},
|
||||
{
|
||||
id: '5c21e19e-e049-4393-8c09-3e3f8fb09ecb',
|
||||
@ -59,7 +59,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
|
||||
employees: 1,
|
||||
_commentCount: 2,
|
||||
accountOwner: null,
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
},
|
||||
{
|
||||
id: '9d162de6-cfbf-4156-a790-e39854dcd4eb',
|
||||
@ -70,7 +70,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
|
||||
employees: 1,
|
||||
_commentCount: 13,
|
||||
accountOwner: null,
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
},
|
||||
{
|
||||
id: '9d162de6-cfbf-4156-a790-e39854dcd4ef',
|
||||
@ -81,6 +81,6 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
|
||||
employees: 1,
|
||||
_commentCount: 1,
|
||||
accountOwner: null,
|
||||
__typename: 'companies',
|
||||
__typename: 'Company',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user