From 498a15970bc560414614814790200efa4241821e Mon Sep 17 00:00:00 2001 From: "Carlos E. Feria Vila" Date: Sun, 3 Jan 2021 19:09:17 +0100 Subject: [PATCH] Edit company (#18) * Add edit company * Add edit company * fix test --- cypress/integration/companyList.test.js | 2 +- src/Paths.ts | 2 + src/api/rest.tsx | 4 + src/pages/companies/companies.tsx | 2 + .../companies/company-list/company-list.tsx | 14 +- .../components/page-header/index.ts | 1 + .../components/page-header/page-header.tsx | 49 +++++++ .../stories/page-header.stories.tsx | 55 ++++++++ .../__snapshots__/page-header.test.tsx.snap | 75 +++++++++++ .../page-header/tests/page-header.test.tsx | 48 +++++++ .../edit-company/edit-company-header.tsx | 122 ++++++++++++++++++ .../companies/edit-company/edit-company.tsx | 41 ++++++ src/pages/companies/edit-company/index.ts | 1 + .../companies/edit-company/overview/index.ts | 1 + .../edit-company/overview/overview.tsx | 10 ++ .../companies/edit-company/sunat/index.ts | 1 + .../companies/edit-company/sunat/sunat.tsx | 10 ++ .../breadcrumb-path/breadcrumb-path.tsx | 31 +++++ .../components/breadcrumb-path/index.ts | 1 + .../stories/breadcrumb-path.stories.tsx | 44 +++++++ .../breadcrumb-path.test.tsx.snap | 34 +++++ .../tests/breadcrumb-path.test.tsx | 27 ++++ .../horizontal-nav/horizontal-nav.tsx | 29 +++++ src/shared/components/horizontal-nav/index.ts | 1 + .../stories/horizontal-nav.stories.tsx | 44 +++++++ .../horizontal-nav.test.tsx.snap | 75 +++++++++++ .../tests/horizontal-nav.test.tsx | 27 ++++ src/shared/components/menu-actions/index.ts | 1 + .../components/menu-actions/menu-actions.tsx | 35 +++++ .../stories/menu-actions.stories.tsx | 26 ++++ .../__snapshots__/menu-actions.test.tsx.snap | 32 +++++ .../menu-actions/tests/menu-actions.test.tsx | 50 +++++++ src/shared/hooks/index.ts | 3 + src/shared/hooks/useDeleteCompany/index.ts | 1 + .../useDeleteCompany.test.tsx | 0 .../useDeleteCompany}/useDeleteCompany.ts | 0 src/shared/hooks/useFetchCompanies/index.ts | 1 + .../useFetchCompanies.test.tsx} | 8 +- .../useFetchCompanies/useFetchCompanies.ts} | 12 +- src/shared/hooks/useFetchCompany/index.ts | 1 + .../useFetchCompany/useFetchCompany.test.tsx | 70 ++++++++++ .../hooks/useFetchCompany/useFetchCompany.ts | 96 ++++++++++++++ src/store/reducerUtils.tsx | 14 ++ 43 files changed, 1085 insertions(+), 16 deletions(-) create mode 100644 src/pages/companies/edit-company/components/page-header/index.ts create mode 100644 src/pages/companies/edit-company/components/page-header/page-header.tsx create mode 100644 src/pages/companies/edit-company/components/page-header/stories/page-header.stories.tsx create mode 100644 src/pages/companies/edit-company/components/page-header/tests/__snapshots__/page-header.test.tsx.snap create mode 100644 src/pages/companies/edit-company/components/page-header/tests/page-header.test.tsx create mode 100644 src/pages/companies/edit-company/edit-company-header.tsx create mode 100644 src/pages/companies/edit-company/edit-company.tsx create mode 100644 src/pages/companies/edit-company/index.ts create mode 100644 src/pages/companies/edit-company/overview/index.ts create mode 100644 src/pages/companies/edit-company/overview/overview.tsx create mode 100644 src/pages/companies/edit-company/sunat/index.ts create mode 100644 src/pages/companies/edit-company/sunat/sunat.tsx create mode 100644 src/shared/components/breadcrumb-path/breadcrumb-path.tsx create mode 100644 src/shared/components/breadcrumb-path/index.ts create mode 100644 src/shared/components/breadcrumb-path/stories/breadcrumb-path.stories.tsx create mode 100644 src/shared/components/breadcrumb-path/tests/__snapshots__/breadcrumb-path.test.tsx.snap create mode 100644 src/shared/components/breadcrumb-path/tests/breadcrumb-path.test.tsx create mode 100644 src/shared/components/horizontal-nav/horizontal-nav.tsx create mode 100644 src/shared/components/horizontal-nav/index.ts create mode 100644 src/shared/components/horizontal-nav/stories/horizontal-nav.stories.tsx create mode 100644 src/shared/components/horizontal-nav/tests/__snapshots__/horizontal-nav.test.tsx.snap create mode 100644 src/shared/components/horizontal-nav/tests/horizontal-nav.test.tsx create mode 100644 src/shared/components/menu-actions/index.ts create mode 100644 src/shared/components/menu-actions/menu-actions.tsx create mode 100644 src/shared/components/menu-actions/stories/menu-actions.stories.tsx create mode 100644 src/shared/components/menu-actions/tests/__snapshots__/menu-actions.test.tsx.snap create mode 100644 src/shared/components/menu-actions/tests/menu-actions.test.tsx create mode 100644 src/shared/hooks/useDeleteCompany/index.ts rename src/{pages/companies/company-list/hooks => shared/hooks/useDeleteCompany}/useDeleteCompany.test.tsx (100%) rename src/{pages/companies/company-list/hooks => shared/hooks/useDeleteCompany}/useDeleteCompany.ts (100%) create mode 100644 src/shared/hooks/useFetchCompanies/index.ts rename src/{pages/companies/company-list/hooks/useFetchCompany.test.tsx => shared/hooks/useFetchCompanies/useFetchCompanies.test.tsx} (94%) rename src/{pages/companies/company-list/hooks/useFetchCompany.ts => shared/hooks/useFetchCompanies/useFetchCompanies.ts} (91%) create mode 100644 src/shared/hooks/useFetchCompany/index.ts create mode 100644 src/shared/hooks/useFetchCompany/useFetchCompany.test.tsx create mode 100644 src/shared/hooks/useFetchCompany/useFetchCompany.ts create mode 100644 src/store/reducerUtils.tsx diff --git a/cypress/integration/companyList.test.js b/cypress/integration/companyList.test.js index 1c86c36..7c6338e 100644 --- a/cypress/integration/companyList.test.js +++ b/cypress/integration/companyList.test.js @@ -88,7 +88,7 @@ context("Test company list", () => { cy.get(".pf-c-table__action").first().click(); cy.get(".pf-c-dropdown__menu-item").contains("Edit").click(); - cy.url().should("eq", Cypress.config().baseUrl + "/companies/company1"); + cy.url().should("eq", Cypress.config().baseUrl + "/companies/company1/overview"); }); it("Company list - new", () => { diff --git a/src/Paths.ts b/src/Paths.ts index 55492ab..5562810 100644 --- a/src/Paths.ts +++ b/src/Paths.ts @@ -16,6 +16,8 @@ export enum Paths { newCompany = "/companies/~new", editCompany = "/companies/:company", + editCompany_overview = "/companies/:company/overview", + editCompany_sunat = "/companies/:company/sunat", } export interface OptionalCompanyRoute { diff --git a/src/api/rest.tsx b/src/api/rest.tsx index f36a284..003d0b3 100644 --- a/src/api/rest.tsx +++ b/src/api/rest.tsx @@ -41,3 +41,7 @@ export const getCompanies = ( export const deleteCompany = (company: Company): AxiosPromise => { return APIClient.delete(`${COMPANIES}/${company.name}`); }; + +export const getCompany = (name: string): AxiosPromise => { + return APIClient.get(`${COMPANIES}/${name}`); +}; diff --git a/src/pages/companies/companies.tsx b/src/pages/companies/companies.tsx index 068d907..93e60fd 100644 --- a/src/pages/companies/companies.tsx +++ b/src/pages/companies/companies.tsx @@ -6,6 +6,7 @@ import { Paths } from "Paths"; const CommpanyList = lazy(() => import("./company-list")); const NewCompany = lazy(() => import("./new-company")); +const EditCompany = lazy(() => import("./edit-company")); export const Projects: React.FC = () => { return ( @@ -14,6 +15,7 @@ export const Projects: React.FC = () => { + diff --git a/src/pages/companies/company-list/company-list.tsx b/src/pages/companies/company-list/company-list.tsx index 71d414d..79f7752 100644 --- a/src/pages/companies/company-list/company-list.tsx +++ b/src/pages/companies/company-list/company-list.tsx @@ -26,6 +26,7 @@ import { } from "@patternfly/react-table"; import { AddCircleOIcon } from "@patternfly/react-icons"; +import { alertActions } from "store/alert"; import { deleteWithMatchModalActions } from "store/delete-with-match-modal"; import { @@ -34,17 +35,18 @@ import { ConditionalRender, SimplePageSection, } from "shared/components"; -import { useTableControls } from "shared/hooks"; +import { + useTableControls, + useFetchCompanies, + useDeleteCompany, +} from "shared/hooks"; import { DeleteWithMatchModalContainer } from "shared/containers"; import { formatPath, Paths } from "Paths"; import { Company, PageQuery, SortByQuery } from "api/models"; +import { getAxiosErrorMessage } from "utils/modelUtils"; import { Welcome } from "./components/welcome"; -import useFetchCompany from "./hooks/useFetchCompany"; -import useDeleteCompany from "./hooks/useDeleteCompany"; -import { alertActions } from "store/alert"; -import { getAxiosErrorMessage } from "utils/modelUtils"; const columns: ICell[] = [ { title: "Name", transforms: [sortable] }, @@ -98,7 +100,7 @@ export const CompanyList: React.FC = ({ history }) => { fetchError, fetchCount, fetchCompanies, - } = useFetchCompany(true); + } = useFetchCompanies(true); const { filterText, diff --git a/src/pages/companies/edit-company/components/page-header/index.ts b/src/pages/companies/edit-company/components/page-header/index.ts new file mode 100644 index 0000000..9101025 --- /dev/null +++ b/src/pages/companies/edit-company/components/page-header/index.ts @@ -0,0 +1 @@ +export { PageHeader } from "./page-header"; diff --git a/src/pages/companies/edit-company/components/page-header/page-header.tsx b/src/pages/companies/edit-company/components/page-header/page-header.tsx new file mode 100644 index 0000000..99026da --- /dev/null +++ b/src/pages/companies/edit-company/components/page-header/page-header.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { + Stack, + StackItem, + Split, + SplitItem, + TextContent, + Text, +} from "@patternfly/react-core"; +import { BreadCrumbPath } from "shared/components/breadcrumb-path"; +import { MenuActions } from "shared/components/menu-actions"; +import { HorizontalNav } from "shared/components/horizontal-nav/horizontal-nav"; + +export interface PageHeaderProps { + title: string; + breadcrumbs: { title: string; path: string }[]; + menuActions: { label: string; callback: () => void }[]; + navItems: { title: string; path: string }[]; +} + +export const PageHeader: React.FC = ({ + title, + breadcrumbs, + menuActions, + navItems, +}) => { + return ( + + + + + + + + + {title} + + + + + + + + + + + + ); +}; diff --git a/src/pages/companies/edit-company/components/page-header/stories/page-header.stories.tsx b/src/pages/companies/edit-company/components/page-header/stories/page-header.stories.tsx new file mode 100644 index 0000000..54c176f --- /dev/null +++ b/src/pages/companies/edit-company/components/page-header/stories/page-header.stories.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { HashRouter } from "react-router-dom"; +import { Story, Meta } from "@storybook/react/types-6-0"; +import { action } from "@storybook/addon-actions"; +import { PageHeader, PageHeaderProps } from "../page-header"; + +export default { + title: "Edit company / PageHeader", + component: PageHeader, + argTypes: {}, +} as Meta; + +const Template: Story = (args) => ( + + + +); +export const Basic = Template.bind({}); +Basic.args = { + title: "mycompany", + breadcrumbs: [ + { + title: "Companies", + path: "/companies", + }, + { + title: "Company details", + path: "/companies/1", + }, + ], + menuActions: [ + { + label: "Edit", + callback: action("Edit"), + }, + { + label: "Delete", + callback: action("Delete"), + }, + ], + navItems: [ + { + title: "Overview", + path: "/companies/1/overview", + }, + { + title: "YAML", + path: "/companies/1/yaml", + }, + { + title: "SUNAT", + path: "/companies/1/sunat", + }, + ], +}; diff --git a/src/pages/companies/edit-company/components/page-header/tests/__snapshots__/page-header.test.tsx.snap b/src/pages/companies/edit-company/components/page-header/tests/__snapshots__/page-header.test.tsx.snap new file mode 100644 index 0000000..e4ec838 --- /dev/null +++ b/src/pages/companies/edit-company/components/page-header/tests/__snapshots__/page-header.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PageHeader Renders without crashing 1`] = ` + + + + + + + + + + mycompany + + + + + + + + + + + + +`; diff --git a/src/pages/companies/edit-company/components/page-header/tests/page-header.test.tsx b/src/pages/companies/edit-company/components/page-header/tests/page-header.test.tsx new file mode 100644 index 0000000..50dec26 --- /dev/null +++ b/src/pages/companies/edit-company/components/page-header/tests/page-header.test.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { PageHeader } from "../page-header"; + +describe("PageHeader", () => { + it("Renders without crashing", () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/pages/companies/edit-company/edit-company-header.tsx b/src/pages/companies/edit-company/edit-company-header.tsx new file mode 100644 index 0000000..6052866 --- /dev/null +++ b/src/pages/companies/edit-company/edit-company-header.tsx @@ -0,0 +1,122 @@ +import React, { useEffect } from "react"; +import { useHistory, useParams } from "react-router-dom"; +import { useDispatch } from "react-redux"; + +import { Alert, Skeleton } from "@patternfly/react-core"; + +import { useDeleteCompany, useFetchCompany } from "shared/hooks"; + +import { alertActions } from "store/alert"; +import { deleteWithMatchModalActions } from "store/delete-with-match-modal"; + +import { CompanytRoute, formatPath, Paths } from "Paths"; +import { getAxiosErrorMessage } from "utils/modelUtils"; + +import { PageHeader } from "./components/page-header"; +import { ConditionalRender } from "shared/components"; + +export interface EditCompanyHeaderProps {} + +export const EditCompanyHeader: React.FC = () => { + const params = useParams(); + const history = useHistory(); + + const { deleteCompany } = useDeleteCompany(); + const { company, isFetching, fetchError, fetchCompany } = useFetchCompany(); + + const dispatch = useDispatch(); + + useEffect(() => { + fetchCompany(params.company); + }, [params, fetchCompany]); + + const handleOnEdit = () => { + const path = formatPath(Paths.editCompany_sunat, { + company: params.company, + }); + history.push(path); + }; + + const handleOnDelete = () => { + if (!company) { + throw new Error("Company not defined, can not delete it"); + } + + dispatch( + deleteWithMatchModalActions.openModal({ + title: "Delete company", + message: `Are you sure you want to delete the company ${params.company}`, + matchText: params.company, + onDelete: () => { + dispatch(deleteWithMatchModalActions.processing()); + deleteCompany( + company, + () => { + dispatch(deleteWithMatchModalActions.closeModal()); + history.push(Paths.companyList); + }, + (error) => { + dispatch(deleteWithMatchModalActions.closeModal()); + dispatch( + alertActions.addAlert( + "danger", + "Error", + getAxiosErrorMessage(error) + ) + ); + } + ); + }, + }) + ); + }; + + if (fetchError) { + return ( + + ); + } + + return ( + } + > + + + ); +}; diff --git a/src/pages/companies/edit-company/edit-company.tsx b/src/pages/companies/edit-company/edit-company.tsx new file mode 100644 index 0000000..b9f8e08 --- /dev/null +++ b/src/pages/companies/edit-company/edit-company.tsx @@ -0,0 +1,41 @@ +import React, { lazy, Suspense } from "react"; +import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom"; + +import { PageSection } from "@patternfly/react-core"; + +import { AppPlaceholder } from "shared/components"; + +import { CompanytRoute, Paths } from "Paths"; + +import { EditCompanyHeader } from "./edit-company-header"; + +const Overview = lazy(() => import("./overview")); +const Sunat = lazy(() => import("./sunat")); + +export interface AnalysisConfigurationProps + extends RouteComponentProps {} + +export const EditCompany: React.FC = ({ + match, +}) => { + return ( + <> + + + + + }> + + + + + + + + + ); +}; diff --git a/src/pages/companies/edit-company/index.ts b/src/pages/companies/edit-company/index.ts new file mode 100644 index 0000000..9a7116d --- /dev/null +++ b/src/pages/companies/edit-company/index.ts @@ -0,0 +1 @@ +export { EditCompany as default } from "./edit-company"; diff --git a/src/pages/companies/edit-company/overview/index.ts b/src/pages/companies/edit-company/overview/index.ts new file mode 100644 index 0000000..8578b0f --- /dev/null +++ b/src/pages/companies/edit-company/overview/index.ts @@ -0,0 +1 @@ +export { Overview as default } from "./overview"; diff --git a/src/pages/companies/edit-company/overview/overview.tsx b/src/pages/companies/edit-company/overview/overview.tsx new file mode 100644 index 0000000..ef93896 --- /dev/null +++ b/src/pages/companies/edit-company/overview/overview.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { RouteComponentProps } from "react-router-dom"; + +import { CompanytRoute } from "Paths"; + +export interface OverviewProps extends RouteComponentProps {} + +export const Overview: React.FC = () => { + return overview; +}; diff --git a/src/pages/companies/edit-company/sunat/index.ts b/src/pages/companies/edit-company/sunat/index.ts new file mode 100644 index 0000000..d14fa1a --- /dev/null +++ b/src/pages/companies/edit-company/sunat/index.ts @@ -0,0 +1 @@ +export { Sunat as default } from "./sunat"; diff --git a/src/pages/companies/edit-company/sunat/sunat.tsx b/src/pages/companies/edit-company/sunat/sunat.tsx new file mode 100644 index 0000000..2431eb8 --- /dev/null +++ b/src/pages/companies/edit-company/sunat/sunat.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { RouteComponentProps } from "react-router-dom"; + +import { CompanytRoute } from "Paths"; + +export interface SunatProps extends RouteComponentProps {} + +export const Sunat: React.FC = () => { + return sunat; +}; diff --git a/src/shared/components/breadcrumb-path/breadcrumb-path.tsx b/src/shared/components/breadcrumb-path/breadcrumb-path.tsx new file mode 100644 index 0000000..e399e16 --- /dev/null +++ b/src/shared/components/breadcrumb-path/breadcrumb-path.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core"; + +export interface BreadCrumbPathProps { + breadcrumbs: { title: string; path: string }[]; +} + +export const BreadCrumbPath: React.FC = ({ + breadcrumbs, +}) => { + return ( + + {breadcrumbs.map((crumb, i, { length }) => { + const isLast = i === length - 1; + + return ( + + {isLast ? ( + crumb.title + ) : ( + + {crumb.title} + + )} + + ); + })} + + ); +}; diff --git a/src/shared/components/breadcrumb-path/index.ts b/src/shared/components/breadcrumb-path/index.ts new file mode 100644 index 0000000..06a5d86 --- /dev/null +++ b/src/shared/components/breadcrumb-path/index.ts @@ -0,0 +1 @@ +export { BreadCrumbPath } from "./breadcrumb-path"; diff --git a/src/shared/components/breadcrumb-path/stories/breadcrumb-path.stories.tsx b/src/shared/components/breadcrumb-path/stories/breadcrumb-path.stories.tsx new file mode 100644 index 0000000..682d6a5 --- /dev/null +++ b/src/shared/components/breadcrumb-path/stories/breadcrumb-path.stories.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { HashRouter } from "react-router-dom"; +import { Story, Meta } from "@storybook/react/types-6-0"; +import { BreadCrumbPath, BreadCrumbPathProps } from "../breadcrumb-path"; + +export default { + title: "Components / BreadcrumbPath", + component: BreadCrumbPath, + argTypes: {}, +} as Meta; + +const Template: Story = (args) => ( + + + +); + +export const Single = Template.bind({}); +Single.args = { + breadcrumbs: [ + { + title: "first", + path: "/first", + }, + ], +}; + +export const Multiple = Template.bind({}); +Multiple.args = { + breadcrumbs: [ + { + title: "first", + path: "/first", + }, + { + title: "second", + path: "/second", + }, + { + title: "thrid", + path: "/thrid", + }, + ], +}; diff --git a/src/shared/components/breadcrumb-path/tests/__snapshots__/breadcrumb-path.test.tsx.snap b/src/shared/components/breadcrumb-path/tests/__snapshots__/breadcrumb-path.test.tsx.snap new file mode 100644 index 0000000..8d5f8ba --- /dev/null +++ b/src/shared/components/breadcrumb-path/tests/__snapshots__/breadcrumb-path.test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BreadCrumbPath Renders without crashing 1`] = ` + + + + first + + + + + second + + + + thrid + + +`; diff --git a/src/shared/components/breadcrumb-path/tests/breadcrumb-path.test.tsx b/src/shared/components/breadcrumb-path/tests/breadcrumb-path.test.tsx new file mode 100644 index 0000000..3f8ee45 --- /dev/null +++ b/src/shared/components/breadcrumb-path/tests/breadcrumb-path.test.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { BreadCrumbPath } from "../breadcrumb-path"; + +describe("BreadCrumbPath", () => { + it("Renders without crashing", () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/shared/components/horizontal-nav/horizontal-nav.tsx b/src/shared/components/horizontal-nav/horizontal-nav.tsx new file mode 100644 index 0000000..dafa91f --- /dev/null +++ b/src/shared/components/horizontal-nav/horizontal-nav.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; + +export interface HorizontalNavProps { + navItems: { title: string; path: string }[]; +} + +export const HorizontalNav: React.FC = ({ navItems }) => { + return ( +
+
    + {navItems.map((f, index) => ( + +
  • + +
  • +
    + ))} +
+
+ ); +}; diff --git a/src/shared/components/horizontal-nav/index.ts b/src/shared/components/horizontal-nav/index.ts new file mode 100644 index 0000000..6396c18 --- /dev/null +++ b/src/shared/components/horizontal-nav/index.ts @@ -0,0 +1 @@ +export { HorizontalNav as Welcome } from "./horizontal-nav"; diff --git a/src/shared/components/horizontal-nav/stories/horizontal-nav.stories.tsx b/src/shared/components/horizontal-nav/stories/horizontal-nav.stories.tsx new file mode 100644 index 0000000..d7d86df --- /dev/null +++ b/src/shared/components/horizontal-nav/stories/horizontal-nav.stories.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { HashRouter } from "react-router-dom"; +import { Story, Meta } from "@storybook/react/types-6-0"; +import { HorizontalNav, HorizontalNavProps } from "../horizontal-nav"; + +export default { + title: "Components / HorizontalNav", + component: HorizontalNav, + argTypes: {}, +} as Meta; + +const Template: Story = (args) => ( + + + +); + +export const Single = Template.bind({}); +Single.args = { + navItems: [ + { + title: "first", + path: "/first", + }, + ], +}; + +export const Multiple = Template.bind({}); +Multiple.args = { + navItems: [ + { + title: "first", + path: "/first", + }, + { + title: "second", + path: "/second", + }, + { + title: "thrid", + path: "/thrid", + }, + ], +}; diff --git a/src/shared/components/horizontal-nav/tests/__snapshots__/horizontal-nav.test.tsx.snap b/src/shared/components/horizontal-nav/tests/__snapshots__/horizontal-nav.test.tsx.snap new file mode 100644 index 0000000..67855e9 --- /dev/null +++ b/src/shared/components/horizontal-nav/tests/__snapshots__/horizontal-nav.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HorizontalNav Renders without crashing 1`] = ` +
+
    + +
  • + +
  • +
    + +
  • + +
  • +
    + +
  • + +
  • +
    +
+
+`; diff --git a/src/shared/components/horizontal-nav/tests/horizontal-nav.test.tsx b/src/shared/components/horizontal-nav/tests/horizontal-nav.test.tsx new file mode 100644 index 0000000..9a84d3b --- /dev/null +++ b/src/shared/components/horizontal-nav/tests/horizontal-nav.test.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { HorizontalNav } from "../horizontal-nav"; + +describe("HorizontalNav", () => { + it("Renders without crashing", () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/shared/components/menu-actions/index.ts b/src/shared/components/menu-actions/index.ts new file mode 100644 index 0000000..3f0952b --- /dev/null +++ b/src/shared/components/menu-actions/index.ts @@ -0,0 +1 @@ +export { MenuActions } from "./menu-actions"; diff --git a/src/shared/components/menu-actions/menu-actions.tsx b/src/shared/components/menu-actions/menu-actions.tsx new file mode 100644 index 0000000..a02456b --- /dev/null +++ b/src/shared/components/menu-actions/menu-actions.tsx @@ -0,0 +1,35 @@ +import React, { useState } from "react"; +import { Dropdown, DropdownItem, DropdownToggle } from "@patternfly/react-core"; +import { CaretDownIcon } from "@patternfly/react-icons"; + +export interface MenuActionsProps { + actions: { label: string; callback: () => void }[]; +} + +export const MenuActions: React.FC = ({ actions }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + { + setIsOpen(!isOpen); + }} + toggle={ + { + setIsOpen(isOpen); + }} + toggleIndicator={CaretDownIcon} + > + Actions + + } + dropdownItems={actions.map((element, index) => ( + + {element.label} + + ))} + /> + ); +}; diff --git a/src/shared/components/menu-actions/stories/menu-actions.stories.tsx b/src/shared/components/menu-actions/stories/menu-actions.stories.tsx new file mode 100644 index 0000000..d8294b4 --- /dev/null +++ b/src/shared/components/menu-actions/stories/menu-actions.stories.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { Story, Meta } from "@storybook/react/types-6-0"; +import { action } from "@storybook/addon-actions"; +import { MenuActions, MenuActionsProps } from "../menu-actions"; + +export default { + title: "Components / MenuActions", + component: MenuActions, + argTypes: {}, +} as Meta; + +const Template: Story = (args) => ; + +export const Basic = Template.bind({}); +Basic.args = { + actions: [ + { + label: "Action1", + callback: action("Action1 callback"), + }, + { + label: "Action2", + callback: action("Action2 callback"), + }, + ], +}; diff --git a/src/shared/components/menu-actions/tests/__snapshots__/menu-actions.test.tsx.snap b/src/shared/components/menu-actions/tests/__snapshots__/menu-actions.test.tsx.snap new file mode 100644 index 0000000..6121108 --- /dev/null +++ b/src/shared/components/menu-actions/tests/__snapshots__/menu-actions.test.tsx.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MenuActions Renders without crashing 1`] = ` + + Action1 + , + + Action2 + , + ] + } + isOpen={false} + onSelect={[Function]} + toggle={ + + Actions + + } +/> +`; diff --git a/src/shared/components/menu-actions/tests/menu-actions.test.tsx b/src/shared/components/menu-actions/tests/menu-actions.test.tsx new file mode 100644 index 0000000..5cac612 --- /dev/null +++ b/src/shared/components/menu-actions/tests/menu-actions.test.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { mount, shallow } from "enzyme"; +import { MenuActions } from "../menu-actions"; + +describe("MenuActions", () => { + it("Renders without crashing", () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + it("Executes callback", () => { + const callback1Mock = jest.fn(); + const callback2Mock = jest.fn(); + + const wrapper = mount( + + ); + + // Select dropdown btn + const dropdownBtn = wrapper.find("button").at(0); + expect(dropdownBtn.text()).toEqual("Actions"); + + // Verify callbacks are executed + + dropdownBtn.simulate("click"); // Opens dropdown + + const action1Btn = wrapper.find(".pf-c-dropdown__menu-item").at(0); + expect(action1Btn.text()).toEqual("Action1"); + action1Btn.simulate("click"); + expect(callback1Mock).toHaveBeenCalledTimes(1); + + dropdownBtn.simulate("click"); // Opens dropdown + const action2Btn = wrapper.find(".pf-c-dropdown__menu-item").at(1); + expect(action2Btn.text()).toEqual("Action2"); + action2Btn.simulate("click"); + expect(callback2Mock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts index 1062b2a..f796b7c 100644 --- a/src/shared/hooks/index.ts +++ b/src/shared/hooks/index.ts @@ -1 +1,4 @@ +export { useDeleteCompany } from "./useDeleteCompany"; +export { useFetchCompanies } from "./useFetchCompanies"; +export { useFetchCompany } from "./useFetchCompany"; export { useTableControls } from "./useTableControls"; diff --git a/src/shared/hooks/useDeleteCompany/index.ts b/src/shared/hooks/useDeleteCompany/index.ts new file mode 100644 index 0000000..5b93132 --- /dev/null +++ b/src/shared/hooks/useDeleteCompany/index.ts @@ -0,0 +1 @@ +export { useDeleteCompany } from "./useDeleteCompany"; diff --git a/src/pages/companies/company-list/hooks/useDeleteCompany.test.tsx b/src/shared/hooks/useDeleteCompany/useDeleteCompany.test.tsx similarity index 100% rename from src/pages/companies/company-list/hooks/useDeleteCompany.test.tsx rename to src/shared/hooks/useDeleteCompany/useDeleteCompany.test.tsx diff --git a/src/pages/companies/company-list/hooks/useDeleteCompany.ts b/src/shared/hooks/useDeleteCompany/useDeleteCompany.ts similarity index 100% rename from src/pages/companies/company-list/hooks/useDeleteCompany.ts rename to src/shared/hooks/useDeleteCompany/useDeleteCompany.ts diff --git a/src/shared/hooks/useFetchCompanies/index.ts b/src/shared/hooks/useFetchCompanies/index.ts new file mode 100644 index 0000000..dab7ef6 --- /dev/null +++ b/src/shared/hooks/useFetchCompanies/index.ts @@ -0,0 +1 @@ +export { useFetchCompanies } from "./useFetchCompanies"; diff --git a/src/pages/companies/company-list/hooks/useFetchCompany.test.tsx b/src/shared/hooks/useFetchCompanies/useFetchCompanies.test.tsx similarity index 94% rename from src/pages/companies/company-list/hooks/useFetchCompany.test.tsx rename to src/shared/hooks/useFetchCompanies/useFetchCompanies.test.tsx index b7af80c..fc51142 100644 --- a/src/pages/companies/company-list/hooks/useFetchCompany.test.tsx +++ b/src/shared/hooks/useFetchCompanies/useFetchCompanies.test.tsx @@ -1,16 +1,16 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import { renderHook, act } from "@testing-library/react-hooks"; -import { useFetchCompany } from "./useFetchCompany"; +import { useFetchCompanies } from "./useFetchCompanies"; import { Company, PageRepresentation } from "api/models"; -describe("useFetchCompany", () => { +describe("useFetchCompanies", () => { it("Fetch error due to no REST API found", async () => { // Mock REST API new MockAdapter(axios).onGet("/user/companies").networkError(); // Use hook - const { result, waitForNextUpdate } = renderHook(() => useFetchCompany()); + const { result, waitForNextUpdate } = renderHook(() => useFetchCompanies()); const { companies, @@ -56,7 +56,7 @@ describe("useFetchCompany", () => { .reply(200, data); // Use hook - const { result, waitForNextUpdate } = renderHook(() => useFetchCompany()); + const { result, waitForNextUpdate } = renderHook(() => useFetchCompanies()); const { companies, diff --git a/src/pages/companies/company-list/hooks/useFetchCompany.ts b/src/shared/hooks/useFetchCompanies/useFetchCompanies.ts similarity index 91% rename from src/pages/companies/company-list/hooks/useFetchCompany.ts rename to src/shared/hooks/useFetchCompanies/useFetchCompanies.ts index fb3bbdb..028c38c 100644 --- a/src/pages/companies/company-list/hooks/useFetchCompany.ts +++ b/src/shared/hooks/useFetchCompanies/useFetchCompanies.ts @@ -15,9 +15,9 @@ export const { success: fetchSuccess, failure: fetchFailure, } = createAsyncAction( - "useFetchCompany/fetch/request", - "useFetchCompany/fetch/success", - "useFetchCompany/fetch/failure" + "useFetchCompanies/fetch/request", + "useFetchCompanies/fetch/success", + "useFetchCompanies/fetch/failure" ), AxiosError>(); type State = Readonly<{ @@ -84,7 +84,9 @@ export interface IState { ) => void; } -export const useFetchCompany = (defaultIsFetching: boolean = false): IState => { +export const useFetchCompanies = ( + defaultIsFetching: boolean = false +): IState => { const [state, dispatch] = useReducer(reducer, defaultIsFetching, initReducer); const fetchCompanies = useCallback( @@ -111,4 +113,4 @@ export const useFetchCompany = (defaultIsFetching: boolean = false): IState => { }; }; -export default useFetchCompany; +export default useFetchCompanies; diff --git a/src/shared/hooks/useFetchCompany/index.ts b/src/shared/hooks/useFetchCompany/index.ts new file mode 100644 index 0000000..48590e0 --- /dev/null +++ b/src/shared/hooks/useFetchCompany/index.ts @@ -0,0 +1 @@ +export { useFetchCompany } from "./useFetchCompany"; diff --git a/src/shared/hooks/useFetchCompany/useFetchCompany.test.tsx b/src/shared/hooks/useFetchCompany/useFetchCompany.test.tsx new file mode 100644 index 0000000..16ae5e7 --- /dev/null +++ b/src/shared/hooks/useFetchCompany/useFetchCompany.test.tsx @@ -0,0 +1,70 @@ +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import { renderHook, act } from "@testing-library/react-hooks"; +import { useFetchCompany } from "./useFetchCompany"; +import { Company, PageRepresentation } from "api/models"; + +describe("useFetchCompany", () => { + it("Fetch error due to no REST API found", async () => { + const COMPANY_NAME = "mycompany"; + + // Mock REST API + new MockAdapter(axios).onGet("/companies/" + COMPANY_NAME).networkError(); + + // Use hook + const { result, waitForNextUpdate } = renderHook(() => useFetchCompany()); + + const { company, isFetching, fetchError, fetchCompany } = result.current; + + expect(isFetching).toBe(false); + expect(company).toBeUndefined(); + expect(fetchError).toBeUndefined(); + + // Init fetch + act(() => fetchCompany(COMPANY_NAME)); + expect(result.current.isFetching).toBe(true); + + // Fetch finished + await waitForNextUpdate(); + expect(result.current.isFetching).toBe(false); + expect(result.current.company).toBeUndefined(); + expect(result.current.fetchError).not.toBeUndefined(); + }); + + it("Fetch success", async () => { + // Mock REST API + const data: Company = { + name: "myCompany", + webServices: { + factura: "http://url1.com", + guia: "http://url2.com", + retenciones: "http://url3.com", + }, + credentials: { + username: "myUsername", + password: "myPassword", + }, + }; + + new MockAdapter(axios).onGet(`/companies/${data.name}`).reply(200, data); + + // Use hook + const { result, waitForNextUpdate } = renderHook(() => useFetchCompany()); + + const { company, isFetching, fetchError, fetchCompany } = result.current; + + expect(isFetching).toBe(false); + expect(company).toBeUndefined(); + expect(fetchError).toBeUndefined(); + + // Init fetch + act(() => fetchCompany(data.name)); + expect(result.current.isFetching).toBe(true); + + // Fetch finished + await waitForNextUpdate(); + expect(result.current.isFetching).toBe(false); + expect(result.current.company).toMatchObject(data); + expect(result.current.fetchError).toBeUndefined(); + }); +}); diff --git a/src/shared/hooks/useFetchCompany/useFetchCompany.ts b/src/shared/hooks/useFetchCompany/useFetchCompany.ts new file mode 100644 index 0000000..3cbb9b8 --- /dev/null +++ b/src/shared/hooks/useFetchCompany/useFetchCompany.ts @@ -0,0 +1,96 @@ +import { useCallback, useReducer } from "react"; +import { AxiosError } from "axios"; +import { ActionType, createAsyncAction, getType } from "typesafe-actions"; + +import { getCompany } from "api/rest"; +import { Company } from "api/models"; + +export const { + request: fetchRequest, + success: fetchSuccess, + failure: fetchFailure, +} = createAsyncAction( + "useFetchCompany/fetch/request", + "useFetchCompany/fetch/success", + "useFetchCompany/fetch/failure" +)(); + +type State = Readonly<{ + isFetching: boolean; + company?: Company; + fetchError?: AxiosError; +}>; + +const defaultState: State = { + isFetching: false, + company: undefined, + fetchError: undefined, +}; + +type Action = ActionType< + typeof fetchRequest | typeof fetchSuccess | typeof fetchFailure +>; + +const initReducer = (isFetching: boolean): State => { + return { + ...defaultState, + isFetching, + }; +}; + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case getType(fetchRequest): + return { + ...state, + isFetching: true, + }; + case getType(fetchSuccess): + return { + ...state, + isFetching: false, + fetchError: undefined, + company: action.payload, + }; + case getType(fetchFailure): + return { + ...state, + isFetching: false, + fetchError: action.payload, + }; + default: + return state; + } +}; + +export interface IState { + company?: Company; + isFetching: boolean; + fetchError?: AxiosError; + fetchCompany: (name: string) => void; +} + +export const useFetchCompany = (defaultIsFetching: boolean = false): IState => { + const [state, dispatch] = useReducer(reducer, defaultIsFetching, initReducer); + + const fetchCompany = useCallback((name: string) => { + dispatch(fetchRequest()); + + getCompany(name) + .then(({ data }) => { + dispatch(fetchSuccess(data)); + }) + .catch((error: AxiosError) => { + dispatch(fetchFailure(error)); + }); + }, []); + + return { + company: state.company, + isFetching: state.isFetching, + fetchError: state.fetchError, + fetchCompany, + }; +}; + +export default useFetchCompany; diff --git a/src/store/reducerUtils.tsx b/src/store/reducerUtils.tsx new file mode 100644 index 0000000..4e80881 --- /dev/null +++ b/src/store/reducerUtils.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { mount } from "enzyme"; +import { Provider } from "react-redux"; +import { applyMiddleware, createStore } from "redux"; +import thunk from "redux-thunk"; +import { rootReducer } from "./rootReducer"; + +export const mockStore = (initialStatus?: any) => + initialStatus + ? createStore(rootReducer, initialStatus, applyMiddleware(thunk)) + : createStore(rootReducer, applyMiddleware(thunk)); + +export const mountWithRedux = (component: any, store: any = mockStore()) => + mount({component});