From 50a4a971453ec534d2d647c394cfa903d2b3a6ba Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sun, 7 May 2023 23:41:22 +0200 Subject: [PATCH] Add new line on Table Views (#110) Add addition on Companies table --- front/package-lock.json | 9 +- front/package.json | 2 + front/src/components/table/Table.tsx | 2 +- .../table/editable-cell/EditableRelation.tsx | 2 +- front/src/interfaces/company.interface.ts | 19 +- .../layout/containers/WithTopBarContainer.tsx | 10 +- front/src/layout/top-bar/TopBar.tsx | 28 +- front/src/pages/companies/Companies.tsx | 45 ++- .../__stories__/Companies.stories.tsx | 2 +- .../companies/__tests__/Companies.test.tsx | 21 ++ front/src/pages/companies/companies-table.tsx | 284 +++++++++--------- front/src/pages/people/People.tsx | 10 +- front/src/services/companies/select.ts | 2 +- front/src/services/companies/update.ts | 45 +++ .../default/tables/public_companies.yaml | 2 + 15 files changed, 329 insertions(+), 154 deletions(-) diff --git a/front/package-lock.json b/front/package-lock.json index 439effa4a..0207aa0c7 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -24,6 +24,7 @@ "react-dom": "^18.2.0", "react-icons": "^4.8.0", "react-router-dom": "^6.4.4", + "uuid": "^9.0.0", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -45,6 +46,7 @@ "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/react-datepicker": "^4.11.2", + "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.45.0", "babel-plugin-named-exports-order": "^0.0.2", "eslint": "^8.28.0", @@ -8883,6 +8885,12 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "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": { "version": "8.5.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", @@ -28338,7 +28346,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true, "bin": { "uuid": "dist/bin/uuid" } diff --git a/front/package.json b/front/package.json index d4193cdb9..5a33bdc65 100644 --- a/front/package.json +++ b/front/package.json @@ -19,6 +19,7 @@ "react-dom": "^18.2.0", "react-icons": "^4.8.0", "react-router-dom": "^6.4.4", + "uuid": "^9.0.0", "web-vitals": "^2.1.4" }, "scripts": { @@ -103,6 +104,7 @@ "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/react-datepicker": "^4.11.2", + "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.45.0", "babel-plugin-named-exports-order": "^0.0.2", "eslint": "^8.28.0", diff --git a/front/src/components/table/Table.tsx b/front/src/components/table/Table.tsx index 2bb6a5ad4..04e95b097 100644 --- a/front/src/components/table/Table.tsx +++ b/front/src/components/table/Table.tsx @@ -99,7 +99,7 @@ function Table({ onFiltersUpdate, onFilterSearch, }: OwnProps) { - const table = useReactTable({ + const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), diff --git a/front/src/components/table/editable-cell/EditableRelation.tsx b/front/src/components/table/editable-cell/EditableRelation.tsx index 794efd8f0..23540b468 100644 --- a/front/src/components/table/editable-cell/EditableRelation.tsx +++ b/front/src/components/table/editable-cell/EditableRelation.tsx @@ -49,7 +49,7 @@ const StyledEditModeResultItem = styled.div` `; export type EditableRelationProps = { - relation?: RelationType; + relation?: RelationType | null; searchPlaceholder: string; searchFilter: FilterType; changeHandler: (relation: RelationType) => void; diff --git a/front/src/interfaces/company.interface.ts b/front/src/interfaces/company.interface.ts index c19f51e69..631d2b66a 100644 --- a/front/src/interfaces/company.interface.ts +++ b/front/src/interfaces/company.interface.ts @@ -13,7 +13,7 @@ export type Company = { employees: number; address: string; opportunities: Opportunity[]; - accountOwner?: User; + accountOwner?: User | null; creationDate: Date; }; @@ -39,11 +39,14 @@ export type GraphqlMutationCompany = { employees: number; address: string; created_at: string; + account_owner?: GraphqlQueryUser | null; }; export const mapCompany = (company: GraphqlQueryCompany): Company => ({ - ...company, + id: company.id, + employees: company.employees, name: company.name, + address: company.address, domain_name: company.domain_name, accountOwner: company.account_owner ? { @@ -57,9 +60,19 @@ export const mapCompany = (company: GraphqlQueryCompany): Company => ({ }); export const mapGqlCompany = (company: Company): GraphqlMutationCompany => ({ - ...company, name: company.name, domain_name: company.domain_name, created_at: company.creationDate.toUTCString(), 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, }); diff --git a/front/src/layout/containers/WithTopBarContainer.tsx b/front/src/layout/containers/WithTopBarContainer.tsx index 2a545e1d1..cc7d0ec1e 100644 --- a/front/src/layout/containers/WithTopBarContainer.tsx +++ b/front/src/layout/containers/WithTopBarContainer.tsx @@ -6,6 +6,7 @@ type OwnProps = { children: JSX.Element; title: string; icon: ReactNode; + onAddButtonClick?: () => void; }; const StyledContainer = styled.div` @@ -34,10 +35,15 @@ const ContentSubContainer = styled.div` flex: 1; `; -function FullWidthContainer({ children, title, icon }: OwnProps) { +function FullWidthContainer({ + children, + title, + icon, + onAddButtonClick, +}: OwnProps) { return ( - + {children} diff --git a/front/src/layout/top-bar/TopBar.tsx b/front/src/layout/top-bar/TopBar.tsx index 6d7055b09..1e9dfa5cb 100644 --- a/front/src/layout/top-bar/TopBar.tsx +++ b/front/src/layout/top-bar/TopBar.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; import { ReactNode } from 'react'; +import { TbPlus } from 'react-icons/tb'; const TopBarContainer = styled.div` display: flex; @@ -17,19 +18,44 @@ const TitleContainer = styled.div` font-family: 'Inter'; margin-left: 4px; 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 = { title: string; icon: ReactNode; + onAddButtonClick?: () => void; }; -function TopBar({ title, icon }: OwnProps) { +function TopBar({ title, icon, onAddButtonClick }: OwnProps) { return ( <> {icon} {title} + {onAddButtonClick && ( + + + + )} ); diff --git a/front/src/pages/companies/Companies.tsx b/front/src/pages/companies/Companies.tsx index 9d0b24b1d..23bb01d88 100644 --- a/front/src/pages/companies/Companies.tsx +++ b/front/src/pages/companies/Companies.tsx @@ -1,16 +1,18 @@ import { FaRegBuilding, FaList } from 'react-icons/fa'; import WithTopBarContainer from '../../layout/containers/WithTopBarContainer'; import styled from '@emotion/styled'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { CompaniesSelectedSortType, defaultOrderBy, + insertCompany, useCompaniesQuery, } from '../../services/companies'; import Table from '../../components/table/Table'; -import { mapCompany } from '../../interfaces/company.interface'; +import { Company, mapCompany } from '../../interfaces/company.interface'; import { - companiesColumns, + useCompaniesColumns, availableFilters, availableSorts, } from './companies-table'; @@ -33,6 +35,7 @@ const StyledCompaniesContainer = styled.div` function Companies() { const [orderBy, setOrderBy] = useState(defaultOrderBy); const [where, setWhere] = useState({}); + const [internalData, setInternalData] = useState>([]); 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 ( - }> + } + onAddButtonClick={addEmptyRow} + > } diff --git a/front/src/pages/companies/__stories__/Companies.stories.tsx b/front/src/pages/companies/__stories__/Companies.stories.tsx index 2dcd9ec11..7c106c727 100644 --- a/front/src/pages/companies/__stories__/Companies.stories.tsx +++ b/front/src/pages/companies/__stories__/Companies.stories.tsx @@ -18,7 +18,7 @@ const mocks = [ request: { query: GET_COMPANIES, variables: { - orderBy: [{ name: 'asc' }], + orderBy: [{ created_at: 'desc' }], where: {}, }, }, diff --git a/front/src/pages/companies/__tests__/Companies.test.tsx b/front/src/pages/companies/__tests__/Companies.test.tsx index 0a574fa42..8ecce15da 100644 --- a/front/src/pages/companies/__tests__/Companies.test.tsx +++ b/front/src/pages/companies/__tests__/Companies.test.tsx @@ -115,3 +115,24 @@ it('Checks company address edit is updating data', async () => { expect(getByText('21 rue de clignancourt')).toBeInTheDocument(); }); }); + +it('Checks insert data is appending a new line', async () => { + const { getByText, getByTestId, container } = render(); + + await waitFor(() => { + expect(getByText('Airbnb')).toBeDefined(); + }); + const tableRows = container.querySelectorAll('table tbody tr'); + + expect(tableRows.length).toBe(6); + + act(() => { + fireEvent.click(getByTestId('add-button')); + }); + + await waitFor(() => { + const tableRows = container.querySelectorAll('table tbody tr'); + + expect(tableRows.length).toBe(7); + }); +}); diff --git a/front/src/pages/companies/companies-table.tsx b/front/src/pages/companies/companies-table.tsx index 888760320..dc454f2c3 100644 --- a/front/src/pages/companies/companies-table.tsx +++ b/front/src/pages/companies/companies-table.tsx @@ -38,6 +38,7 @@ import { import EditableDate from '../../components/table/editable-cell/EditableDate'; import EditableRelation from '../../components/table/editable-cell/EditableRelation'; import { GraphqlQueryUser, PartialUser } from '../../interfaces/user.interface'; +import { useMemo } from 'react'; export const availableSorts = [ { @@ -136,139 +137,150 @@ export const availableFilters = [ ] satisfies Array>; const columnHelper = createColumnHelper(); -export const companiesColumns = [ - columnHelper.accessor('id', { - header: () => ( - - ), - cell: (props) => ( - - ), - }), - columnHelper.accessor('name', { - header: () => } />, - cell: (props) => ( - { - const company = props.row.original; - company.name = value; - updateCompany(company); - }} - ChipComponent={CompanyChip} - /> - ), - }), - columnHelper.accessor('employees', { - header: () => } />, - cell: (props) => ( - { - const company = props.row.original; - company.employees = parseInt(value); - updateCompany(company); - }} - /> - ), - }), - columnHelper.accessor('domain_name', { - header: () => } />, - cell: (props) => ( - { - const company = props.row.original; - company.domain_name = value; - updateCompany(company); - }} - /> - ), - }), - columnHelper.accessor('address', { - header: () => } />, - cell: (props) => ( - { - const company = props.row.original; - company.address = value; - updateCompany(company); - }} - /> - ), - }), - columnHelper.accessor('creationDate', { - header: () => } />, - cell: (props) => ( - { - const company = props.row.original; - company.creationDate = value; - updateCompany(company); - }} - /> - ), - }), - columnHelper.accessor('accountOwner', { - header: () => ( - } /> - ), - cell: (props) => ( - - relation={props.row.original.accountOwner} - searchPlaceholder="Account Owner" - ChipComponent={PersonChip} - chipComponentPropsMapper={( - accountOwner: PartialUser, - ): 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: , - 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 - } - /> - ), - }), -]; + +export const useCompaniesColumns = () => { + return useMemo(() => { + return [ + columnHelper.accessor('id', { + header: () => ( + + ), + cell: (props) => ( + + ), + }), + columnHelper.accessor('name', { + header: () => ( + } /> + ), + cell: (props) => ( + { + const company = props.row.original; + company.name = value; + updateCompany(company); + }} + ChipComponent={CompanyChip} + /> + ), + }), + columnHelper.accessor('employees', { + header: () => ( + } /> + ), + cell: (props) => ( + { + const company = props.row.original; + company.employees = parseInt(value); + updateCompany(company); + }} + /> + ), + }), + columnHelper.accessor('domain_name', { + header: () => } />, + cell: (props) => ( + { + const company = props.row.original; + company.domain_name = value; + updateCompany(company); + }} + /> + ), + }), + columnHelper.accessor('address', { + header: () => } />, + cell: (props) => ( + { + const company = props.row.original; + company.address = value; + updateCompany(company); + }} + /> + ), + }), + columnHelper.accessor('creationDate', { + header: () => ( + } /> + ), + cell: (props) => ( + { + const company = props.row.original; + company.creationDate = value; + updateCompany(company); + }} + /> + ), + }), + columnHelper.accessor('accountOwner', { + header: () => ( + } /> + ), + cell: (props) => ( + + relation={props.row.original.accountOwner} + searchPlaceholder="Account Owner" + ChipComponent={PersonChip} + chipComponentPropsMapper={( + accountOwner: PartialUser, + ): 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: , + 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 + } + /> + ), + }), + ]; + }, []); +}; diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index 979322f7f..54e85a791 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -44,10 +44,18 @@ function People() { [], ); + const addEmptyRow = useCallback(() => { + console.log('add row'); + }, []); + const { data } = usePeopleQuery(orderBy, where); return ( - }> + } + onAddButtonClick={addEmptyRow} + > {
> { @@ -50,3 +84,14 @@ export async function updateCompany( }); return result; } + +export async function insertCompany( + company: Company, +): Promise> { + const result = await apiClient.mutate({ + mutation: INSERT_COMPANY, + variables: mapGqlCompany(company), + }); + + return result; +} diff --git a/hasura/metadata/databases/default/tables/public_companies.yaml b/hasura/metadata/databases/default/tables/public_companies.yaml index e8b5fb032..f477d08c9 100644 --- a/hasura/metadata/databases/default/tables/public_companies.yaml +++ b/hasura/metadata/databases/default/tables/public_companies.yaml @@ -20,6 +20,8 @@ insert_permissions: check: workspace_id: _eq: x-hasura-workspace-id + set: + workspace_id: x-hasura-Workspace-Id columns: - id - workspace_id