Hide Opportunities as nothing is built yet and make company table fully editable (#109)

* Hide Opportunities as nothing is built yet and make company table fully editable

* Fix tests
This commit is contained in:
Charles Bochet
2023-05-06 19:08:47 +02:00
committed by GitHub
parent 760a49c5e3
commit 8c7815af79
12 changed files with 201 additions and 73 deletions

View File

@ -10,7 +10,6 @@ it('Checks the App component renders', async () => {
const { getByText } = render(<RegularApp />); const { getByText } = render(<RegularApp />);
expect(getByText('Companies')).toBeDefined(); expect(getByText('Companies')).toBeDefined();
expect(getByText('Opportunities')).toBeDefined();
await waitFor(() => { await waitFor(() => {
expect(getByText('Twenty')).toBeDefined(); expect(getByText('Twenty')).toBeDefined();
}); });

View File

@ -2,7 +2,7 @@ import * as React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import PersonPlaceholder from './person-placeholder.png'; import PersonPlaceholder from './person-placeholder.png';
type OwnProps = { export type PersonChipPropsType = {
name: string; name: string;
picture?: string; picture?: string;
}; };
@ -28,7 +28,7 @@ const StyledContainer = styled.span`
} }
`; `;
function PersonChip({ name, picture }: OwnProps) { function PersonChip({ name, picture }: PersonChipPropsType) {
return ( return (
<StyledContainer data-testid="person-chip"> <StyledContainer data-testid="person-chip">
<img <img

View File

@ -51,7 +51,7 @@ function EditableChip({
}} }}
/> />
} }
nonEditModeContent={<ChipComponent name={value} picture={picture} />} nonEditModeContent={<ChipComponent name={inputValue} picture={picture} />}
></EditableCellWrapper> ></EditableCellWrapper>
); );
} }

View File

@ -49,7 +49,7 @@ const StyledEditModeResultItem = styled.div`
`; `;
export type EditableRelationProps<RelationType, ChipComponentPropsType> = { export type EditableRelationProps<RelationType, ChipComponentPropsType> = {
relation: RelationType; relation?: RelationType;
searchPlaceholder: string; searchPlaceholder: string;
searchFilter: FilterType<People_Bool_Exp>; searchFilter: FilterType<People_Bool_Exp>;
changeHandler: (relation: RelationType) => void; changeHandler: (relation: RelationType) => void;

View File

@ -1,5 +1,4 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import DropdownButton from './DropdownButton';
import { import {
FilterType, FilterType,
SelectedFilterType, SelectedFilterType,
@ -147,8 +146,6 @@ function TableHeader<SortField, FilterProperties>({
availableSorts={availableSorts || []} availableSorts={availableSorts || []}
onSortSelect={sortSelect} onSortSelect={sortSelect}
/> />
<DropdownButton label="Settings" isActive={false}></DropdownButton>
</StyledFilters> </StyledFilters>
</StyledTableHeader> </StyledTableHeader>
{sorts.length + filters.length > 0 && ( {sorts.length + filters.length > 0 && (

View File

@ -18,6 +18,9 @@ export interface User {
workspace_member?: WorkspaceMember; workspace_member?: WorkspaceMember;
} }
export type PartialUser = Partial<User> &
Pick<User, 'id' | 'displayName' | 'email'>;
export const mapUser = (user: GraphqlQueryUser): User => { export const mapUser = (user: GraphqlQueryUser): User => {
const mappedUser = { const mappedUser = {
id: user.id, id: user.id,

View File

@ -5,7 +5,7 @@ import { Workspace } from '../../interfaces/workspace.interface';
import NavItem from './NavItem'; import NavItem from './NavItem';
import NavTitle from './NavTitle'; import NavTitle from './NavTitle';
import WorkspaceContainer from './WorkspaceContainer'; import WorkspaceContainer from './WorkspaceContainer';
import { FaRegUser, FaRegBuilding, FaBullseye } from 'react-icons/fa'; import { FaRegUser, FaRegBuilding } from 'react-icons/fa';
const NavbarContainer = styled.div` const NavbarContainer = styled.div`
display: flex; display: flex;
@ -55,17 +55,6 @@ function Navbar({ workspace }: OwnProps) {
}) })
} }
/> />
<NavItem
label="Opportunities"
to="/opportunities"
icon={<FaBullseye />}
active={
!!useMatch({
path: useResolvedPath('/opportunities').pathname,
end: true,
})
}
/>
</NavItemsContainer> </NavItemsContainer>
</NavbarContainer> </NavbarContainer>
</> </>

View File

@ -1,15 +1,117 @@
import { render, waitFor } from '@testing-library/react'; import { fireEvent, render, waitFor } from '@testing-library/react';
import { CompaniesDefault } from '../__stories__/Companies.stories'; import { CompaniesDefault } from '../__stories__/Companies.stories';
import { act } from 'react-dom/test-utils';
import {
GraphqlMutationCompany,
GraphqlQueryCompany,
} from '../../../interfaces/company.interface';
it('Checks the Companies page render', async () => { jest.mock('../../../apollo', () => {
const { getByTestId } = render(<CompaniesDefault />); const companyInterface = jest.requireActual(
'../../../interfaces/company.interface',
);
return {
apiClient: {
mutate: (arg: {
mutation: unknown;
variables: GraphqlMutationCompany;
}) => {
const gqlCompany = arg.variables as unknown as GraphqlQueryCompany;
return { data: companyInterface.mapCompany(gqlCompany) };
},
},
};
});
const title = getByTestId('top-bar-title'); it('Checks company name edit is updating data', async () => {
expect(title).toHaveTextContent('Companies'); const { getByText, getByDisplayValue } = render(<CompaniesDefault />);
await waitFor(() => { await waitFor(() => {
const row = getByTestId('row-id-0'); expect(getByText('Airbnb')).toBeDefined();
expect(row).toBeDefined(); });
act(() => {
fireEvent.click(getByText('Airbnb'));
});
await waitFor(() => {
expect(getByDisplayValue('Airbnb')).toBeInTheDocument();
});
act(() => {
const nameInput = getByDisplayValue('Airbnb');
if (!nameInput) {
throw new Error('nameInput is null');
}
fireEvent.change(nameInput, { target: { value: 'Airbnbb' } });
fireEvent.click(getByText('All Companies')); // Click outside
});
await waitFor(() => {
expect(getByText('Airbnbb')).toBeDefined();
});
});
it('Checks company url edit is updating data', async () => {
const { getByText, getByDisplayValue } = render(<CompaniesDefault />);
await waitFor(() => {
expect(getByText('airbnb.com')).toBeDefined();
});
act(() => {
fireEvent.click(getByText('airbnb.com'));
});
await waitFor(() => {
expect(getByDisplayValue('airbnb.com')).toBeInTheDocument();
});
act(() => {
const urlInput = getByDisplayValue('airbnb.com');
if (!urlInput) {
throw new Error('urlInput is null');
}
fireEvent.change(urlInput, { target: { value: 'airbnb.co' } });
fireEvent.click(getByText('All Companies')); // Click outside
});
await waitFor(() => {
expect(getByText('airbnb.co')).toBeInTheDocument();
});
});
it('Checks company address edit is updating data', async () => {
const { getByText, getByDisplayValue } = render(<CompaniesDefault />);
await waitFor(() => {
expect(getByText('17 rue de clignancourt')).toBeDefined();
});
act(() => {
fireEvent.click(getByText('17 rue de clignancourt'));
});
await waitFor(() => {
expect(getByDisplayValue('17 rue de clignancourt')).toBeInTheDocument();
});
act(() => {
const addressInput = getByDisplayValue('17 rue de clignancourt');
if (!addressInput) {
throw new Error('addressInput is null');
}
fireEvent.change(addressInput, {
target: { value: '21 rue de clignancourt' },
});
fireEvent.click(getByText('All Companies')); // Click outside
});
await waitFor(() => {
expect(getByText('21 rue de clignancourt')).toBeInTheDocument();
}); });
}); });

View File

@ -8,19 +8,19 @@ import ColumnHead from '../../components/table/ColumnHead';
import Checkbox from '../../components/form/Checkbox'; import Checkbox from '../../components/form/Checkbox';
import CompanyChip from '../../components/chips/CompanyChip'; import CompanyChip from '../../components/chips/CompanyChip';
import EditableText from '../../components/table/editable-cell/EditableText'; import EditableText from '../../components/table/editable-cell/EditableText';
import PipeChip from '../../components/chips/PipeChip';
import { import {
FaRegBuilding, FaRegBuilding,
FaCalendar, FaCalendar,
FaLink, FaLink,
FaMapPin, FaMapPin,
FaStream,
FaRegUser, FaRegUser,
FaUsers, FaUsers,
FaBuilding, FaBuilding,
FaUser,
} from 'react-icons/fa'; } from 'react-icons/fa';
import ClickableCell from '../../components/table/ClickableCell'; import PersonChip, {
import PersonChip from '../../components/chips/PersonChip'; PersonChipPropsType,
} from '../../components/chips/PersonChip';
import EditableChip from '../../components/table/editable-cell/EditableChip'; import EditableChip from '../../components/table/editable-cell/EditableChip';
import { import {
FilterType, FilterType,
@ -29,8 +29,15 @@ import {
import { import {
Companies_Bool_Exp, Companies_Bool_Exp,
Companies_Order_By, Companies_Order_By,
Users_Bool_Exp,
} from '../../generated/graphql'; } from '../../generated/graphql';
import { SEARCH_COMPANY_QUERY } from '../../services/search/search'; import {
SEARCH_COMPANY_QUERY,
SEARCH_USER_QUERY,
} from '../../services/search/search';
import EditableDate from '../../components/table/editable-cell/EditableDate';
import EditableRelation from '../../components/table/editable-cell/EditableRelation';
import { GraphqlQueryUser, PartialUser } from '../../interfaces/user.interface';
export const availableSorts = [ export const availableSorts = [
{ {
@ -151,7 +158,7 @@ export const companiesColumns = [
changeHandler={(value: string) => { changeHandler={(value: string) => {
const company = props.row.original; const company = props.row.original;
company.name = value; company.name = value;
updateCompany(company).catch((error) => console.error(error)); updateCompany(company);
}} }}
ChipComponent={CompanyChip} ChipComponent={CompanyChip}
/> />
@ -165,7 +172,7 @@ export const companiesColumns = [
changeHandler={(value) => { changeHandler={(value) => {
const company = props.row.original; const company = props.row.original;
company.employees = parseInt(value); company.employees = parseInt(value);
updateCompany(company).catch((error) => console.error(error)); updateCompany(company);
}} }}
/> />
), ),
@ -178,7 +185,7 @@ export const companiesColumns = [
changeHandler={(value) => { changeHandler={(value) => {
const company = props.row.original; const company = props.row.original;
company.domain_name = value; company.domain_name = value;
updateCompany(company).catch((error) => console.error(error)); updateCompany(company);
}} }}
/> />
), ),
@ -191,33 +198,22 @@ export const companiesColumns = [
changeHandler={(value) => { changeHandler={(value) => {
const company = props.row.original; const company = props.row.original;
company.address = value; company.address = value;
updateCompany(company).catch((error) => console.error(error)); updateCompany(company);
}} }}
/> />
), ),
}), }),
columnHelper.accessor('opportunities', {
header: () => (
<ColumnHead viewName="Opportunities" viewIcon={<FaStream />} />
),
cell: (props) => (
<ClickableCell href="#">
{props.row.original.opportunities.map((opportunity) => (
<PipeChip opportunity={opportunity} />
))}
</ClickableCell>
),
}),
columnHelper.accessor('creationDate', { columnHelper.accessor('creationDate', {
header: () => <ColumnHead viewName="Creation" viewIcon={<FaCalendar />} />, header: () => <ColumnHead viewName="Creation" viewIcon={<FaCalendar />} />,
cell: (props) => ( cell: (props) => (
<ClickableCell href="#"> <EditableDate
{new Intl.DateTimeFormat(undefined, { value={props.row.original.creationDate}
month: 'short', changeHandler={(value: Date) => {
day: 'numeric', const company = props.row.original;
year: 'numeric', company.creationDate = value;
}).format(props.row.original.creationDate)} updateCompany(company);
</ClickableCell> }}
/>
), ),
}), }),
columnHelper.accessor('accountOwner', { columnHelper.accessor('accountOwner', {
@ -225,13 +221,54 @@ export const companiesColumns = [
<ColumnHead viewName="Account Owner" viewIcon={<FaRegUser />} /> <ColumnHead viewName="Account Owner" viewIcon={<FaRegUser />} />
), ),
cell: (props) => ( cell: (props) => (
<ClickableCell href="#"> <EditableRelation<PartialUser, PersonChipPropsType>
<> relation={props.row.original.accountOwner}
{props.row.original.accountOwner && ( searchPlaceholder="Account Owner"
<PersonChip name={props.row.original.accountOwner?.displayName} /> ChipComponent={PersonChip}
)} chipComponentPropsMapper={(
</> accountOwner: PartialUser,
</ClickableCell> ): PersonChipPropsType => {
return {
name: accountOwner.displayName,
};
}}
changeHandler={(relation: PartialUser) => {
const company = props.row.original;
if (company.accountOwner) {
company.accountOwner.id = relation.id;
} else {
company.accountOwner = {
id: relation.id,
email: relation.email,
displayName: relation.displayName,
};
}
updateCompany(company);
}}
searchFilter={
{
key: 'account_owner_name',
label: 'Account Owner',
icon: <FaUser />,
whereTemplate: () => {
return {};
},
searchQuery: SEARCH_USER_QUERY,
searchTemplate: (searchInput: string) => ({
displayName: { _ilike: `%${searchInput}%` },
}),
searchResultMapper: (accountOwner: GraphqlQueryUser) => ({
displayValue: accountOwner.displayName,
value: {
id: accountOwner.id,
email: accountOwner.email,
displayName: accountOwner.displayName,
},
}),
operands: [],
} satisfies FilterType<Users_Bool_Exp>
}
/>
), ),
}), }),
]; ];

View File

@ -5,19 +5,16 @@ import {
FaRegUser, FaRegUser,
FaMapPin, FaMapPin,
FaPhone, FaPhone,
FaStream,
FaUser, FaUser,
FaBuilding, FaBuilding,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { createColumnHelper } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table';
import ClickableCell from '../../components/table/ClickableCell';
import ColumnHead from '../../components/table/ColumnHead'; import ColumnHead from '../../components/table/ColumnHead';
import Checkbox from '../../components/form/Checkbox'; import Checkbox from '../../components/form/Checkbox';
import CompanyChip, { import CompanyChip, {
CompanyChipPropsType, CompanyChipPropsType,
} from '../../components/chips/CompanyChip'; } from '../../components/chips/CompanyChip';
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface'; import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface';
import PipeChip from '../../components/chips/PipeChip';
import EditableText from '../../components/table/editable-cell/EditableText'; import EditableText from '../../components/table/editable-cell/EditableText';
import { import {
FilterType, FilterType,
@ -361,14 +358,6 @@ export const peopleColumns = [
/> />
), ),
}), }),
columnHelper.accessor('pipe', {
header: () => <ColumnHead viewName="Pipe" viewIcon={<FaStream />} />,
cell: (props) => (
<ClickableCell href="#">
<PipeChip opportunity={props.row.original.pipe} />
</ClickableCell>
),
}),
columnHelper.accessor('city', { columnHelper.accessor('city', {
header: () => <ColumnHead viewName="City" viewIcon={<FaMapPin />} />, header: () => <ColumnHead viewName="City" viewIcon={<FaMapPin />} />,
cell: (props) => ( cell: (props) => (

View File

@ -8,6 +8,7 @@ export const UPDATE_COMPANY = gql`
$name: String $name: String
$domain_name: String $domain_name: String
$account_owner_id: uuid $account_owner_id: uuid
$created_at: timestamptz
$address: String $address: String
$employees: Int $employees: Int
) { ) {
@ -19,6 +20,7 @@ export const UPDATE_COMPANY = gql`
domain_name: $domain_name domain_name: $domain_name
employees: $employees employees: $employees
name: $name name: $name
created_at: $created_at
} }
) { ) {
affected_rows affected_rows

View File

@ -18,6 +18,16 @@ export const SEARCH_PEOPLE_QUERY = gql`
} }
`; `;
export const SEARCH_USER_QUERY = gql`
query SearchQuery($where: users_bool_exp, $limit: Int) {
searchResults: users(where: $where, limit: $limit) {
id
email
displayName
}
}
`;
const EMPTY_QUERY = gql` const EMPTY_QUERY = gql`
query EmptyQuery { query EmptyQuery {
_ _