Add new line on Table Views (#110)

Add addition on Companies table
This commit is contained in:
Charles Bochet
2023-05-07 23:41:22 +02:00
committed by GitHub
parent 8c7815af79
commit 50a4a97145
15 changed files with 329 additions and 154 deletions

View File

@ -24,6 +24,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.8.0", "react-icons": "^4.8.0",
"react-router-dom": "^6.4.4", "react-router-dom": "^6.4.4",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@ -45,6 +46,7 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/react-datepicker": "^4.11.2", "@types/react-datepicker": "^4.11.2",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"babel-plugin-named-exports-order": "^0.0.2", "babel-plugin-named-exports-order": "^0.0.2",
"eslint": "^8.28.0", "eslint": "^8.28.0",
@ -8883,6 +8885,12 @@
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
"dev": true "dev": true
}, },
"node_modules/@types/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
"dev": true
},
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.4", "version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
@ -28338,7 +28346,6 @@
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"dev": true,
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }

View File

@ -19,6 +19,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.8.0", "react-icons": "^4.8.0",
"react-router-dom": "^6.4.4", "react-router-dom": "^6.4.4",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
@ -103,6 +104,7 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/react-datepicker": "^4.11.2", "@types/react-datepicker": "^4.11.2",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"babel-plugin-named-exports-order": "^0.0.2", "babel-plugin-named-exports-order": "^0.0.2",
"eslint": "^8.28.0", "eslint": "^8.28.0",

View File

@ -99,7 +99,7 @@ function Table<TData extends { id: string }, SortField, FilterProperies>({
onFiltersUpdate, onFiltersUpdate,
onFilterSearch, onFilterSearch,
}: OwnProps<TData, SortField, FilterProperies>) { }: OwnProps<TData, SortField, FilterProperies>) {
const table = useReactTable({ const table = useReactTable<TData>({
data, data,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),

View File

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

View File

@ -13,7 +13,7 @@ export type Company = {
employees: number; employees: number;
address: string; address: string;
opportunities: Opportunity[]; opportunities: Opportunity[];
accountOwner?: User; accountOwner?: User | null;
creationDate: Date; creationDate: Date;
}; };
@ -39,11 +39,14 @@ export type GraphqlMutationCompany = {
employees: number; employees: number;
address: string; address: string;
created_at: string; created_at: string;
account_owner?: GraphqlQueryUser | null;
}; };
export const mapCompany = (company: GraphqlQueryCompany): Company => ({ export const mapCompany = (company: GraphqlQueryCompany): Company => ({
...company, id: company.id,
employees: company.employees,
name: company.name, name: company.name,
address: company.address,
domain_name: company.domain_name, domain_name: company.domain_name,
accountOwner: company.account_owner accountOwner: company.account_owner
? { ? {
@ -57,9 +60,19 @@ export const mapCompany = (company: GraphqlQueryCompany): Company => ({
}); });
export const mapGqlCompany = (company: Company): GraphqlMutationCompany => ({ export const mapGqlCompany = (company: Company): GraphqlMutationCompany => ({
...company,
name: company.name, name: company.name,
domain_name: company.domain_name, domain_name: company.domain_name,
created_at: company.creationDate.toUTCString(), created_at: company.creationDate.toUTCString(),
account_owner_id: company.accountOwner?.id, account_owner_id: company.accountOwner?.id,
address: company.address,
employees: company.employees,
id: company.id,
account_owner: company.accountOwner
? {
id: company.accountOwner?.id,
email: company.accountOwner?.email,
displayName: company.accountOwner?.displayName,
__typename: 'users',
}
: null,
}); });

View File

@ -6,6 +6,7 @@ type OwnProps = {
children: JSX.Element; children: JSX.Element;
title: string; title: string;
icon: ReactNode; icon: ReactNode;
onAddButtonClick?: () => void;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -34,10 +35,15 @@ const ContentSubContainer = styled.div`
flex: 1; flex: 1;
`; `;
function FullWidthContainer({ children, title, icon }: OwnProps) { function FullWidthContainer({
children,
title,
icon,
onAddButtonClick,
}: OwnProps) {
return ( return (
<StyledContainer> <StyledContainer>
<TopBar title={title} icon={icon} /> <TopBar title={title} icon={icon} onAddButtonClick={onAddButtonClick} />
<ContentContainer> <ContentContainer>
<ContentSubContainer>{children}</ContentSubContainer> <ContentSubContainer>{children}</ContentSubContainer>
</ContentContainer> </ContentContainer>

View File

@ -1,5 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { TbPlus } from 'react-icons/tb';
const TopBarContainer = styled.div` const TopBarContainer = styled.div`
display: flex; display: flex;
@ -17,19 +18,44 @@ const TitleContainer = styled.div`
font-family: 'Inter'; font-family: 'Inter';
margin-left: 4px; margin-left: 4px;
font-size: 14px; font-size: 14px;
display: flex;
width: 100%;
`;
const AddButtonContainer = styled.div`
display: flex;
justify-self: flex-end;
border: 1px solid ${(props) => props.theme.primaryBorder};
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
border-radius: 4px;
color: ${(props) => props.theme.text60};
cursor: pointer;
margin-right: ${(props) => props.theme.spacing(1)};
`; `;
type OwnProps = { type OwnProps = {
title: string; title: string;
icon: ReactNode; icon: ReactNode;
onAddButtonClick?: () => void;
}; };
function TopBar({ title, icon }: OwnProps) { function TopBar({ title, icon, onAddButtonClick }: OwnProps) {
return ( return (
<> <>
<TopBarContainer> <TopBarContainer>
{icon} {icon}
<TitleContainer data-testid="top-bar-title">{title}</TitleContainer> <TitleContainer data-testid="top-bar-title">{title}</TitleContainer>
{onAddButtonClick && (
<AddButtonContainer
data-testid="add-button"
onClick={onAddButtonClick}
>
<TbPlus />
</AddButtonContainer>
)}
</TopBarContainer> </TopBarContainer>
</> </>
); );

View File

@ -1,16 +1,18 @@
import { FaRegBuilding, FaList } from 'react-icons/fa'; import { FaRegBuilding, FaList } from 'react-icons/fa';
import WithTopBarContainer from '../../layout/containers/WithTopBarContainer'; import WithTopBarContainer from '../../layout/containers/WithTopBarContainer';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState, useCallback } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { import {
CompaniesSelectedSortType, CompaniesSelectedSortType,
defaultOrderBy, defaultOrderBy,
insertCompany,
useCompaniesQuery, useCompaniesQuery,
} from '../../services/companies'; } from '../../services/companies';
import Table from '../../components/table/Table'; import Table from '../../components/table/Table';
import { mapCompany } from '../../interfaces/company.interface'; import { Company, mapCompany } from '../../interfaces/company.interface';
import { import {
companiesColumns, useCompaniesColumns,
availableFilters, availableFilters,
availableSorts, availableSorts,
} from './companies-table'; } from './companies-table';
@ -33,6 +35,7 @@ const StyledCompaniesContainer = styled.div`
function Companies() { function Companies() {
const [orderBy, setOrderBy] = useState<Companies_Order_By[]>(defaultOrderBy); const [orderBy, setOrderBy] = useState<Companies_Order_By[]>(defaultOrderBy);
const [where, setWhere] = useState<Companies_Bool_Exp>({}); const [where, setWhere] = useState<Companies_Bool_Exp>({});
const [internalData, setInternalData] = useState<Array<Company>>([]);
const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch(); const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch();
@ -47,13 +50,43 @@ function Companies() {
[], [],
); );
const { data } = useCompaniesQuery(orderBy, where); const { data, loading, refetch } = useCompaniesQuery(orderBy, where);
useEffect(() => {
if (!loading) {
if (data) {
setInternalData(data.companies.map(mapCompany));
}
}
}, [loading, setInternalData, data]);
const addEmptyRow = useCallback(() => {
const newCompany: Company = {
id: uuidv4(),
name: '',
domain_name: '',
employees: 0,
address: '',
opportunities: [],
creationDate: new Date(),
accountOwner: null,
};
insertCompany(newCompany);
setInternalData([newCompany, ...internalData]);
refetch();
}, [internalData, setInternalData, refetch]);
const companiesColumns = useCompaniesColumns();
return ( return (
<WithTopBarContainer title="Companies" icon={<FaRegBuilding />}> <WithTopBarContainer
title="Companies"
icon={<FaRegBuilding />}
onAddButtonClick={addEmptyRow}
>
<StyledCompaniesContainer> <StyledCompaniesContainer>
<Table <Table
data={data ? data.companies.map(mapCompany) : []} data={internalData}
columns={companiesColumns} columns={companiesColumns}
viewName="All Companies" viewName="All Companies"
viewIcon={<FaList />} viewIcon={<FaList />}

View File

@ -18,7 +18,7 @@ const mocks = [
request: { request: {
query: GET_COMPANIES, query: GET_COMPANIES,
variables: { variables: {
orderBy: [{ name: 'asc' }], orderBy: [{ created_at: 'desc' }],
where: {}, where: {},
}, },
}, },

View File

@ -115,3 +115,24 @@ it('Checks company address edit is updating data', async () => {
expect(getByText('21 rue de clignancourt')).toBeInTheDocument(); expect(getByText('21 rue de clignancourt')).toBeInTheDocument();
}); });
}); });
it('Checks insert data is appending a new line', async () => {
const { getByText, getByTestId, container } = render(<CompaniesDefault />);
await waitFor(() => {
expect(getByText('Airbnb')).toBeDefined();
});
const tableRows = container.querySelectorAll<HTMLElement>('table tbody tr');
expect(tableRows.length).toBe(6);
act(() => {
fireEvent.click(getByTestId('add-button'));
});
await waitFor(() => {
const tableRows = container.querySelectorAll<HTMLElement>('table tbody tr');
expect(tableRows.length).toBe(7);
});
});

View File

@ -38,6 +38,7 @@ import {
import EditableDate from '../../components/table/editable-cell/EditableDate'; import EditableDate from '../../components/table/editable-cell/EditableDate';
import EditableRelation from '../../components/table/editable-cell/EditableRelation'; import EditableRelation from '../../components/table/editable-cell/EditableRelation';
import { GraphqlQueryUser, PartialUser } from '../../interfaces/user.interface'; import { GraphqlQueryUser, PartialUser } from '../../interfaces/user.interface';
import { useMemo } from 'react';
export const availableSorts = [ export const availableSorts = [
{ {
@ -136,139 +137,150 @@ export const availableFilters = [
] satisfies Array<FilterType<Companies_Bool_Exp>>; ] satisfies Array<FilterType<Companies_Bool_Exp>>;
const columnHelper = createColumnHelper<Company>(); const columnHelper = createColumnHelper<Company>();
export const companiesColumns = [
columnHelper.accessor('id', { export const useCompaniesColumns = () => {
header: () => ( return useMemo(() => {
<Checkbox id="company-select-all" name="company-select-all" /> return [
), columnHelper.accessor('id', {
cell: (props) => ( header: () => (
<Checkbox <Checkbox id="company-select-all" name="company-select-all" />
id={`company-selected-${props.row.original.id}`} ),
name={`company-selected-${props.row.original.id}`} cell: (props) => (
/> <Checkbox
), id={`company-selected-${props.row.original.id}`}
}), name={`company-selected-${props.row.original.id}`}
columnHelper.accessor('name', { />
header: () => <ColumnHead viewName="Name" viewIcon={<FaRegBuilding />} />, ),
cell: (props) => ( }),
<EditableChip columnHelper.accessor('name', {
value={props.row.original.name} header: () => (
placeholder="Name" <ColumnHead viewName="Name" viewIcon={<FaRegBuilding />} />
picture={`https://www.google.com/s2/favicons?domain=${props.row.original.domain_name}&sz=256`} ),
changeHandler={(value: string) => { cell: (props) => (
const company = props.row.original; <EditableChip
company.name = value; value={props.row.original.name}
updateCompany(company); placeholder="Name"
}} picture={`https://www.google.com/s2/favicons?domain=${props.row.original.domain_name}&sz=256`}
ChipComponent={CompanyChip} changeHandler={(value: string) => {
/> const company = props.row.original;
), company.name = value;
}), updateCompany(company);
columnHelper.accessor('employees', { }}
header: () => <ColumnHead viewName="Employees" viewIcon={<FaUsers />} />, ChipComponent={CompanyChip}
cell: (props) => ( />
<EditableText ),
content={props.row.original.employees.toFixed(0)} }),
changeHandler={(value) => { columnHelper.accessor('employees', {
const company = props.row.original; header: () => (
company.employees = parseInt(value); <ColumnHead viewName="Employees" viewIcon={<FaUsers />} />
updateCompany(company); ),
}} cell: (props) => (
/> <EditableText
), content={props.row.original.employees.toFixed(0)}
}), changeHandler={(value) => {
columnHelper.accessor('domain_name', { const company = props.row.original;
header: () => <ColumnHead viewName="URL" viewIcon={<FaLink />} />, company.employees = parseInt(value);
cell: (props) => ( updateCompany(company);
<EditableText }}
content={props.row.original.domain_name} />
changeHandler={(value) => { ),
const company = props.row.original; }),
company.domain_name = value; columnHelper.accessor('domain_name', {
updateCompany(company); header: () => <ColumnHead viewName="URL" viewIcon={<FaLink />} />,
}} cell: (props) => (
/> <EditableText
), content={props.row.original.domain_name}
}), changeHandler={(value) => {
columnHelper.accessor('address', { const company = props.row.original;
header: () => <ColumnHead viewName="Address" viewIcon={<FaMapPin />} />, company.domain_name = value;
cell: (props) => ( updateCompany(company);
<EditableText }}
content={props.row.original.address} />
changeHandler={(value) => { ),
const company = props.row.original; }),
company.address = value; columnHelper.accessor('address', {
updateCompany(company); header: () => <ColumnHead viewName="Address" viewIcon={<FaMapPin />} />,
}} cell: (props) => (
/> <EditableText
), content={props.row.original.address}
}), changeHandler={(value) => {
columnHelper.accessor('creationDate', { const company = props.row.original;
header: () => <ColumnHead viewName="Creation" viewIcon={<FaCalendar />} />, company.address = value;
cell: (props) => ( updateCompany(company);
<EditableDate }}
value={props.row.original.creationDate} />
changeHandler={(value: Date) => { ),
const company = props.row.original; }),
company.creationDate = value; columnHelper.accessor('creationDate', {
updateCompany(company); header: () => (
}} <ColumnHead viewName="Creation" viewIcon={<FaCalendar />} />
/> ),
), cell: (props) => (
}), <EditableDate
columnHelper.accessor('accountOwner', { value={props.row.original.creationDate}
header: () => ( changeHandler={(value: Date) => {
<ColumnHead viewName="Account Owner" viewIcon={<FaRegUser />} /> const company = props.row.original;
), company.creationDate = value;
cell: (props) => ( updateCompany(company);
<EditableRelation<PartialUser, PersonChipPropsType> }}
relation={props.row.original.accountOwner} />
searchPlaceholder="Account Owner" ),
ChipComponent={PersonChip} }),
chipComponentPropsMapper={( columnHelper.accessor('accountOwner', {
accountOwner: PartialUser, header: () => (
): PersonChipPropsType => { <ColumnHead viewName="Account Owner" viewIcon={<FaRegUser />} />
return { ),
name: accountOwner.displayName, cell: (props) => (
}; <EditableRelation<PartialUser, PersonChipPropsType>
}} relation={props.row.original.accountOwner}
changeHandler={(relation: PartialUser) => { searchPlaceholder="Account Owner"
const company = props.row.original; ChipComponent={PersonChip}
if (company.accountOwner) { chipComponentPropsMapper={(
company.accountOwner.id = relation.id; accountOwner: PartialUser,
} else { ): PersonChipPropsType => {
company.accountOwner = { return {
id: relation.id, name: accountOwner.displayName,
email: relation.email, };
displayName: relation.displayName, }}
}; changeHandler={(relation: PartialUser) => {
} const company = props.row.original;
updateCompany(company); if (company.accountOwner) {
}} company.accountOwner.id = relation.id;
searchFilter={ } else {
{ company.accountOwner = {
key: 'account_owner_name', id: relation.id,
label: 'Account Owner', email: relation.email,
icon: <FaUser />, displayName: relation.displayName,
whereTemplate: () => { };
return {}; }
}, updateCompany(company);
searchQuery: SEARCH_USER_QUERY, }}
searchTemplate: (searchInput: string) => ({ searchFilter={
displayName: { _ilike: `%${searchInput}%` }, {
}), key: 'account_owner_name',
searchResultMapper: (accountOwner: GraphqlQueryUser) => ({ label: 'Account Owner',
displayValue: accountOwner.displayName, icon: <FaUser />,
value: { whereTemplate: () => {
id: accountOwner.id, return {};
email: accountOwner.email, },
displayName: accountOwner.displayName, searchQuery: SEARCH_USER_QUERY,
}, searchTemplate: (searchInput: string) => ({
}), displayName: { _ilike: `%${searchInput}%` },
operands: [], }),
} satisfies FilterType<Users_Bool_Exp> searchResultMapper: (accountOwner: GraphqlQueryUser) => ({
} displayValue: accountOwner.displayName,
/> value: {
), id: accountOwner.id,
}), email: accountOwner.email,
]; displayName: accountOwner.displayName,
},
}),
operands: [],
} satisfies FilterType<Users_Bool_Exp>
}
/>
),
}),
];
}, []);
};

View File

@ -44,10 +44,18 @@ function People() {
[], [],
); );
const addEmptyRow = useCallback(() => {
console.log('add row');
}, []);
const { data } = usePeopleQuery(orderBy, where); const { data } = usePeopleQuery(orderBy, where);
return ( return (
<WithTopBarContainer title="People" icon={<FaRegUser />}> <WithTopBarContainer
title="People"
icon={<FaRegUser />}
onAddButtonClick={addEmptyRow}
>
<StyledPeopleContainer> <StyledPeopleContainer>
{ {
<Table <Table

View File

@ -41,6 +41,6 @@ export function useCompaniesQuery(
export const defaultOrderBy: Companies_Order_By[] = [ export const defaultOrderBy: Companies_Order_By[] = [
{ {
name: Order_By.Asc, created_at: Order_By.Desc,
}, },
]; ];

View File

@ -41,6 +41,40 @@ export const UPDATE_COMPANY = gql`
} }
`; `;
export const INSERT_COMPANY = gql`
mutation InsertCompany(
$id: uuid
$name: String
$domain_name: String
$account_owner_id: uuid
$created_at: timestamptz
$address: String
$employees: Int
) {
insert_companies(
objects: {
id: $id
name: $name
domain_name: $domain_name
account_owner_id: $account_owner_id
created_at: $created_at
address: $address
employees: $employees
}
) {
affected_rows
returning {
address
created_at
domain_name
employees
id
name
}
}
}
`;
export async function updateCompany( export async function updateCompany(
company: Company, company: Company,
): Promise<FetchResult<Company>> { ): Promise<FetchResult<Company>> {
@ -50,3 +84,14 @@ export async function updateCompany(
}); });
return result; return result;
} }
export async function insertCompany(
company: Company,
): Promise<FetchResult<Company>> {
const result = await apiClient.mutate({
mutation: INSERT_COMPANY,
variables: mapGqlCompany(company),
});
return result;
}

View File

@ -20,6 +20,8 @@ insert_permissions:
check: check:
workspace_id: workspace_id:
_eq: x-hasura-workspace-id _eq: x-hasura-workspace-id
set:
workspace_id: x-hasura-Workspace-Id
columns: columns:
- id - id
- workspace_id - workspace_id