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:
Sammy Teillet
2023-06-14 17:06:50 +02:00
committed by GitHub
parent 5381e28253
commit 287168f691
15 changed files with 246 additions and 37 deletions

View File

@ -33,12 +33,12 @@ describe('Company mappers', () => {
__typename: 'Pipe', __typename: 'Pipe',
}, },
], ],
__typename: 'companies', __typename: 'Company',
} satisfies GraphqlQueryCompany; } satisfies GraphqlQueryCompany;
const company = mapToCompany(graphQLCompany); const company = mapToCompany(graphQLCompany);
expect(company).toStrictEqual({ expect(company).toStrictEqual({
__typename: 'companies', __typename: 'Company',
id: graphQLCompany.id, id: graphQLCompany.id,
name: graphQLCompany.name, name: graphQLCompany.name,
domainName: graphQLCompany.domainName, domainName: graphQLCompany.domainName,
@ -77,7 +77,7 @@ describe('Company mappers', () => {
__typename: 'users', __typename: 'users',
}, },
createdAt: now, createdAt: now,
__typename: 'companies', __typename: 'Company',
} satisfies Company; } satisfies Company;
const graphQLCompany = mapToGqlCompany(company); const graphQLCompany = mapToGqlCompany(company);
expect(graphQLCompany).toStrictEqual({ expect(graphQLCompany).toStrictEqual({
@ -88,7 +88,7 @@ describe('Company mappers', () => {
employees: company.employees, employees: company.employees,
address: company.address, address: company.address,
accountOwnerId: '522d4ec4-c46b-4360-a0a7-df8df170be81', accountOwnerId: '522d4ec4-c46b-4360-a0a7-df8df170be81',
__typename: 'companies', __typename: 'Company',
} satisfies GraphqlMutationCompany); } satisfies GraphqlMutationCompany);
}); });
}); });

View File

@ -9,7 +9,7 @@ import {
} from '../../users/interfaces/user.interface'; } from '../../users/interfaces/user.interface';
export type Company = { export type Company = {
__typename: 'companies'; __typename: 'Company';
id: string; id: string;
name?: string; name?: string;
domainName?: string; domainName?: string;
@ -54,7 +54,7 @@ export type GraphqlMutationCompany = {
}; };
export const mapToCompany = (company: GraphqlQueryCompany): Company => ({ export const mapToCompany = (company: GraphqlQueryCompany): Company => ({
__typename: 'companies', __typename: 'Company',
id: company.id, id: company.id,
employees: company.employees, employees: company.employees,
name: company.name, name: company.name,
@ -80,5 +80,5 @@ export const mapToGqlCompany = (company: Company): GraphqlMutationCompany => ({
createdAt: company.createdAt ? company.createdAt.toUTCString() : undefined, createdAt: company.createdAt ? company.createdAt.toUTCString() : undefined,
accountOwnerId: company.accountOwner?.id, accountOwnerId: company.accountOwner?.id,
__typename: 'companies', __typename: 'Company',
}); });

View File

@ -61,9 +61,7 @@ export const Board = ({ initialBoard, items }: BoardProps) => {
> >
{(draggableProvided) => ( {(draggableProvided) => (
<BoardItem draggableProvided={draggableProvided}> <BoardItem draggableProvided={draggableProvided}>
<BoardCard> <BoardCard item={items[itemKey]} />
{items[itemKey]?.id || 'Item not found'}
</BoardCard>
</BoardItem> </BoardItem>
)} )}
</Draggable> </Draggable>

View File

@ -1,5 +1,122 @@
import styled from '@emotion/styled'; 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}; 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>
);
};

View File

@ -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>
),
};

View File

@ -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'; import { Column, Items } from '../../../ui/components/board/Board';
export const items: Items = { export const items: Items = {
'item-1': { id: 'item-1', content: 'Item 1' }, 'item-1': mockedCompaniesData[0],
'item-2': { id: 'item-2', content: 'Item 2' }, 'item-2': mockedCompaniesData[1],
'item-3': { id: 'item-3', content: 'Item 3' }, 'item-3': mockedCompaniesData[2],
'item-4': { id: 'item-4', content: 'Item 4' }, 'item-4': mockedPeopleData[0],
'item-5': { id: 'item-5', content: 'Item 5' }, 'item-5': mockedPeopleData[1],
'item-6': { id: 'item-6', content: 'Item 6' }, 'item-6': mockedPeopleData[2],
}; };
for (let i = 7; i <= 20; i++) { for (let i = 7; i <= 20; i++) {
const key = `item-${i}`; const key = `item-${i}`;
items[key] = { id: key, content: `Item ${i}` }; items[key] = {
...mockedCompaniesData[i % mockedCompaniesData.length],
id: key,
};
} }
export const initialBoard = [ export const initialBoard = [

View File

@ -39,7 +39,7 @@ describe('Person mappers', () => {
phone: graphQLPerson.phone, phone: graphQLPerson.phone,
_commentCount: 1, _commentCount: 1,
company: { company: {
__typename: 'companies', __typename: 'Company',
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87', id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87',
accountOwner: undefined, accountOwner: undefined,
address: undefined, address: undefined,

View File

@ -31,7 +31,7 @@ it('updates a person', async () => {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
name: 'ACME', name: 'ACME',
domainName: 'example.com', domainName: 'example.com',
__typename: 'companies', __typename: 'Company',
}, },
phone: '+1 (555) 123-4567', phone: '+1 (555) 123-4567',
pipes: [ pipes: [

View File

@ -8,10 +8,7 @@ export const StyledBoard = styled.div`
`; `;
export type BoardItemKey = `item-${number | string}`; export type BoardItemKey = `item-${number | string}`;
export interface Item { export type Item = any & { id: string };
id: string;
content?: string;
}
export interface Items { export interface Items {
[key: string]: Item; [key: string]: Item;
} }

View File

@ -7,12 +7,12 @@ const StyledCard = styled.div`
border-radius: ${({ theme }) => theme.borderRadius}; border-radius: ${({ theme }) => theme.borderRadius};
box-shadow: ${({ theme }) => theme.boxShadow}; box-shadow: ${({ theme }) => theme.boxShadow};
margin-bottom: ${({ theme }) => theme.spacing(2)}; margin-bottom: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)}; max-width: 300px;
`; `;
type BoardCardProps = { type BoardCardProps = {
children: React.ReactNode; children: React.ReactNode;
draggableProvided: DraggableProvided; draggableProvided?: DraggableProvided;
}; };
export const BoardItem = ({ children, draggableProvided }: BoardCardProps) => { export const BoardItem = ({ children, draggableProvided }: BoardCardProps) => {

View File

@ -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);
});
});

View File

@ -24,7 +24,7 @@ import { currentRowSelectionState } from '../../tables/states/rowSelectionState'
import { TableHeader } from './table-header/TableHeader'; import { TableHeader } from './table-header/TableHeader';
type OwnProps< type OwnProps<
TData extends { id: string; __typename: 'companies' | 'people' }, TData extends { id: string; __typename: 'Company' | 'people' },
SortField, SortField,
> = { > = {
data: Array<TData>; data: Array<TData>;
@ -109,7 +109,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
`; `;
export function EntityTable< export function EntityTable<
TData extends { id: string; __typename: 'companies' | 'people' }, TData extends { id: string; __typename: 'Company' | 'people' },
SortField, SortField,
>({ >({
data, data,

View File

@ -66,7 +66,7 @@ export function Companies() {
pipes: [], pipes: [],
createdAt: new Date(), createdAt: new Date(),
accountOwner: null, accountOwner: null,
__typename: 'companies', __typename: 'Company',
}; };
await insertCompany(newCompany); await insertCompany(newCompany);

View File

@ -7,7 +7,7 @@ describe('PeopleFilter', () => {
id: 'test-id', id: 'test-id',
name: 'test-name', name: 'test-name',
domainName: 'test-domain-name', domainName: 'test-domain-name',
__typename: 'companies', __typename: 'Company',
}), }),
).toMatchSnapshot(); ).toMatchSnapshot();
}); });

View File

@ -15,7 +15,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
__typename: 'users', __typename: 'users',
}, },
__typename: 'companies', __typename: 'Company',
}, },
{ {
id: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae', id: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
@ -26,7 +26,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
employees: 1, employees: 1,
_commentCount: 1, _commentCount: 1,
accountOwner: null, accountOwner: null,
__typename: 'companies', __typename: 'Company',
}, },
{ {
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d', id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
@ -37,7 +37,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
employees: 1, employees: 1,
_commentCount: 1, _commentCount: 1,
accountOwner: null, accountOwner: null,
__typename: 'companies', __typename: 'Company',
}, },
{ {
id: 'b1cfd51b-a831-455f-ba07-4e30671e1dc3', id: 'b1cfd51b-a831-455f-ba07-4e30671e1dc3',
@ -48,7 +48,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
employees: 10, employees: 10,
_commentCount: 0, _commentCount: 0,
accountOwner: null, accountOwner: null,
__typename: 'companies', __typename: 'Company',
}, },
{ {
id: '5c21e19e-e049-4393-8c09-3e3f8fb09ecb', id: '5c21e19e-e049-4393-8c09-3e3f8fb09ecb',
@ -59,7 +59,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
employees: 1, employees: 1,
_commentCount: 2, _commentCount: 2,
accountOwner: null, accountOwner: null,
__typename: 'companies', __typename: 'Company',
}, },
{ {
id: '9d162de6-cfbf-4156-a790-e39854dcd4eb', id: '9d162de6-cfbf-4156-a790-e39854dcd4eb',
@ -70,7 +70,7 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
employees: 1, employees: 1,
_commentCount: 13, _commentCount: 13,
accountOwner: null, accountOwner: null,
__typename: 'companies', __typename: 'Company',
}, },
{ {
id: '9d162de6-cfbf-4156-a790-e39854dcd4ef', id: '9d162de6-cfbf-4156-a790-e39854dcd4ef',
@ -81,6 +81,6 @@ export const mockedCompaniesData: Array<GraphqlQueryCompany> = [
employees: 1, employees: 1,
_commentCount: 1, _commentCount: 1,
accountOwner: null, accountOwner: null,
__typename: 'companies', __typename: 'Company',
}, },
]; ];