From 9b65a41216bfb3062d34f3cf4dfcc9ea83a62e38 Mon Sep 17 00:00:00 2001 From: "Carlos E. Feria Vila" Date: Mon, 8 Mar 2021 10:56:03 -0800 Subject: [PATCH] Add stakeholder, stakeholder-groups (#2) * Add stakeholder groups * Add the title to modals * Add unit tests * delete unused code * rename files * Add business services test full form * Move e2e test to custom folders * Enhance filtering test * change controls default port to 8081 * Change controls db port to 5433 * Add readme docs * Add stakeholder groups * Restore proxy * add stakeholder groups * Change tabs order * add placeholders * Add select job * Move select-stakeholders to the shared folder to avoid conflicting order of css * Add test for stakeholders * Remove sort by members and groups * Reduce description width * Change widh * Changed 'name' field for to be 'role' (#1) * Fix test and remove creatable Co-authored-by: Marco Rizzi --- README.md | 60 ++- .../businessServiceList.test.js | 45 +- .../newBusinessService.test.js | 137 ++++++ .../stakeholder/stakeholderList.test.js | 274 +++++++++++ .../integration/newBusinessService.test.js | 59 --- package.json | 7 +- public/locales/en/translation.json | 20 +- public/locales/es/translation.json | 20 +- src/api/models.tsx | 29 ++ src/api/rest.tsx | 147 +++++- .../business-services/business-services.tsx | 271 ++++------- ....tsx => business-service-form.stories.tsx} | 2 +- .../tests/new-business-service-form.test.tsx | 12 - .../components/search-filter/index.ts | 1 - .../select-stakeholder-field.tsx | 11 +- src/pages/controls/controls-header.tsx | 8 +- src/pages/controls/controls.tsx | 12 +- .../new-stakeholder-group-modal/index.ts | 1 + .../new-stakeholder-group-modal.tsx | 34 ++ .../select-member-form-field/index.ts | 1 + .../select-member-form-field.tsx | 43 ++ .../stakeholder-group-form/index.ts | 1 + .../stakeholder-group-form.tsx | 215 +++++++++ .../stories/stakeholder-form.stories.tsx | 29 ++ .../update-stakeholder-group-modal/index.ts | 1 + .../update-stakeholder-group-modal.tsx | 38 ++ .../stakeholder-groups/stakeholder-groups.tsx | 420 ++++++++++++++++- .../components/new-stakeholder-modal/index.ts | 1 + .../new-stakeholder-modal.tsx | 34 ++ .../select-group-form-field/index.ts | 1 + .../select-group-form-field.tsx | 38 ++ .../components/select-group/index.ts | 1 + .../components/select-group/select-group.tsx | 102 ++++ .../stories/select-member.stories.tsx | 71 +++ .../__snapshots__/select-group.test.tsx.snap | 95 ++++ .../select-group/tests/select-group.test.tsx | 25 + .../select-job-function-form-field/index.ts | 1 + .../select-job-function-form-field.tsx | 38 ++ .../components/select-job-function/index.ts | 1 + .../select-job-function.tsx} | 38 +- .../stories/select-job-function.stories.tsx | 74 +++ .../select-job-function.test.tsx.snap | 99 ++++ .../tests/select-job-function.test.tsx | 29 ++ .../components/stakeholder-form/index.ts | 1 + .../stakeholder-form/stakeholder-form.tsx | 248 ++++++++++ .../stories/stakeholder-form.stories.tsx | 26 ++ .../update-stakeholder-modal/index.ts | 1 + .../update-stakeholder-modal.tsx | 38 ++ .../controls/stakeholders/stakeholders.tsx | 435 +++++++++++++++++- .../app-table-action-buttons.tsx | 30 ++ .../app-table-action-buttons/index.ts | 1 + .../app-table-action-buttons.test.tsx.snap | 30 ++ .../tests/app-table-action-buttons.test.tsx | 35 ++ .../app-table-toolbar-toggle-group.tsx | 81 ++++ .../app-table-toolbar-toggle-group/index.ts | 1 + ...p-table-toolbar-toggle-group.test.tsx.snap | 48 ++ .../app-table-toolbar-toggle-group.test.tsx | 33 ++ .../app-table-with-controls.tsx | 16 +- .../app-table-with-controls.test.tsx.snap | 40 +- .../tests/app-table-with-controls.test.tsx | 6 +- src/shared/components/app-table/app-table.tsx | 5 + src/shared/components/index.ts | 4 + src/shared/components/search-filter/index.ts | 1 + .../search-filter/search-filter.tsx | 2 +- .../components/select-stakeholder/index.ts | 0 .../select-stakeholder/select-stakeholder.tsx | 131 ++++++ .../stories/select-stakeholder.stories.tsx | 0 .../select-stakeholder.test.tsx.snap | 8 +- .../tests/select-stakeholder.test.tsx | 0 src/shared/hooks/index.ts | 4 + .../hooks/useDeleteStakeholder/index.ts | 1 + .../useDeleteStakeholder.test.tsx | 78 ++++ .../useDeleteStakeholder.ts | 49 ++ .../hooks/useDeleteStakeholderGroup/index.ts | 1 + .../useDeleteStakeholderGroup.test.tsx | 80 ++++ .../useDeleteStakeholderGroup.ts | 51 ++ .../hooks/useFetchJobFunctions/index.ts | 1 + .../useFetchJobFunctions.test.tsx | 49 ++ .../useFetchJobFunctions.ts | 114 +++++ .../hooks/useFetchStakeholderGroups/index.ts | 1 + .../useFetchStakeholderGroups.test.tsx | 125 +++++ .../useFetchStakeholderGroups.ts | 161 +++++++ .../useFetchStakeholders.test.tsx | 8 +- .../useFetchStakeholders.ts | 12 +- yarn.lock | 108 +++-- 85 files changed, 4194 insertions(+), 416 deletions(-) rename cypress/integration/{ => controls/business-service}/businessServiceList.test.js (84%) create mode 100644 cypress/integration/controls/business-service/newBusinessService.test.js create mode 100644 cypress/integration/controls/stakeholder/stakeholderList.test.js delete mode 100644 cypress/integration/newBusinessService.test.js rename src/pages/controls/business-services/components/business-service-form/stories/{new-business-service-form.stories.tsx => business-service-form.stories.tsx} (93%) delete mode 100644 src/pages/controls/business-services/components/business-service-form/tests/new-business-service-form.test.tsx delete mode 100644 src/pages/controls/business-services/components/search-filter/index.ts create mode 100644 src/pages/controls/stakeholder-groups/components/new-stakeholder-group-modal/index.ts create mode 100644 src/pages/controls/stakeholder-groups/components/new-stakeholder-group-modal/new-stakeholder-group-modal.tsx create mode 100644 src/pages/controls/stakeholder-groups/components/select-member-form-field/index.ts create mode 100644 src/pages/controls/stakeholder-groups/components/select-member-form-field/select-member-form-field.tsx create mode 100644 src/pages/controls/stakeholder-groups/components/stakeholder-group-form/index.ts create mode 100644 src/pages/controls/stakeholder-groups/components/stakeholder-group-form/stakeholder-group-form.tsx create mode 100644 src/pages/controls/stakeholder-groups/components/stakeholder-group-form/stories/stakeholder-form.stories.tsx create mode 100644 src/pages/controls/stakeholder-groups/components/update-stakeholder-group-modal/index.ts create mode 100644 src/pages/controls/stakeholder-groups/components/update-stakeholder-group-modal/update-stakeholder-group-modal.tsx create mode 100644 src/pages/controls/stakeholders/components/new-stakeholder-modal/index.ts create mode 100644 src/pages/controls/stakeholders/components/new-stakeholder-modal/new-stakeholder-modal.tsx create mode 100644 src/pages/controls/stakeholders/components/select-group-form-field/index.ts create mode 100644 src/pages/controls/stakeholders/components/select-group-form-field/select-group-form-field.tsx create mode 100644 src/pages/controls/stakeholders/components/select-group/index.ts create mode 100644 src/pages/controls/stakeholders/components/select-group/select-group.tsx create mode 100644 src/pages/controls/stakeholders/components/select-group/stories/select-member.stories.tsx create mode 100644 src/pages/controls/stakeholders/components/select-group/tests/__snapshots__/select-group.test.tsx.snap create mode 100644 src/pages/controls/stakeholders/components/select-group/tests/select-group.test.tsx create mode 100644 src/pages/controls/stakeholders/components/select-job-function-form-field/index.ts create mode 100644 src/pages/controls/stakeholders/components/select-job-function-form-field/select-job-function-form-field.tsx create mode 100644 src/pages/controls/stakeholders/components/select-job-function/index.ts rename src/pages/controls/{business-services/components/select-stakeholder/select-stakeholder.tsx => stakeholders/components/select-job-function/select-job-function.tsx} (68%) create mode 100644 src/pages/controls/stakeholders/components/select-job-function/stories/select-job-function.stories.tsx create mode 100644 src/pages/controls/stakeholders/components/select-job-function/tests/__snapshots__/select-job-function.test.tsx.snap create mode 100644 src/pages/controls/stakeholders/components/select-job-function/tests/select-job-function.test.tsx create mode 100644 src/pages/controls/stakeholders/components/stakeholder-form/index.ts create mode 100644 src/pages/controls/stakeholders/components/stakeholder-form/stakeholder-form.tsx create mode 100644 src/pages/controls/stakeholders/components/stakeholder-form/stories/stakeholder-form.stories.tsx create mode 100644 src/pages/controls/stakeholders/components/update-stakeholder-modal/index.ts create mode 100644 src/pages/controls/stakeholders/components/update-stakeholder-modal/update-stakeholder-modal.tsx create mode 100644 src/shared/components/app-table-action-buttons/app-table-action-buttons.tsx create mode 100644 src/shared/components/app-table-action-buttons/index.ts create mode 100644 src/shared/components/app-table-action-buttons/tests/__snapshots__/app-table-action-buttons.test.tsx.snap create mode 100644 src/shared/components/app-table-action-buttons/tests/app-table-action-buttons.test.tsx create mode 100644 src/shared/components/app-table-toolbar-toggle-group/app-table-toolbar-toggle-group.tsx create mode 100644 src/shared/components/app-table-toolbar-toggle-group/index.ts create mode 100644 src/shared/components/app-table-toolbar-toggle-group/tests/__snapshots__/app-table-toolbar-toggle-group.test.tsx.snap create mode 100644 src/shared/components/app-table-toolbar-toggle-group/tests/app-table-toolbar-toggle-group.test.tsx create mode 100644 src/shared/components/search-filter/index.ts rename src/{pages/controls/business-services => shared}/components/search-filter/search-filter.tsx (98%) rename src/{pages/controls/business-services => shared}/components/select-stakeholder/index.ts (100%) create mode 100644 src/shared/components/select-stakeholder/select-stakeholder.tsx rename src/{pages/controls/business-services => shared}/components/select-stakeholder/stories/select-stakeholder.stories.tsx (100%) rename src/{pages/controls/business-services => shared}/components/select-stakeholder/tests/__snapshots__/select-stakeholder.test.tsx.snap (97%) rename src/{pages/controls/business-services => shared}/components/select-stakeholder/tests/select-stakeholder.test.tsx (100%) create mode 100644 src/shared/hooks/useDeleteStakeholder/index.ts create mode 100644 src/shared/hooks/useDeleteStakeholder/useDeleteStakeholder.test.tsx create mode 100644 src/shared/hooks/useDeleteStakeholder/useDeleteStakeholder.ts create mode 100644 src/shared/hooks/useDeleteStakeholderGroup/index.ts create mode 100644 src/shared/hooks/useDeleteStakeholderGroup/useDeleteStakeholderGroup.test.tsx create mode 100644 src/shared/hooks/useDeleteStakeholderGroup/useDeleteStakeholderGroup.ts create mode 100644 src/shared/hooks/useFetchJobFunctions/index.ts create mode 100644 src/shared/hooks/useFetchJobFunctions/useFetchJobFunctions.test.tsx create mode 100644 src/shared/hooks/useFetchJobFunctions/useFetchJobFunctions.ts create mode 100644 src/shared/hooks/useFetchStakeholderGroups/index.ts create mode 100644 src/shared/hooks/useFetchStakeholderGroups/useFetchStakeholderGroups.test.tsx create mode 100644 src/shared/hooks/useFetchStakeholderGroups/useFetchStakeholderGroups.ts diff --git a/README.md b/README.md index 91fa24079d..27af38da9b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This project depends on other resources: - Keycloak - Controls -## Start dependencines with docker-compose +## Start dependencies with docker-compose Start the dependencies using `docker-compose.yml`: @@ -56,7 +56,7 @@ Start the controls' database: ```shell docker run -d \ --network konveyor --network-alias controls-db \ --p 5432:5432 \ +-p 5433:5432 \ -e POSTGRES_USER=user \ -e POSTGRES_PASSWORD=password \ -e POSTGRES_DB=controls_db \ @@ -68,7 +68,7 @@ Start the controls: ```shell docker run -d \ --network konveyor --network-alias controls \ --p 8080:8080 \ +-p 8081:8080 \ -e QUARKUS_HTTP_PORT=8080 \ -e QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://controls-db:5432/controls_db \ -e QUARKUS_DATASOURCE_USERNAME=user \ @@ -94,3 +94,57 @@ yarn start ``` You should be able to open http://localhost:3000 and start working on the UI. + +# Use tackle-controls in dev mode + +Fork/clone the `tackle-controls` repository: + +```shell +git clone https://github.com/konveyor/tackle-controls +``` + +Start a database which will be used by the `tackle-controls` project: + +```shell +docker run -d -p 5432:5432 \ +-e POSTGRES_USER=username \ +-e POSTGRES_PASSWORD=password \ +-e POSTGRES_DB=controls_db \ +postgres:13.1 +``` + +Move your terminal to the `tackle-controls` repository you cloned and then: + +```shell +./mvnw quarkus:dev \ +-Dquarkus.http.port=8080 \ +-Dquarkus.datasource.username=username \ +-Dquarkus.datasource.password=password \ +-Dquarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/controls_db \ +-Dquarkus.oidc.client-id=controls-api \ +-Dquarkus.oidc.credentials.secret=secret \ +-Dquarkus.oidc.auth-server-url=http://localhost:8180/auth/realms/konveyor +``` + +Finally, open `src/setupProxy.js` and change the port (from 8081 to 8080) of the `/api/controls` endpoint. It should look like: + +```javascript +module.exports = function (app) { + app.use( + "/api/controls", + createProxyMiddleware({ + target: "http://localhost:8080", + changeOrigin: true, + pathRewrite: { + "^/api/controls": "/controls", + }, + }) + ); +}; +``` + +You need to restart the local ui server. Stop the ui server and then execute: + +```shell +yarn start +``` diff --git a/cypress/integration/businessServiceList.test.js b/cypress/integration/controls/business-service/businessServiceList.test.js similarity index 84% rename from cypress/integration/businessServiceList.test.js rename to cypress/integration/controls/business-service/businessServiceList.test.js index 93e5d62ae1..3065824b8e 100644 --- a/cypress/integration/businessServiceList.test.js +++ b/cypress/integration/controls/business-service/businessServiceList.test.js @@ -57,7 +57,7 @@ context("Test business service list", () => { method: "POST", headers: headers, body: { - name: `any`, + email: `email${i}@domain.com`, displayName: `stakeholder${i}`, }, url: `${Cypress.env("controls_base_url")}/stakeholder`, @@ -97,19 +97,56 @@ context("Test business service list", () => { cy.wait("@apiCheck"); cy.get("tbody > tr").should("have.length", 10); - // Apply first filter + // Apply first filter: 'byName' cy.get("input[aria-label='filter-text']").type("service12"); cy.get("button[aria-label='search']").click(); cy.wait("@apiCheck"); cy.get("tbody > tr").should("have.length", 1).contains("service12"); - // Apply second filter + // Apply second filter: 'byName' cy.get("input[aria-label='filter-text']").type("service5"); cy.get("button[aria-label='search']").click(); cy.wait("@apiCheck"); - cy.get("tbody > tr").should("have.length", 2).contains("service5"); + cy.get("tbody > tr") + .should("have.length", 2) + .should("contain", "service12") + .should("contain", "service5"); + + // Apply second filter: 'byOwner' + cy.get(".pf-c-toolbar button.pf-c-dropdown__toggle").click(); + cy.get(".pf-c-dropdown__menu button.pf-c-dropdown__menu-item") + .eq(2) + .click(); + + cy.get("input[aria-label='filter-text']").type("stakeholder1"); + cy.get("button[aria-label='search']").click(); + + cy.wait("@apiCheck"); + cy.get("tbody > tr") + .should("have.length", 1) + .should("contain", "service12"); + + // Remove filter 'byOwner' chip + cy.get(".pf-c-chip button.pf-c-button").eq(2).click(); + + cy.wait("@apiCheck"); + cy.get("tbody > tr") + .should("have.length", 2) + .should("contain", "service12") + .should("contain", "service5"); + + // Clear all filters + cy.get(".pf-c-toolbar__item > button.pf-m-link") + .contains("Clear all filters") + .click({ force: true }); + + cy.wait("@apiCheck"); + cy.get("tbody > tr") + .should("have.length", 10) + .should("contain", "service1") + .should("contain", "service7"); }); it("Pagination", () => { diff --git a/cypress/integration/controls/business-service/newBusinessService.test.js b/cypress/integration/controls/business-service/newBusinessService.test.js new file mode 100644 index 0000000000..f3e2e7f000 --- /dev/null +++ b/cypress/integration/controls/business-service/newBusinessService.test.js @@ -0,0 +1,137 @@ +/// + +context("Test NewBusinessService", () => { + beforeEach(() => { + cy.kcLogout(); + cy.kcLogin("alice").as("tokens"); + + cy.get("@tokens").then((tokens) => { + const headers = { + "Content-Type": "application/json", + Accept: "application/hal+json", + Authorization: "Bearer " + tokens.access_token, + }; + + // Delete all business services + cy.request({ + method: "GET", + headers: headers, + url: `${Cypress.env("controls_base_url")}/business-service?size=1000`, + }) + .then((result) => { + result.body._embedded["business-service"].forEach((e) => { + cy.request({ + method: "DELETE", + headers: headers, + url: `${Cypress.env("controls_base_url")}/business-service/${ + e.id + }`, + }); + }); + }) + + // Delete all stakeholders + .then(() => { + cy.request({ + method: "GET", + headers: headers, + url: `${Cypress.env("controls_base_url")}/stakeholder?size=1000`, + }); + }) + .then((response) => { + response.body._embedded["stakeholder"].forEach((elem) => { + cy.request({ + method: "DELETE", + headers: headers, + url: `${Cypress.env("controls_base_url")}/stakeholder/${elem.id}`, + }); + }); + }) + + // Create stakeholders + .then(() => { + for (let i = 1; i <= 12; i++) { + cy.request({ + method: "POST", + headers: headers, + body: { + email: `email${i}@domain.com`, + displayName: `stakeholder${i}`, + }, + url: `${Cypress.env("controls_base_url")}/stakeholder`, + }); + } + }); + }); + }); + + it("Minimun data", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/business-service", + }).as("apiCheck"); + + cy.visit("/controls/business-services"); + + // Open modal + cy.get("button[aria-label='create-business-service']").click(); + + // Verify primary button is disabled + cy.get("button[aria-label='submit']").should("be.disabled"); + + // Fill form + cy.get("input[name='name']").type("my business service"); + + cy.get("button[aria-label='submit']").should("not.be.disabled"); + cy.get("form").submit(); + + cy.wait("@apiCheck"); + + // Verify table + cy.get("tbody > tr") + .should("have.length", 1) + .contains("my business service"); + }); + + it("Fill all fields", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/business-service", + }).as("apiCheckBusinessService"); + + cy.intercept({ + method: "GET", + url: "/api/controls/stakeholder", + }).as("apiCheckStakeholder"); + + cy.visit("/controls/business-services"); + + // Open modal + cy.get("button[aria-label='create-business-service']").click(); + + // Verify primary button is disabled + cy.get("button[aria-label='submit']").should("be.disabled"); + + // Fill form + cy.wait("@apiCheckStakeholder"); + cy.get("input[name='name']").type("my name"); + cy.get("textarea[name='description']").type("my description"); + + cy.get("button.pf-c-select__toggle-button").click(); + cy.get("button.pf-c-select__menu-item") + .eq(0) + .click({ waitForAnimations: false }); + + cy.get("button[aria-label='submit']").should("not.be.disabled"); + cy.get("form").submit(); + + cy.wait("@apiCheckBusinessService"); + + // Verify table + cy.get("tbody > tr") + .should("have.length", 1) + .should("contain", "my name") + .and("contain", "my description") + .and("contain", "stakeholder1"); + }); +}); diff --git a/cypress/integration/controls/stakeholder/stakeholderList.test.js b/cypress/integration/controls/stakeholder/stakeholderList.test.js new file mode 100644 index 0000000000..1bd764b065 --- /dev/null +++ b/cypress/integration/controls/stakeholder/stakeholderList.test.js @@ -0,0 +1,274 @@ +/// + +context("Test business service list", () => { + beforeEach(() => { + cy.kcLogout(); + cy.kcLogin("alice").as("tokens"); + + cy.get("@tokens").then((tokens) => { + const headers = { + "Content-Type": "application/json", + Accept: "application/hal+json", + Authorization: "Bearer " + tokens.access_token, + }; + + const stakeholders = []; + + // Delete stakeholders + cy.request({ + method: "GET", + headers: headers, + url: `${Cypress.env("controls_base_url")}/stakeholder?size=1000`, + }) + .then((response) => { + response.body._embedded["stakeholder"].forEach((elem) => { + cy.request({ + method: "DELETE", + headers: headers, + url: `${Cypress.env("controls_base_url")}/stakeholder/${elem.id}`, + }); + }); + }) + + // Create stakeholders + .then(() => { + for (let i = 1; i <= 12; i++) { + cy.request({ + method: "POST", + headers: headers, + body: { + email: `email-${(i + 9).toString(36)}@domain.com`, + displayName: `stakeholder${i}`, + }, + url: `${Cypress.env("controls_base_url")}/stakeholder`, + }).then((response) => { + stakeholders.push(response.body); + }); + } + }); + }); + }); + + it("Filtering", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/stakeholder", + }).as("apiCheck"); + + cy.visit("/controls/stakeholders"); + + // + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 10); + + // Apply first filter: 'byEmail' + cy.get("input[aria-label='filter-text']").type("email-l"); + cy.get("button[aria-label='search']").click(); + + cy.wait("@apiCheck"); + cy.get("tbody > tr") + .should("have.length", 1) + .contains("email-l@domain.com"); + + // Apply second filter: 'byEmail' + cy.get("input[aria-label='filter-text']").type("email-e"); + cy.get("button[aria-label='search']").click(); + + cy.wait("@apiCheck"); + cy.get("tbody > tr") + .should("have.length", 2) + .should("contain", "email-l@domain.com") + .should("contain", "email-e@domain.com"); + + // Apply second filter: 'byDisplayName' + cy.get(".pf-c-toolbar button.pf-c-dropdown__toggle").click(); + cy.get(".pf-c-dropdown__menu button.pf-c-dropdown__menu-item") + .eq(1) + .click(); + + cy.get("input[aria-label='filter-text']").type("stakeholder1"); + cy.get("button[aria-label='search']").click(); + + cy.wait("@apiCheck"); + cy.get("tbody > tr") + .should("have.length", 1) + .should("contain", "stakeholder12"); + + // Clear all filters + cy.get(".pf-c-toolbar__item > button.pf-m-link") + .contains("Clear all filters") + .click({ force: true }); + + cy.wait("@apiCheck"); + cy.get("tbody > tr") + .should("have.length", 10) + .should("contain", "stakeholder1") + .should("contain", "stakeholder10"); + }); + + it("Pagination", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/stakeholder", + }).as("apiCheck"); + + cy.visit("/controls/stakeholders"); + + // Remember that by default the table is sorted by name + + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 10); + cy.get("tbody > tr").contains("stakeholder1"); + cy.get("tbody > tr").contains("stakeholder10"); + + cy.get("button[data-action='next']").first().click(); + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 2); + cy.get("tbody > tr").contains("stakeholder11"); + cy.get("tbody > tr").contains("stakeholder12"); + + cy.get("button[data-action='previous']").first().click(); + cy.wait("@apiCheck"); + cy.get("tbody > tr").should("have.length", 10); + cy.get("tbody > tr").contains("stakeholder1"); + cy.get("tbody > tr").contains("stakeholder10"); + }); + + it("Sorting", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/stakeholder", + }).as("apiCheck"); + + cy.visit("/controls/stakeholders"); + + // Verify default sort + cy.wait("@apiCheck"); + cy.get("th.pf-c-table__sort") + .first() + .should("have.attr", "aria-sort", "ascending") + .contains("Email"); + cy.get("tbody > tr").eq(0).contains("stakeholder1"); + cy.get("tbody > tr").eq(9).contains("stakeholder10"); + + // Reverse sort + cy.get("th.pf-c-table__sort > button").first().click(); + cy.wait("@apiCheck"); + cy.get("th.pf-c-table__sort") + .first() + .should("have.attr", "aria-sort", "descending") + .contains("Email"); + cy.get("tbody > tr").eq(0).contains("stakeholder12"); + cy.get("tbody > tr").eq(9).contains("stakeholder3"); + + // Sort by displayName + cy.get("th.pf-c-table__sort > button").contains("Display name").click(); + cy.wait("@apiCheck"); + cy.get("th.pf-c-table__sort") + .eq(1) + .should("have.attr", "aria-sort", "ascending") + .contains("Display name"); + cy.get("tbody > tr").eq(0).contains("stakeholder1"); + cy.get("tbody > tr").eq(9).contains("stakeholder7"); + + // Reverse sort + cy.get("th.pf-c-table__sort > button").contains("Display name").click(); + cy.wait("@apiCheck"); + cy.get("th.pf-c-table__sort") + .eq(1) + .should("have.attr", "aria-sort", "descending") + .contains("Display name"); + cy.get("tbody > tr").eq(0).contains("stakeholder9"); + cy.get("tbody > tr").eq(9).contains("stakeholder11"); + + // TODO test by job function and owner + }); + + it("Create new - mininum data", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/stakeholder", + }).as("apiCheck"); + + cy.visit("/controls/stakeholders"); + + // Open modal + cy.get("button[aria-label='create-stakeholder']").click(); + + // Verify primary button is disabled + cy.get("button[aria-label='submit']").should("be.disabled"); + + // Fill form + cy.get("input[name='email']").type("aaa@domain.com"); + cy.get("input[name='displayName']").type("myDisplayName"); + + cy.get("button[aria-label='submit']").should("not.be.disabled"); + cy.get("form").submit(); + + cy.wait("@apiCheck"); + + // Verify table + cy.get("tbody > tr") + .should("contain", "aaa@domain.com") + .should("contain", "myDisplayName"); + }); + + it("Edit", () => { + cy.intercept({ + method: "GET", + url: "/api/controls/stakeholder", + }).as("apiCheck"); + + cy.visit("/controls/stakeholders"); + + cy.wait("@apiCheck"); + + // Open modal + cy.get("button[aria-label='edit']").first().click(); + + // Verify primary button is disabled + cy.get("button[aria-label='submit']").should("be.disabled"); + + // Fill form + cy.get("input[name='email']").clear().type("aaa@domain.com"); + cy.get("input[name='displayName']").clear().type("myDisplayName"); + + cy.get("button[aria-label='submit']").should("not.be.disabled"); + cy.get("form").submit(); + + cy.wait("@apiCheck"); + + // Verify table + cy.get("tbody > tr").contains("aaa@domain.com"); + cy.get("tbody > tr").contains("myDisplayName"); + }); + + it("Delete", () => { + cy.intercept({ + method: "GET", + path: "/api/controls/stakeholder*", + }).as("apiCheck"); + + cy.intercept({ + method: "DELETE", + path: "/api/controls/stakeholder/*", + }).as("apiDeleteCheck"); + + cy.visit("/controls/stakeholders"); + + cy.wait("@apiCheck"); + + // Verify table has 12 elements + cy.get(".pf-c-options-menu__toggle-text").contains(12); + + // Open delete modal + cy.get("button[aria-label='delete']").first().click(); + cy.get("button[aria-label='confirm']").click(); + + cy.wait("@apiDeleteCheck"); + cy.wait("@apiCheck"); + + // Verify company has been deleted + cy.get(".pf-c-options-menu__toggle-text").contains(11); + }); +}); diff --git a/cypress/integration/newBusinessService.test.js b/cypress/integration/newBusinessService.test.js deleted file mode 100644 index 7918af4367..0000000000 --- a/cypress/integration/newBusinessService.test.js +++ /dev/null @@ -1,59 +0,0 @@ -/// - -context("Test NewBusinessService", () => { - beforeEach(() => { - cy.kcLogout(); - cy.kcLogin("alice").as("tokens"); - - cy.get("@tokens").then((tokens) => { - const headers = { - "Content-Type": "application/json", - "Accept": "application/hal+json", - Authorization: "Bearer " + tokens.access_token, - }; - - // Delete all business services - cy.request({ - method: "GET", - headers: headers, - url: `${Cypress.env("controls_base_url")}/business-service?size=1000`, - }).then((result) => { - result.body._embedded["business-service"].forEach((e) => { - cy.request({ - method: "DELETE", - headers: headers, - url: `${Cypress.env("controls_base_url")}/business-service/${e.id}`, - }); - }); - }); - }); - }); - - it("Minimun data", () => { - cy.intercept({ - method: "GET", - url: "/api/controls/business-service", - }).as("apiCheck"); - - cy.visit("/controls/business-services"); - - // Open modal - cy.get("button[aria-label='create-business-service']").click(); - - // Verify primary button is disabled - cy.get("button[aria-label='submit']").should("be.disabled"); - - // Fill form - cy.get("input[name='name']").type("my business service"); - - cy.get("button[aria-label='submit']").should("not.be.disabled"); - cy.get("form").submit(); - - cy.wait("@apiCheck"); - - // Verify table - cy.get("tbody > tr") - .should("have.length", 1) - .contains("my business service"); - }); -}); diff --git a/package.json b/package.json index 72d3f06726..d4f75a28af 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "0.1", "private": true, "dependencies": { - "@patternfly/patternfly": "4.82.0", - "@patternfly/react-core": "4.92.1", - "@patternfly/react-table": "4.20.21", + "@konveyor/lib-ui": "^2.0.0", + "@patternfly/patternfly": "4.87.3", + "@patternfly/react-core": "4.97.3", + "@patternfly/react-table": "4.23.3", "@react-keycloak/web": "^3.4.0", "@redhat-cloud-services/frontend-components-notifications": "3.0.3", "@testing-library/jest-dom": "^5.11.4", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 52326e9749..e41d44e99f 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -12,7 +12,14 @@ "businessServices": "Business services", "stakeholders": "Stakeholders", "stakeholderGroups": "Stakeholder groups", - "tags": "Tags" + "tags": "Tags", + "member(s)": "Member(s)", + "member": "Member", + "email": "Email", + "displayName": "Display name", + "jobFunction": "Job function", + "group": "Group", + "group(s)": "Group(s)" }, "actions": { "edit": "Edit", @@ -26,7 +33,11 @@ "title": { "delete": "Delete '{{what}}'", "newBusinessService": "New business service", - "updateBusinessService": "Update business service" + "updateBusinessService": "Update business service", + "newStakeholderGroup": "New stakeholder group", + "updateStakeholderGroup": "Update stakeholder group", + "newStakeholder": "New stakeholder", + "updateStakeholder": "Update stakeholder" }, "message": { "delete": "Are you sure you want to delete '{{what}}'? This will delete all resources associated with it and cannot be undone. Make sure this is something you really want to do!" @@ -41,6 +52,7 @@ "required": "This field is required.", "minLength": "This field must contain at least {{length}} characters.", "maxLength": "This field must contain fewer than {{length}} characters.", - "onlyCharactersAndUnderscore": "This field must contain only alphanumeric characters including underscore." + "onlyCharactersAndUnderscore": "This field must contain only alphanumeric characters including underscore.", + "email": "This field requires a valid email." } -} \ No newline at end of file +} diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index f126b339c4..dc60a32a34 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -12,7 +12,14 @@ "businessServices": "Servicios de negocio", "stakeholders": "Interesados", "stakeholderGroups": "Grupos de interesados", - "tags": "Etiquetas" + "tags": "Etiquetas", + "member(s)": "Miembro(s)", + "member": "Miembro", + "email": "Email", + "displayName": "Nombre a mostrar", + "jobFunction": "Función laboral", + "group": "Grupo", + "group(s)": "Grupo(s)" }, "actions": { "edit": "Editar", @@ -26,7 +33,11 @@ "title": { "delete": "Eliminar '{{what}}'", "newBusinessService": "Nuevo servicio de negocio", - "updateBusinessService": "Actualizar servicio de negocio" + "updateBusinessService": "Actualizar servicio de negocio", + "newStakeholderGroup": "Nuevo grupo de interesados", + "updateStakeholderGroup": "Actualizar grupo de interesados", + "newStakeholder": "Nuevo interesado", + "updateStakeholder": "Actualizar interesado" }, "message": { "delete": "¿Estás seguro de querer eliminar '{{what}}'?. Esta acción eliminará todos los recursos asociados al mismo y no puede ser revertida. ¡Asegúrate de que esto es lo que realmente quieres!" @@ -41,6 +52,7 @@ "required": "Este campo es requerido.", "minLength": "Este campo debe de tener al menos {{length}} caracteres.", "maxLength": "Este campo debe de tener menos de {{length}} caracteres.", - "onlyCharactersAndUnderscore": "Este campo debe de contener solo caracteres alfanuméricos incluyendo el subguión." + "onlyCharactersAndUnderscore": "Este campo debe de contener solo caracteres alfanuméricos incluyendo el subguión.", + "email": "Este campo requiere un email válido." } -} \ No newline at end of file +} diff --git a/src/api/models.tsx b/src/api/models.tsx index 20474d9874..87fed28fe0 100644 --- a/src/api/models.tsx +++ b/src/api/models.tsx @@ -25,8 +25,23 @@ export interface BusinessService { } export interface Stakeholder { + id?: number; displayName: string; email: string; + jobFunction?: JobFunction; + groups?: StakeholderGroup[]; +} + +export interface StakeholderGroup { + id?: number; + name: string; + description: string; + members?: Stakeholder[]; +} + +export interface JobFunction { + id?: number; + role: string; } export interface BusinessServicePage { @@ -42,3 +57,17 @@ export interface StakeholderPage { }; total_count: number; } + +export interface StakeholderGroupPage { + _embedded: { + "stakeholder-group": StakeholderGroup[]; + }; + total_count: number; +} + +export interface JobFunctionPage { + _embedded: { + "job-function": JobFunction[]; + }; + total_count: number; +} diff --git a/src/api/rest.tsx b/src/api/rest.tsx index eb8a34cb3c..b0d8d4f416 100644 --- a/src/api/rest.tsx +++ b/src/api/rest.tsx @@ -2,15 +2,21 @@ import { AxiosPromise } from "axios"; import { APIClient } from "axios-config"; import { + PageQuery, BusinessService, BusinessServicePage, - PageQuery, + Stakeholder, StakeholderPage, + StakeholderGroup, + StakeholderGroupPage, + JobFunctionPage, } from "./models"; export const BASE_URL = "controls"; export const BUSINESS_SERVICES = BASE_URL + "/business-service"; export const STAKEHOLDERS = BASE_URL + "/stakeholder"; +export const STAKEHOLDER_GROUPS = BASE_URL + "/stakeholder-group"; +export const JOB_FUNCTIONS = BASE_URL + "/job-function"; const headers = { Accept: "application/hal+json" }; @@ -58,6 +64,7 @@ export const getBusinessServices = ( page: pagination.page - 1, size: pagination.perPage, sort: sortByQuery, + name: filters.name, description: filters.description, "owner.displayName": filters.owner, @@ -101,6 +108,8 @@ export const updateBusinessService = ( export enum StakeholderSortBy { EMAIL, DISPLAY_NAME, + JOB_FUNCTION, + GROUP, } export interface StakeholderSortByQuery { field: StakeholderSortBy; @@ -109,7 +118,10 @@ export interface StakeholderSortByQuery { export const getStakeholders = ( filters: { - filterText?: string; + email?: string[]; + displayName?: string[]; + jobFuction?: string[]; + group?: string[]; }, pagination: PageQuery, sortBy?: StakeholderSortByQuery @@ -124,6 +136,12 @@ export const getStakeholders = ( case StakeholderSortBy.DISPLAY_NAME: field = "displayName"; break; + case StakeholderSortBy.JOB_FUNCTION: + field = "jobFunction"; + break; + case StakeholderSortBy.GROUP: + field = "group"; + break; default: throw new Error("Could not define SortBy field name"); } @@ -136,12 +154,24 @@ export const getStakeholders = ( page: pagination.page - 1, size: pagination.perPage, sort: sortByQuery, - filter: filters.filterText, + + email: filters.email, + displayName: filters.displayName, + jobFunction: filters.jobFuction, + group: filters.group, }; + Object.keys(params).forEach((key) => { const value = (params as any)[key]; - if (value !== undefined) { - query.push(`${key}=${value}`); + + if (value !== undefined && value !== null) { + let queryParamValues: string[] = []; + if (Array.isArray(value)) { + queryParamValues = value; + } else { + queryParamValues = [value]; + } + queryParamValues.forEach((v) => query.push(`${key}=${v}`)); } }); @@ -151,3 +181,110 @@ export const getStakeholders = ( export const getAllStakeholders = (): AxiosPromise => { return APIClient.get(`${STAKEHOLDERS}?size=1000`, { headers }); }; + +export const deleteStakeholder = (id: number): AxiosPromise => { + return APIClient.delete(`${STAKEHOLDERS}/${id}`); +}; + +export const createStakeholder = ( + obj: Stakeholder +): AxiosPromise => { + return APIClient.post(`${STAKEHOLDERS}`, obj); +}; + +export const updateStakeholder = ( + obj: Stakeholder +): AxiosPromise => { + return APIClient.put(`${STAKEHOLDERS}/${obj.id}`, obj); +}; + +// Stakeholder groups + +export enum StakeholderGroupSortBy { + NAME, + MEMBERS, +} +export interface StakeholderGroupSortByQuery { + field: StakeholderGroupSortBy; + direction?: Direction; +} + +export const getStakeholderGroups = ( + filters: { + name?: string[]; + description?: string[]; + member?: string[]; + }, + pagination: PageQuery, + sortBy?: StakeholderGroupSortByQuery +): AxiosPromise => { + let sortByQuery: string | undefined = undefined; + if (sortBy) { + let field; + switch (sortBy.field) { + case StakeholderGroupSortBy.NAME: + field = "name"; + break; + case StakeholderGroupSortBy.MEMBERS: + field = "members"; + break; + default: + throw new Error("Could not define SortBy field name"); + } + sortByQuery = `${sortBy.direction === "desc" ? "-" : ""}${field}`; + } + + const query: string[] = []; + + const params = { + page: pagination.page - 1, + size: pagination.perPage, + sort: sortByQuery, + + name: filters.name, + description: filters.description, + member: filters.member, + }; + + Object.keys(params).forEach((key) => { + const value = (params as any)[key]; + + if (value !== undefined && value !== null) { + let queryParamValues: string[] = []; + if (Array.isArray(value)) { + queryParamValues = value; + } else { + queryParamValues = [value]; + } + queryParamValues.forEach((v) => query.push(`${key}=${v}`)); + } + }); + + return APIClient.get(`${STAKEHOLDER_GROUPS}?${query.join("&")}`, { headers }); +}; + +export const getAllStakeholderGroups = (): AxiosPromise => { + return APIClient.get(`${STAKEHOLDER_GROUPS}?size=1000`, { headers }); +}; + +export const deleteStakeholderGroup = (id: number): AxiosPromise => { + return APIClient.delete(`${STAKEHOLDER_GROUPS}/${id}`); +}; + +export const createStakeholderGroup = ( + obj: StakeholderGroup +): AxiosPromise => { + return APIClient.post(`${STAKEHOLDER_GROUPS}`, obj); +}; + +export const updateStakeholderGroup = ( + obj: StakeholderGroup +): AxiosPromise => { + return APIClient.put(`${STAKEHOLDER_GROUPS}/${obj.id}`, obj); +}; + +// Job functions + +export const getAllJobFunctions = (): AxiosPromise => { + return APIClient.get(`${JOB_FUNCTIONS}?size=1000`, { headers }); +}; diff --git a/src/pages/controls/business-services/business-services.tsx b/src/pages/controls/business-services/business-services.tsx index 3b78eef529..ee5cef1b44 100644 --- a/src/pages/controls/business-services/business-services.tsx +++ b/src/pages/controls/business-services/business-services.tsx @@ -9,16 +9,17 @@ import { EmptyStateBody, EmptyStateIcon, EmptyStateVariant, - Flex, - FlexItem, Title, - ToolbarChip, - ToolbarChipGroup, - ToolbarFilter, ToolbarGroup, ToolbarItem, } from "@patternfly/react-core"; -import { cellWidth, ICell, sortable, TableText } from "@patternfly/react-table"; +import { + cellWidth, + ICell, + IRow, + sortable, + TableText, +} from "@patternfly/react-table"; import { AddCircleOIcon } from "@patternfly/react-icons"; import { useDispatch } from "react-redux"; @@ -29,6 +30,9 @@ import { AppPlaceholder, ConditionalRender, AppTableWithControls, + SearchFilter, + AppTableActionButtons, + AppTableToolbarToggleGroup, } from "shared/components"; import { useTableControls, @@ -42,10 +46,6 @@ import { getAxiosErrorMessage } from "utils/utils"; import { NewBusinessServiceModal } from "./components/new-business-service-modal"; import { UpdateBusinessServiceModal } from "./components/update-business-service-modal"; -import { - FilterOption, - SearchFilter, -} from "./components/search-filter/search-filter"; enum FilterKey { NAME = "name", @@ -53,7 +53,7 @@ enum FilterKey { OWNER = "owner", } -const toBusinessServiceSortByQuery = ( +const toSortByQuery = ( sortBy?: SortByQuery ): BusinessServiceSortByQuery | undefined => { if (!sortBy) { @@ -78,19 +78,33 @@ const toBusinessServiceSortByQuery = ( }; }; -const BUSINESS_SERVICE_FIELD = "businessService"; +const ENTITY_FIELD = "entity"; // const getRow = (rowData: IRowData): BusinessService => { -// return rowData[BUSINESS_SERVICE_FIELD]; +// return rowData[ENTITY_FIELD]; // }; export const BusinessServices: React.FC = () => { const { t } = useTranslation(); const dispatch = useDispatch(); - const [nameFilters, setNameFilters] = useState([]); - const [descriptionFilters, setDescriptionFilters] = useState([]); - const [ownerFilters, setOwnerFilters] = useState([]); + const filters = [ + { + key: FilterKey.NAME, + name: t("terms.name"), + }, + { + key: FilterKey.DESCRIPTION, + name: t("terms.description"), + }, + { + key: FilterKey.OWNER, + name: t("terms.owner"), + }, + ]; + const [filtersValue, setFiltersValue] = useState>( + new Map([]) + ); const [isNewModalOpen, setIsNewModalOpen] = useState(false); const [rowToUpdate, setRowToUpdate] = useState(); @@ -116,48 +130,31 @@ export const BusinessServices: React.FC = () => { const refreshTable = useCallback(() => { fetchBusinessServices( { - name: nameFilters, - description: descriptionFilters, - owner: ownerFilters, + name: filtersValue.get(FilterKey.NAME), + description: filtersValue.get(FilterKey.DESCRIPTION), + owner: filtersValue.get(FilterKey.OWNER), }, paginationQuery, - toBusinessServiceSortByQuery(sortByQuery) + toSortByQuery(sortByQuery) ); - }, [ - nameFilters, - descriptionFilters, - ownerFilters, - paginationQuery, - sortByQuery, - fetchBusinessServices, - ]); + }, [filtersValue, paginationQuery, sortByQuery, fetchBusinessServices]); useEffect(() => { fetchBusinessServices( { - name: nameFilters, - description: descriptionFilters, - owner: ownerFilters, + name: filtersValue.get(FilterKey.NAME), + description: filtersValue.get(FilterKey.DESCRIPTION), + owner: filtersValue.get(FilterKey.OWNER), }, paginationQuery, - toBusinessServiceSortByQuery(sortByQuery) + toSortByQuery(sortByQuery) ); - }, [ - nameFilters, - descriptionFilters, - ownerFilters, - paginationQuery, - sortByQuery, - fetchBusinessServices, - ]); + }, [filtersValue, paginationQuery, sortByQuery, fetchBusinessServices]); const columns: ICell[] = [ - { title: t("terms.name"), transforms: [sortable, cellWidth(30)] }, - { title: t("terms.description"), transforms: [cellWidth(40)] }, - { - title: t("terms.owner"), - transforms: [sortable], - }, + { title: t("terms.name"), transforms: [sortable, cellWidth(25)] }, + { title: t("terms.description"), transforms: [cellWidth(35)] }, + { title: t("terms.owner"), transforms: [sortable] }, { title: "", props: { @@ -166,9 +163,10 @@ export const BusinessServices: React.FC = () => { }, ]; - const itemsToRow = (items: BusinessService[]) => { - return items.map((item) => ({ - [BUSINESS_SERVICE_FIELD]: item, + const rows: IRow[] = []; + businessServices?.data.forEach((item) => { + rows.push({ + [ENTITY_FIELD]: item, cells: [ { title: item.name, @@ -183,31 +181,17 @@ export const BusinessServices: React.FC = () => { }, { title: ( - - - - - - - - + editRow(item)} + onDelete={() => deleteRow(item)} + /> ), }, ], - })); - }; + }); + }); + + // Rows // const actions: IActions = [ // { @@ -266,86 +250,29 @@ export const BusinessServices: React.FC = () => { // Advanced filters - const filterOptions: FilterOption[] = [ - { - key: FilterKey.NAME, - name: t("terms.name"), - }, - { - key: FilterKey.DESCRIPTION, - name: t("terms.description"), - }, - { - key: FilterKey.OWNER, - name: t("terms.owner"), - }, - ]; - const handleOnClearAllFilters = () => { - setNameFilters([]); - setDescriptionFilters([]); - setOwnerFilters([]); + setFiltersValue((current) => { + const newVal = new Map(current); + Array.from(newVal.keys()).forEach((key) => { + newVal.set(key, []); + }); + return newVal; + }); }; - const handleOnFilterApplied = (key: string, filterText: string) => { - if (key === FilterKey.NAME) { - setNameFilters([...nameFilters, filterText]); - } else if (key === FilterKey.DESCRIPTION) { - setDescriptionFilters([...descriptionFilters, filterText]); - } else if (key === FilterKey.OWNER) { - setOwnerFilters([...ownerFilters, filterText]); - } else { - throw new Error("Can not apply filter " + key + ". It's not supported"); - } + const handleOnAddFilter = (key: string, filterText: string) => { + const filterKey: FilterKey = key as FilterKey; + setFiltersValue((current) => { + const values: string[] = current.get(filterKey) || []; + return new Map(current).set(filterKey, [...values, filterText]); + }); handlePaginationChange({ page: 1 }); }; - const handleOnDeleteFilter = ( - category: string | ToolbarChipGroup, - chip: ToolbarChip | string - ) => { - if (typeof chip !== "string") { - throw new Error("Can not delete filter. Chip must be a string"); - } - - let categoryKey: string; - if (typeof category === "string") { - categoryKey = category; - } else { - categoryKey = category.key; - } - - if (categoryKey === FilterKey.NAME) { - setNameFilters(nameFilters.filter((f) => f !== chip)); - } else if (categoryKey === FilterKey.DESCRIPTION) { - setDescriptionFilters(descriptionFilters.filter((f) => f !== chip)); - } else if (categoryKey === FilterKey.OWNER) { - setOwnerFilters(ownerFilters.filter((f) => f !== chip)); - } else { - throw new Error( - "Can not delete chip. Chip " + chip + " is not supported" - ); - } - }; - - const handleOnDeleteFilterGroup = (category: string | ToolbarChipGroup) => { - let categoryKey: string; - if (typeof category === "string") { - categoryKey = category; - } else { - categoryKey = category.key; - } - - if (categoryKey === FilterKey.NAME) { - setNameFilters([]); - } else if (categoryKey === FilterKey.DESCRIPTION) { - setDescriptionFilters([]); - } else if (categoryKey === FilterKey.OWNER) { - setOwnerFilters([]); - } else { - throw new Error("Can not delete ChipGroup. ChipGroup is not supported"); - } + const handleOnDeleteFilter = (key: string, value: string[]) => { + const filterKey: FilterKey = key as FilterKey; + setFiltersValue((current) => new Map(current).set(filterKey, value)); }; // Create Modal @@ -393,60 +320,34 @@ export const BusinessServices: React.FC = () => { > - 0 + Array.from(filtersValue.values()).reduce( + (current, accumulator) => [...accumulator, ...current], + [] + ).length > 0 } toolbarToggle={ - - - {null} - - - {null} - - - - - + + + } toolbar={ diff --git a/src/pages/controls/business-services/components/business-service-form/stories/new-business-service-form.stories.tsx b/src/pages/controls/business-services/components/business-service-form/stories/business-service-form.stories.tsx similarity index 93% rename from src/pages/controls/business-services/components/business-service-form/stories/new-business-service-form.stories.tsx rename to src/pages/controls/business-services/components/business-service-form/stories/business-service-form.stories.tsx index 718966a192..a98ec24de4 100644 --- a/src/pages/controls/business-services/components/business-service-form/stories/new-business-service-form.stories.tsx +++ b/src/pages/controls/business-services/components/business-service-form/stories/business-service-form.stories.tsx @@ -7,7 +7,7 @@ import { import { Modal } from "@patternfly/react-core"; export default { - title: "Components / NewBusinessServiceForm", + title: "BusinessServices / BusinessServiceForm", component: BusinessServiceForm, argTypes: { onCancel: { action: "onCancel" }, diff --git a/src/pages/controls/business-services/components/business-service-form/tests/new-business-service-form.test.tsx b/src/pages/controls/business-services/components/business-service-form/tests/new-business-service-form.test.tsx deleted file mode 100644 index 39e9ebd286..0000000000 --- a/src/pages/controls/business-services/components/business-service-form/tests/new-business-service-form.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { mount, shallow } from "enzyme"; - -import { BusinessServiceForm } from "../business-service-form"; -import { Button } from "@patternfly/react-core"; - -describe("NewBusinessServiceForm", () => { - it("Renders without crashing", () => { - // const wrapper = shallow(); - // expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/src/pages/controls/business-services/components/search-filter/index.ts b/src/pages/controls/business-services/components/search-filter/index.ts deleted file mode 100644 index eca0583751..0000000000 --- a/src/pages/controls/business-services/components/search-filter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SearchFilter as NewBusinessServiceModal } from "./search-filter"; diff --git a/src/pages/controls/business-services/components/select-stakeholder-form-field/select-stakeholder-field.tsx b/src/pages/controls/business-services/components/select-stakeholder-form-field/select-stakeholder-field.tsx index 80e02d8efb..9ea5ad8d29 100644 --- a/src/pages/controls/business-services/components/select-stakeholder-form-field/select-stakeholder-field.tsx +++ b/src/pages/controls/business-services/components/select-stakeholder-form-field/select-stakeholder-field.tsx @@ -2,10 +2,9 @@ import React from "react"; import { AxiosError } from "axios"; import { FieldHookConfig, useField } from "formik"; +import { SelectStakeholder } from "shared/components"; import { Stakeholder } from "api/models"; -import { SelectStakeholder } from "../select-stakeholder"; - export interface SelectStakeholderFormFieldProps { stakeholders: Stakeholder[]; isFetching: boolean; @@ -17,7 +16,11 @@ export const SelectStakeholderFormField: React.FC< > = ({ stakeholders, isFetching, fetchError, ...props }) => { const [field, , helpers] = useField(props); - const handleOnSelect = (value: Stakeholder) => { + const handleOnSelect = (value: Stakeholder | Stakeholder[]) => { + if (Array.isArray(value)) { + throw new Error("Component was expecting a value not an array"); + } + helpers.setValue(value); }; @@ -27,6 +30,8 @@ export const SelectStakeholderFormField: React.FC< return ( { breadcrumbs={[]} menuActions={[]} navItems={[ - { - title: t("terms.businessServices"), - path: Paths.controls_businessServices, - }, { title: t("terms.stakeholders"), path: Paths.controls_stakeholders, @@ -26,6 +22,10 @@ export const EditCompanyHeader: React.FC = () => { title: t("terms.stakeholderGroups"), path: Paths.controls_stakeholderGroups, }, + { + title: t("terms.businessServices"), + path: Paths.controls_businessServices, + }, { title: t("terms.tags"), path: Paths.controls_tags, diff --git a/src/pages/controls/controls.tsx b/src/pages/controls/controls.tsx index 88bdc4f9cb..6b8b42a22b 100644 --- a/src/pages/controls/controls.tsx +++ b/src/pages/controls/controls.tsx @@ -7,9 +7,9 @@ import { AppPlaceholder } from "shared/components"; import { EditCompanyHeader } from "./controls-header"; -const businessServices = lazy(() => import("./business-services")); const Stakeholders = lazy(() => import("./stakeholders")); const StakeholderGroups = lazy(() => import("./stakeholder-groups")); +const businessServices = lazy(() => import("./business-services")); const Tags = lazy(() => import("./tags")); export const Controls: React.FC = () => { @@ -19,10 +19,6 @@ export const Controls: React.FC = () => { }> - { path={Paths.controls_stakeholderGroups} component={StakeholderGroups} /> + diff --git a/src/pages/controls/stakeholder-groups/components/new-stakeholder-group-modal/index.ts b/src/pages/controls/stakeholder-groups/components/new-stakeholder-group-modal/index.ts new file mode 100644 index 0000000000..6c56d9d0c6 --- /dev/null +++ b/src/pages/controls/stakeholder-groups/components/new-stakeholder-group-modal/index.ts @@ -0,0 +1 @@ +export { NewStakeholderGroupModal } from "./new-stakeholder-group-modal"; diff --git a/src/pages/controls/stakeholder-groups/components/new-stakeholder-group-modal/new-stakeholder-group-modal.tsx b/src/pages/controls/stakeholder-groups/components/new-stakeholder-group-modal/new-stakeholder-group-modal.tsx new file mode 100644 index 0000000000..905ee2645c --- /dev/null +++ b/src/pages/controls/stakeholder-groups/components/new-stakeholder-group-modal/new-stakeholder-group-modal.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { AxiosResponse } from "axios"; +import { useTranslation } from "react-i18next"; + +import { Modal, ModalVariant } from "@patternfly/react-core"; + +import { StakeholderGroup } from "api/models"; + +import { StakeholderGroupForm } from "../stakeholder-group-form"; + +export interface NewStakeholderGroupModalProps { + isOpen: boolean; + onSaved: (response: AxiosResponse) => void; + onCancel: () => void; +} + +export const NewStakeholderGroupModal: React.FC = ({ + isOpen, + onSaved, + onCancel, +}) => { + const { t } = useTranslation(); + + return ( + + + + ); +}; diff --git a/src/pages/controls/stakeholder-groups/components/select-member-form-field/index.ts b/src/pages/controls/stakeholder-groups/components/select-member-form-field/index.ts new file mode 100644 index 0000000000..aa08dcc227 --- /dev/null +++ b/src/pages/controls/stakeholder-groups/components/select-member-form-field/index.ts @@ -0,0 +1 @@ +export { SelectMemberFormField } from "./select-member-form-field"; diff --git a/src/pages/controls/stakeholder-groups/components/select-member-form-field/select-member-form-field.tsx b/src/pages/controls/stakeholder-groups/components/select-member-form-field/select-member-form-field.tsx new file mode 100644 index 0000000000..a0344933f0 --- /dev/null +++ b/src/pages/controls/stakeholder-groups/components/select-member-form-field/select-member-form-field.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { AxiosError } from "axios"; +import { FieldHookConfig, useField } from "formik"; + +import { SelectStakeholder } from "shared/components"; +import { Stakeholder } from "api/models"; + +export interface SelectMemberFormFieldProps { + stakeholders: Stakeholder[]; + isFetching: boolean; + fetchError?: AxiosError; +} + +export const SelectMemberFormField: React.FC< + FieldHookConfig & SelectMemberFormFieldProps +> = ({ stakeholders, isFetching, fetchError, ...props }) => { + const [field, , helpers] = useField(props); + + const handleOnSelect = (value: Stakeholder | Stakeholder[]) => { + if (!Array.isArray(value)) { + throw new Error("Component was expecting an array"); + } + + helpers.setValue(value); + }; + + const handleOnClear = () => { + helpers.setValue([]); + }; + + return ( + + ); +}; diff --git a/src/pages/controls/stakeholder-groups/components/stakeholder-group-form/index.ts b/src/pages/controls/stakeholder-groups/components/stakeholder-group-form/index.ts new file mode 100644 index 0000000000..59109a69e9 --- /dev/null +++ b/src/pages/controls/stakeholder-groups/components/stakeholder-group-form/index.ts @@ -0,0 +1 @@ +export { StakeholderGroupForm } from "./stakeholder-group-form"; diff --git a/src/pages/controls/stakeholder-groups/components/stakeholder-group-form/stakeholder-group-form.tsx b/src/pages/controls/stakeholder-groups/components/stakeholder-group-form/stakeholder-group-form.tsx new file mode 100644 index 0000000000..f1acea2535 --- /dev/null +++ b/src/pages/controls/stakeholder-groups/components/stakeholder-group-form/stakeholder-group-form.tsx @@ -0,0 +1,215 @@ +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AxiosError, AxiosPromise, AxiosResponse } from "axios"; +import { useFormik, FormikProvider, FormikHelpers } from "formik"; +import { object, string } from "yup"; + +import { + ActionGroup, + Alert, + Button, + ButtonVariant, + Form, + FormGroup, + TextArea, + TextInput, +} from "@patternfly/react-core"; + +import { createStakeholderGroup, updateStakeholderGroup } from "api/rest"; +import { Stakeholder, StakeholderGroup } from "api/models"; +import { + getAxiosErrorMessage, + getValidatedFromError, + getValidatedFromErrorTouched, +} from "utils/utils"; +import { useFetchStakeholders } from "shared/hooks"; +import { SelectMemberFormField } from "../select-member-form-field"; + +export interface FormValues { + name: string; + description: string; + members: Stakeholder[]; +} + +export interface StakeholderGroupFormProps { + stakeholderGroup?: StakeholderGroup; + onSaved: (response: AxiosResponse) => void; + onCancel: () => void; +} + +export const StakeholderGroupForm: React.FC = ({ + stakeholderGroup, + onSaved, + onCancel, +}) => { + const { t } = useTranslation(); + + const [error, setError] = useState(); + + const { + stakeholders, + isFetching, + fetchError, + fetchAllStakeholders, + } = useFetchStakeholders(); + + useEffect(() => { + fetchAllStakeholders(); + }, [fetchAllStakeholders]); + + const initialValues: FormValues = { + name: stakeholderGroup?.name || "", + description: stakeholderGroup?.description || "", + members: stakeholderGroup?.members || [], + }; + + const validationSchema = object().shape({ + name: string() + .trim() + .required(t("validation.required")) + .min(3, t("validation.minLength", { length: 3 })) + .max(120, t("validation.maxLength", { length: 120 })), + description: string() + .trim() + .max(250, t("validation.maxLength", { length: 250 })), + }); + + const onSubmit = ( + formValues: FormValues, + formikHelpers: FormikHelpers + ) => { + const payload: StakeholderGroup = { + name: formValues.name, + description: formValues.description, + members: [...formValues.members], + }; + + let promise: AxiosPromise; + if (stakeholderGroup) { + promise = updateStakeholderGroup({ + ...stakeholderGroup, + ...payload, + }); + } else { + promise = createStakeholderGroup(payload); + } + + promise + .then((response) => { + formikHelpers.setSubmitting(false); + onSaved(response); + }) + .catch((error) => { + formikHelpers.setSubmitting(false); + setError(error); + }); + }; + + const formik = useFormik({ + enableReinitialize: true, + initialValues: initialValues, + validationSchema: validationSchema, + onSubmit: onSubmit, + }); + + const onChangeField = (value: string, event: React.FormEvent) => { + formik.handleChange(event); + }; + + return ( + +
+ {error && ( + + )} + + + + +