From 77f3b9d5efdb56ff7545b445c0a7fc4f5f5ed525 Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Thu, 15 Feb 2024 14:35:16 +0100 Subject: [PATCH 01/51] Specs and fixtures for profile and profile update page. --- cypress/e2e/profile.cy.ts | 150 ++++++++++++++++++++++++++ cypress/fixtures/profile_empty.json | 7 ++ cypress/fixtures/profile_updated.json | 12 +++ 3 files changed, 169 insertions(+) create mode 100644 cypress/e2e/profile.cy.ts create mode 100644 cypress/fixtures/profile_empty.json create mode 100644 cypress/fixtures/profile_updated.json diff --git a/cypress/e2e/profile.cy.ts b/cypress/e2e/profile.cy.ts new file mode 100644 index 00000000..38ac8b99 --- /dev/null +++ b/cypress/e2e/profile.cy.ts @@ -0,0 +1,150 @@ +/// +describe("/profile", () => { + before(() => { + cy.setupValidation("identified"); + }); + + beforeEach(() => { + cy.kcLogout(); + cy.kcLogin("identified").as("identifiedTokens"); + cy.visit("/profile"); + }); + + it("checks an identified user profile information", () => { + // Check if profile information is correct. + cy.intercept("GET", "http://localhost:8080/v1/users/profile", { + fixture: "profile_empty.json", + }); + cy.get("#user-id").should("contain", "identified_ui_voperson_id"); + cy.get("#identified").should("contain", "Identified"); + }); + + it("does not allow validations without updating details", () => { + cy.intercept("GET", "http://localhost:8080/v1/users/profile", { + fixture: "profile_empty.json", + }); + + cy.get("#validation-alert-warning").should( + "contain", + "You should update your personal details in order to be able to create validation requests", + ); + }); + + it("does not show assessments/subjects without validation", () => { + cy.intercept("GET", "http://localhost:8080/v1/users/profile", { + fixture: "profile_updated.json", + }); + cy.get("#assessments_section").should("have.class", "disabled"); + cy.get("#subjects_section").should("have.class", "disabled"); + }); + + it("links to validations", () => { + cy.get("#view_validations_button").should( + "have.attr", + "href", + "/validations", + ); + cy.get("#create_validation_button").should( + "have.attr", + "href", + "/validations/request", + ); + }); + + it("links to assessments", () => { + cy.get("#view_assessments_button").should( + "have.attr", + "href", + "/assessments", + ); + cy.get("#create_assessment_button").should( + "have.attr", + "href", + "/assessments/create", + ); + }); + + it("links to subjects", () => { + cy.get("#view_subjects_button").should("have.attr", "href", "/subjects"); + cy.get("#create_subject_button").should( + "have.attr", + "href", + "/subjects?create", + ); + }); +}); + +describe("/profile/update", () => { + beforeEach(() => { + cy.kcLogout(); + cy.kcLogin("identified").as("identifiedTokens"); + cy.visit("/profile/update"); + }); + + it("requires the required fields to be filled out", () => { + cy.get("#inputName").clear(); + cy.get("#inputSurname").clear(); + cy.get("#inputEmail").clear(); + cy.get("#inputOrcidId").clear(); + cy.get("#submit-button").click(); + // Check if the input element has the class "is-invalid", indicating a red outline + cy.get("#inputName").should("have.class", "is-invalid"); + cy.get("#inputSurname").should("have.class", "is-invalid"); + cy.get("#inputEmail").should("have.class", "is-invalid"); + + cy.get(".invalid-feedback").each(($element) => { + cy.wrap($element).should("contain.text", "is required"); + }); + }); + + it("requires the name to be longer than 3 characters", () => { + cy.get("#inputName").clear().type("UI"); + + cy.get("#submit-button").click(); + cy.get(".invalid-feedback").should("contain", "Minimum length is 3"); + }); + + it("requires a valid email address", () => { + cy.get("#inputEmail").clear().type("test"); + cy.get("#submit-button").click(); + cy.get("#inputEmail").then(($input) => { + expect($input[0].validationMessage).to.contain("Please include an"); + }); + + cy.get("#inputEmail").clear().type("test@"); + cy.get("#submit-button").click(); + cy.get("#inputEmail").then(($input) => { + expect($input[0].validationMessage).to.contain("Please enter a part"); + }); + + cy.get("#inputEmail").clear().type("test@test"); + cy.get("#submit-button").click(); + cy.get("#inputEmail").should("have.class", "is-invalid"); + }); + + it("Does not require an ORCID id", () => { + cy.get("#inputName").clear().type("Identified"); + cy.get("#inputSurname").clear().type("Test"); + cy.get("#inputEmail").clear().type("identified@example.com"); + cy.get("#inputOrcidId").clear(); + cy.get("#submit-button").click(); + + cy.url().should("match", /\/profile$/); + }); + + it("updates the personal details", () => { + cy.get("#inputName").clear().type("Identified"); + cy.get("#inputSurname").clear().type("Test"); + cy.get("#inputEmail").clear().type("identified@example.com"); + cy.get("#inputOrcidId").clear().type("0000-0002-0255-5101"); + + cy.get("#submit-button").click(); + + cy.url().should("match", /\/profile$/); + + cy.get("#name").should("contain", "Identified"); + cy.get("#surname").should("contain", "Test"); + cy.get("#email").should("contain", "identified@example.com"); + cy.get("#orcid").should("contain", "0000-0002-0255-5101"); + }); +}); diff --git a/cypress/fixtures/profile_empty.json b/cypress/fixtures/profile_empty.json new file mode 100644 index 00000000..ac0d2cb6 --- /dev/null +++ b/cypress/fixtures/profile_empty.json @@ -0,0 +1,7 @@ +{ + "id": "identified_ui_voperson_id", + "registered_on": "2024-01-03T15:39:23.000+01:00", + "user_type": "Identified", + "roles": ["identified"], + "banned": false +} diff --git a/cypress/fixtures/profile_updated.json b/cypress/fixtures/profile_updated.json new file mode 100644 index 00000000..863da31d --- /dev/null +++ b/cypress/fixtures/profile_updated.json @@ -0,0 +1,12 @@ +{ + "id": "identified_ui_voperson_id", + "registered_on": "2024-01-03T15:39:23.000+01:00", + "user_type": "Identified", + "name": "Identified", + "surname": "Test", + "email": "identified@example.com", + "orcid_id": "0000-0002-0255-5101", + "updated_on": "2024-01-03T17:07:50.478+01:00", + "roles": ["identified"], + "banned": false +} From 62758da4c2c42870097141648c79d8107500c320 Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Thu, 15 Feb 2024 14:41:00 +0100 Subject: [PATCH 02/51] Added ids to the profile page. --- src/pages/Profile.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 67306d4d..c2f89405 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -113,12 +113,14 @@ function Profile() {
View List @@ -130,20 +132,22 @@ function Profile() {
-
Assesments
-
+
Assessments
+
{(userProfile?.user_type === "Validated" || userProfile?.user_type === "Admin") && ( <>
View your current assessments or create a new one.
View List @@ -156,16 +160,21 @@ function Profile() {
Subjects
-
+
{(userProfile?.user_type === "Validated" || userProfile?.user_type === "Admin") && ( <>
View and manage your current Assessment Subjects
- + View List From 994371f59d4b29182b4b916a70ed97d07f527d0c Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Wed, 14 Feb 2024 15:21:32 +0100 Subject: [PATCH 03/51] Login spec to test keycloak login. --- cypress/e2e/login.cy.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 cypress/e2e/login.cy.ts diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts new file mode 100644 index 00000000..271ce146 --- /dev/null +++ b/cypress/e2e/login.cy.ts @@ -0,0 +1,30 @@ +describe("User Login Tests", () => { + beforeEach(() => { + cy.visit("/login"); + }); + it("greets with sign in", () => { + cy.origin("http://localhost:58080", () => { + cy.get("h1").should("contain", "Sign in to your account"); + }); + }); + it("requires username", () => { + cy.origin("http://localhost:58080", () => { + cy.get("input#kc-login").click(); + cy.get("#input-error").should("contain", "Invalid username or password."); + }); + }); + it("requires password", () => { + cy.origin("http://localhost:58080", () => { + cy.get("input#kc-login").click(); + cy.get("#input-error").should("contain", "Invalid username or password."); + }); + }); + it("logs in", () => { + cy.origin("http://localhost:58080", () => { + cy.get("input#username").type("identified_ui"); + cy.get("input#password").type("identified_ui"); + cy.get("input#kc-login").click(); + }); + cy.url().should("include", "login"); + }); +}); From 8612e9096c760dad89a43e36cdf9006f567cc133 Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Thu, 15 Feb 2024 16:01:36 +0100 Subject: [PATCH 04/51] Created specs and fixtures for validations. --- cypress/e2e/validation.cy.ts | 30 +++++++++++++++++++++++++ cypress/fixtures/validation_review.json | 16 +++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 cypress/e2e/validation.cy.ts create mode 100644 cypress/fixtures/validation_review.json diff --git a/cypress/e2e/validation.cy.ts b/cypress/e2e/validation.cy.ts new file mode 100644 index 00000000..8495f8f5 --- /dev/null +++ b/cypress/e2e/validation.cy.ts @@ -0,0 +1,30 @@ +/// + +describe("/validations/request", () => { + beforeEach(() => { + cy.kcLogout(); + cy.kcLogin("identified").as("identifiedTokens"); + cy.visit("/validations/request"); + }); + + it("creates a validation", () => { + cy.intercept("POST", "http://localhost:8080/v1/validations", { + fixture: "validation_review.json", + }).as("getValidations"); + cy.get(".select__input").click().type("Oxford"); + cy.contains(".select__option", "University of Oxford").click(); + cy.get("#actor_id").select(1); + cy.get("#organisation_role").type("Team Manager"); + cy.get("#actor_id") + .should("have.value", "1") + .contains("PID Standards Body"); + cy.get("#create_validation").click(); + cy.wait(["@getValidations"]); + cy.url().should("contain", "/validations"); + }); + + it("requires an organization role", () => { + cy.get("#create_validation").click(); + cy.get("#organisation_role").should("have.class", "is-invalid"); + }); +}); diff --git a/cypress/fixtures/validation_review.json b/cypress/fixtures/validation_review.json new file mode 100644 index 00000000..119ad948 --- /dev/null +++ b/cypress/fixtures/validation_review.json @@ -0,0 +1,16 @@ +{ + "id": 3, + "user_id": "identified_ui_voperson_id", + "user_name": "Identified", + "user_surname": "Test", + "user_email": "identified@example.com", + "organisation_role": "Team Manager", + "organisation_id": "03nm8n513", + "organisation_source": "ROR", + "organisation_name": "Oxford Photovoltaics (United Kingdom)", + "organisation_website": "http://www.oxfordpv.com/", + "actor_id": 5, + "actor_name": "PID Manager", + "status": "REVIEW", + "created_on": "2024-01-03T14:42:44.463+01:00" +} From 7a1970fb8428df046b21ef1a8dcf841dba8a3acd Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Thu, 15 Feb 2024 16:02:23 +0100 Subject: [PATCH 05/51] Added id for validation create button. --- src/pages/validations/RequestValidation.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/validations/RequestValidation.tsx b/src/pages/validations/RequestValidation.tsx index c9832e6b..14261682 100644 --- a/src/pages/validations/RequestValidation.tsx +++ b/src/pages/validations/RequestValidation.tsx @@ -430,7 +430,11 @@ function RequestValidation() {
- From 3772ec7bdc5d1246733709786d78d9c28f7ed727 Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Thu, 15 Feb 2024 16:03:09 +0100 Subject: [PATCH 06/51] Made profile name follow the name length requirement. --- cypress/fixtures/profile.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/fixtures/profile.json b/cypress/fixtures/profile.json index 6c3b79c4..d901941e 100644 --- a/cypress/fixtures/profile.json +++ b/cypress/fixtures/profile.json @@ -1,6 +1,6 @@ { "name": "Identified", - "surname": "UI", + "surname": "Test", "email": "identified@example.com", "orcid_id": "0000-0002-0255-5101" } From e1c334971caee5821fe2fff01719a84c94d02670 Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Thu, 15 Feb 2024 16:19:15 +0100 Subject: [PATCH 07/51] Added assertion to check that the validation was succesfully created. --- cypress/e2e/validation.cy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cypress/e2e/validation.cy.ts b/cypress/e2e/validation.cy.ts index 8495f8f5..12458a99 100644 --- a/cypress/e2e/validation.cy.ts +++ b/cypress/e2e/validation.cy.ts @@ -21,6 +21,9 @@ describe("/validations/request", () => { cy.get("#create_validation").click(); cy.wait(["@getValidations"]); cy.url().should("contain", "/validations"); + cy.contains("Validation request succesfully submitted.").should( + "be.visible", + ); }); it("requires an organization role", () => { From 2502210ae7308cc78ea8107ee24261dfe1334fe4 Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Thu, 15 Feb 2024 16:39:25 +0100 Subject: [PATCH 08/51] Created a separate directory for all validation fixtures. --- .../{ => validations}/validation.json | 0 .../validations/validation_approved.json | 18 +++++++++++++ .../{ => validations}/validation_review.json | 0 cypress/fixtures/validations/validations.json | 25 +++++++++++++++++ .../validations/validations_approved.json | 27 +++++++++++++++++++ .../validations/validations_rejected.json | 27 +++++++++++++++++++ 6 files changed, 97 insertions(+) rename cypress/fixtures/{ => validations}/validation.json (100%) create mode 100644 cypress/fixtures/validations/validation_approved.json rename cypress/fixtures/{ => validations}/validation_review.json (100%) create mode 100644 cypress/fixtures/validations/validations.json create mode 100644 cypress/fixtures/validations/validations_approved.json create mode 100644 cypress/fixtures/validations/validations_rejected.json diff --git a/cypress/fixtures/validation.json b/cypress/fixtures/validations/validation.json similarity index 100% rename from cypress/fixtures/validation.json rename to cypress/fixtures/validations/validation.json diff --git a/cypress/fixtures/validations/validation_approved.json b/cypress/fixtures/validations/validation_approved.json new file mode 100644 index 00000000..d4357a71 --- /dev/null +++ b/cypress/fixtures/validations/validation_approved.json @@ -0,0 +1,18 @@ +{ + "id": 1, + "user_id": "identified_ui_voperson_id", + "user_name": "Identified", + "user_surname": "Test", + "user_email": "identified@example.com", + "organisation_role": "Team Manager", + "organisation_id": "05n2x7017", + "organisation_source": "ROR", + "organisation_name": "Oxford Fertility", + "organisation_website": null, + "actor_id": 1, + "actor_name": "PID Standards Body", + "status": "APPROVED", + "created_on": "2023-12-20T15:30:40.782+01:00", + "validated_on": "2023-12-20T17:04:15.076+01:00", + "validated_by": "admin_ui_voperson_id" +} diff --git a/cypress/fixtures/validation_review.json b/cypress/fixtures/validations/validation_review.json similarity index 100% rename from cypress/fixtures/validation_review.json rename to cypress/fixtures/validations/validation_review.json diff --git a/cypress/fixtures/validations/validations.json b/cypress/fixtures/validations/validations.json new file mode 100644 index 00000000..53754e0b --- /dev/null +++ b/cypress/fixtures/validations/validations.json @@ -0,0 +1,25 @@ +{ + "size_of_page": 1, + "number_of_page": 1, + "total_elements": 1, + "total_pages": 1, + "content": [ + { + "id": 1, + "user_id": "identified_ui_voperson_id", + "user_name": "Identified", + "user_surname": "Test", + "user_email": "identified@example.com", + "organisation_role": "Team Manager", + "organisation_id": "05n2x7017", + "organisation_source": "ROR", + "organisation_name": "Oxford Fertility", + "organisation_website": null, + "actor_id": 1, + "actor_name": "PID Standards Body", + "status": "REVIEW", + "created_on": "2023-12-20T15:30:40.782+01:00" + } + ], + "links": [] +} diff --git a/cypress/fixtures/validations/validations_approved.json b/cypress/fixtures/validations/validations_approved.json new file mode 100644 index 00000000..5ec68f04 --- /dev/null +++ b/cypress/fixtures/validations/validations_approved.json @@ -0,0 +1,27 @@ +{ + "size_of_page": 1, + "number_of_page": 1, + "total_elements": 1, + "total_pages": 1, + "content": [ + { + "id": 1, + "user_id": "identified_ui_voperson_id", + "user_name": "Identified", + "user_surname": "Test", + "user_email": "identified@example.com", + "organisation_role": "Team Manager", + "organisation_id": "05n2x7017", + "organisation_source": "ROR", + "organisation_name": "Oxford Fertility", + "organisation_website": null, + "actor_id": 1, + "actor_name": "PID Standards Body", + "status": "APPROVED", + "created_on": "2023-12-20T15:30:40.782+01:00", + "validated_on": "2023-12-20T17:04:15.076+01:00", + "validated_by": "admin_ui_voperson_id" + } + ], + "links": [] +} diff --git a/cypress/fixtures/validations/validations_rejected.json b/cypress/fixtures/validations/validations_rejected.json new file mode 100644 index 00000000..627c7658 --- /dev/null +++ b/cypress/fixtures/validations/validations_rejected.json @@ -0,0 +1,27 @@ +{ + "size_of_page": 1, + "number_of_page": 1, + "total_elements": 1, + "total_pages": 1, + "content": [ + { + "id": 1, + "user_id": "identified_ui_voperson_id", + "user_name": "Identified", + "user_surname": "Test", + "user_email": "identified@example.com", + "organisation_role": "Team Manager", + "organisation_id": "05n2x7017", + "organisation_source": "ROR", + "organisation_name": "Oxford Fertility", + "organisation_website": null, + "actor_id": 1, + "actor_name": "PID Standards Body", + "status": "REJECTED", + "created_on": "2023-12-20T15:30:40.782+01:00", + "validated_on": "2023-12-20T17:04:15.076+01:00", + "validated_by": "admin_ui_voperson_id" + } + ], + "links": [] +} From dc0f2108751471e00b5360f6c7c019868d02bed2 Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Thu, 15 Feb 2024 16:40:48 +0100 Subject: [PATCH 09/51] Created spec for admin validation creation. Updated validation spec and commands to use correct fixture location. --- cypress/e2e/admin.cy.ts | 83 ++++++++++++++++++++++++++++++++++++ cypress/e2e/validation.cy.ts | 22 +++++++++- cypress/support/commands.ts | 2 +- 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 cypress/e2e/admin.cy.ts diff --git a/cypress/e2e/admin.cy.ts b/cypress/e2e/admin.cy.ts new file mode 100644 index 00000000..0ac6a072 --- /dev/null +++ b/cypress/e2e/admin.cy.ts @@ -0,0 +1,83 @@ +/// +describe("admin", () => { + beforeEach(() => { + cy.kcLogout(); + cy.kcLogin("admin").as("admin"); + cy.visit("/profile"); + }); + + it("accept a validation request", () => { + cy.intercept( + "GET", + "http://localhost:8080/v1/admin/validations?size=10&page=1&sortby=asc", + { + fixture: "validations/validations.json", + }, + ).as("getValidations"); + cy.intercept("GET", "http://localhost:8080/v1/admin/validations/1", { + fixture: "validations/validation.json", + }); + // Navigate to validations list for admins + cy.get("#admin_nav").click(); + cy.get("#admin_validation").contains("Validations").click(); + cy.wait(["@getValidations"]); + cy.url().should("include", "/validations"); + + // Click the approve icon for the validation. + cy.get("table").contains("Oxford Fertility"); + cy.get(".cat-action-approve-link").click(); + // Click the approve button on the validation's page. + cy.get(".btn-success").click(); + cy.intercept( + "PUT", + "http://localhost:8080/v1/admin/validations/1/update-status", + { + fixture: "validations/validation_approved.json", + }, + ).as("updateStatus"); + cy.intercept( + "http://localhost:8080/v1/admin/validations?size=10&page=1&sortby=asc", + { + fixture: "validations/validations_approved.json", + }, + ).as("validationsApproved"); + cy.contains("Validation successfully approved."); + cy.wait(["@validationsApproved"]); + cy.contains("APPROVED"); + }); + + it("declines a validation request", () => { + cy.intercept( + { + method: "GET", + url: "http://localhost:8080/v1/admin/validations?size=10&page=1&sortby=asc", + }, + (req) => { + if (req.alias === "rejectedValidations") { + req.reply({ fixture: "validations/validations_rejected.json" }); + } else { + req.reply({ fixture: "validations/validations.json" }); + } + }, + ).as("getValidations"); + // Navigate to validations list for admins + cy.get("#admin_nav").click(); + cy.get("#admin_validation").contains("Validations").click(); + cy.wait(["@getValidations"]); + cy.url().should("include", "/validations"); + + // Click the approve icon for the validation. + cy.get("table").contains("Oxford Fertility"); + cy.get(".cat-action-reject-link").click(); + // Click the approve button on the validation's page. + cy.get(".btn-danger").click(); + cy.intercept( + "PUT", + "http://localhost:8080/v1/admin/validations/1/update-status", + { fixture: "validations/validations_rejected.json" }, + ); + + cy.contains("Validation successfully rejected."); + cy.url().should("include", "/validations"); + }); +}); diff --git a/cypress/e2e/validation.cy.ts b/cypress/e2e/validation.cy.ts index 12458a99..1d370983 100644 --- a/cypress/e2e/validation.cy.ts +++ b/cypress/e2e/validation.cy.ts @@ -9,7 +9,7 @@ describe("/validations/request", () => { it("creates a validation", () => { cy.intercept("POST", "http://localhost:8080/v1/validations", { - fixture: "validation_review.json", + fixture: "validations/validation_review.json", }).as("getValidations"); cy.get(".select__input").click().type("Oxford"); cy.contains(".select__option", "University of Oxford").click(); @@ -26,6 +26,26 @@ describe("/validations/request", () => { ); }); + it("creates a validation with a custom organisation", () => { + cy.intercept("POST", "http://localhost:8080/v1/validations", { + fixture: "validations/validation_review.json", + }).as("getValidations"); + cy.get("#organisation_source").select("Custom"); + cy.get("#organisation_name").type("Test Organisation"); + cy.get("#organisation_website").type("www.example.com"); + cy.get("#actor_id").select(1); + cy.get("#organisation_role").type("Team Manager"); + cy.get("#actor_id") + .should("have.value", "1") + .contains("PID Standards Body"); + cy.get("#create_validation").click(); + cy.wait(["@getValidations"]); + cy.url().should("contain", "/validations"); + cy.contains("Validation request succesfully submitted.").should( + "be.visible", + ); + }); + it("requires an organization role", () => { cy.get("#create_validation").click(); cy.get("#organisation_role").should("have.class", "is-invalid"); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index a1c18627..82bc11cd 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -20,7 +20,7 @@ Cypress.Commands.add("updateProfile", (authToken) => { }); Cypress.Commands.add("createValidation", (authToken) => { - cy.fixture("validation").then((orgData) => { + cy.fixture("validations/validation").then((orgData) => { cy.request({ method: "POST", url: "http://localhost:8080/v1/validations", From d9ae682856301fc6d99c7a0c20629b8e67717c53 Mon Sep 17 00:00:00 2001 From: Fjodor van Rijsselberg Date: Thu, 15 Feb 2024 16:49:51 +0100 Subject: [PATCH 10/51] Added ids to select Admin dropdown in header. --- src/components/Header.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 7f9225fa..d5677707 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -125,6 +125,7 @@ function Header() { {authenticated && userProfile?.user_type === "Admin" && (
-
-
+ + +
+ + Validation Request Rejection +
+
+ + Are you sure you want to reject validation with ID:{" "} + {params.id} ? + + + + + +
); } if (props.toApprove) { approveCard = ( -
-
-
-
- - Validation Request Approval -
-
-
- Are you sure you want to approve validation with ID:{" "} - {params.id} ? -
-
- - -
-
-
+ + +
+ + Validation Request Approval +
+
+ + Are you sure you want to approve validation with ID:{" "} + {params.id} ? + + + + + +
); } if (keycloak?.token) { return (
+ {rejectCard} {approveCard}
Approve Reject @@ -321,8 +319,6 @@ function ValidationDetails(props: ValidationProps) { > Back - -
); } else { From 2029cbbdf46a3fcc68c48f1473828b0d9c8efaae Mon Sep 17 00:00:00 2001 From: Konstantinos Kagkelidis Date: Thu, 29 Feb 2024 02:02:01 +0200 Subject: [PATCH 16/51] CAT-328 Assessment creation: fix issue with valid actor duplicates --- src/pages/assessments/AssessmentEdit.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/assessments/AssessmentEdit.tsx b/src/pages/assessments/AssessmentEdit.tsx index 14b03dfe..7b3b39bc 100644 --- a/src/pages/assessments/AssessmentEdit.tsx +++ b/src/pages/assessments/AssessmentEdit.tsx @@ -120,13 +120,19 @@ const AssessmentEdit = ({ createMode = true }: AssessmentEditProps) => { // TODO: Get all available pages in an infinite scroll not all sequentially. useEffect(() => { if (data?.content) { - setValidations((validations) => [...validations, ...data.content]); + // if we are on the first page replace previous content + if (page === 1) { + setValidations(data.content); + } else { + setValidations((validations) => [...validations, ...data.content]); + } + if (data?.number_of_page < data?.total_pages) { setPage((page) => page + 1); refetchGetValidationList(); } } - }, [data, refetchGetValidationList]); + }, [data, refetchGetValidationList, page, validations]); // After retrieving user's valitions we create a struct for the option boxes creation const [actorsOrgsMap, setActorsOrgsMap] = useState< From f60f3cdae92c32d953efbc6a68d0f77808abaffb Mon Sep 17 00:00:00 2001 From: Konstantinos Kagkelidis Date: Thu, 29 Feb 2024 02:21:02 +0200 Subject: [PATCH 17/51] CAT-329 Simplify navigation menu --- src/components/Header.tsx | 38 ++++++++------------------- src/pages/assessments/Assessments.tsx | 31 ++++++++++++++-------- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index d5677707..69b2cff7 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -12,7 +12,7 @@ import logo from "@/logo.svg"; import { useGetProfile } from "@/api"; import { trimProfileID } from "@/utils"; import { AuthContext } from "@/auth"; -import { FaUser, FaCog, FaShieldAlt } from "react-icons/fa"; +import { FaUser, FaShieldAlt } from "react-icons/fa"; import { UserProfile } from "@/types"; function Header() { @@ -84,42 +84,26 @@ function Header() { {/* Collapsible part that holds navigation links */} {authenticated && userProfile?.user_type === "Admin" && ( @@ -128,7 +112,7 @@ function Header() { id="admin_nav" title={ - ADMIN + ADMIN MODE } className="cat-nav-item" diff --git a/src/pages/assessments/Assessments.tsx b/src/pages/assessments/Assessments.tsx index d65d943d..5b1a567f 100644 --- a/src/pages/assessments/Assessments.tsx +++ b/src/pages/assessments/Assessments.tsx @@ -8,6 +8,8 @@ import ownersImg from "@/assets/thumb_user.png"; import { useGetActors } from "@/api"; import { Col, Row } from "react-bootstrap"; import { ActorCard } from "./components/ActorCard"; +import { AuthContext } from "@/auth"; +import { useContext } from "react"; interface CardProps { id: number; @@ -26,6 +28,8 @@ function Assessments() { sortBy: "asc", }); + const { authenticated } = useContext(AuthContext)!; + const cardProps: CardProps[] = []; const cardImgs: Record = { @@ -65,17 +69,22 @@ function Assessments() { assessments
-
- - Create New - - - View Your Assessments - -
+ {authenticated && ( +
+ + Create New + + + View Your Assessments + +
+ )}
<> From 3fb3812de879a8c5243de84ac1c495fd39bb25cf Mon Sep 17 00:00:00 2001 From: Konstantinos Kagkelidis Date: Thu, 7 Mar 2024 10:23:28 +0200 Subject: [PATCH 18/51] CAT-335 Update styling in create assessment wizard --- src/App.css | 32 +++++++++++---- .../assessments/components/CriteriaTabs.tsx | 40 +++++++++---------- .../components/tests/TestBinaryForm.tsx | 14 ++----- .../components/tests/TestValueForm.tsx | 11 ++--- 4 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/App.css b/src/App.css index f52f9506..663e8223 100644 --- a/src/App.css +++ b/src/App.css @@ -12,6 +12,30 @@ border-bottom: solid 2px #919191; } +.cat-crit-sidebar { + background-color: rgb(226 227 229); +} + +.cat-crit-tab .nav-link { + color: black; +} + +.assessment-card .nav-link { + color: #3b3b3b !important; +} + +.cat-crit-tab .nav-link:hover { + background: white; +} + +.active { + color: red; +} + +.cat-crit-tab .active { + background: white; +} + .cat-view-heading { color: #00bcd4; } @@ -83,10 +107,6 @@ footer a { padding: 15px; } -.active { - color: red; -} - .custom-pagination input { display: inline-block; float: left; @@ -116,10 +136,6 @@ footer a { opacity: 0.5; /* Reduce opacity to make it appear disabled */ } -.assessment-card .nav-link { - color: #3b3b3b !important; -} - .assessment-card .nav-link.active { color: #0b5ed7 !important; } diff --git a/src/pages/assessments/components/CriteriaTabs.tsx b/src/pages/assessments/components/CriteriaTabs.tsx index 2eaba61b..799f94a6 100644 --- a/src/pages/assessments/components/CriteriaTabs.tsx +++ b/src/pages/assessments/components/CriteriaTabs.tsx @@ -54,16 +54,16 @@ export function CriteriaTabs(props: CriteriaTabsProps) { }, [props]); props.principles.forEach((principle) => { - // push principle lable to navigation list - navs.push( - - {principle.id} - {principle.name}: - , - ); + // comment to not push principle lable to navigation list + // navs.push( + // + // {principle.id} - {principle.name}: + // , + // ); principle.criteria.forEach((criterion) => { navs.push( - + {criterion.id} - {criterion.name} @@ -108,14 +108,16 @@ export function CriteriaTabs(props: CriteriaTabsProps) { eventKey={criterion.id} > {/* add a principle info box before criterion content */} - -
- Principle {principle.id}: {principle.name} -
- {principle.description} -
+
- + + +
+ Principle {principle.id}: {principle.name} +
+ {principle.description} +
+
@@ -224,7 +226,7 @@ export function CriteriaTabs(props: CriteriaTabsProps) { - {testList} + {testList}
, ); @@ -237,11 +239,9 @@ export function CriteriaTabs(props: CriteriaTabsProps) { activeKey={activeKey} onSelect={(key) => setActiveKey(key || "")} > - - - + + + {tabs} diff --git a/src/pages/assessments/components/tests/TestBinaryForm.tsx b/src/pages/assessments/components/tests/TestBinaryForm.tsx index ab344b9d..2194e29c 100644 --- a/src/pages/assessments/components/tests/TestBinaryForm.tsx +++ b/src/pages/assessments/components/tests/TestBinaryForm.tsx @@ -44,9 +44,9 @@ export const TestBinaryForm = (props: AssessmentTestProps) => {
-
+
Test {props.test.id}: {props.test.name}{" "} -
+ + {props.mode == UserModalMode.Ban && ( + + )} + {props.mode == UserModalMode.Unban && ( + + )} + + + ); +} function Users() { + // toast alert reference used in notification messaging + const toastAlert = useRef({ + message: "", + }); + + const [userModalConfig, setUserModalConfig] = useState({ + mode: UserModalMode.Ban, + show: false, + id: "", + }); + // mutation hook for creating a new subject + const { keycloak } = useContext(AuthContext)!; + + // hooks for handling subjects in through the backend + const mutationBanUser = useBanUser(keycloak?.token || ""); + const mutationUnbanUser = useUnbanUser(keycloak?.token || ""); + + const handleUnban = (id: string) => { + const promise = mutationUnbanUser + .mutateAsync({ user_id: id }) + .catch((err) => { + toastAlert.current = { + message: "Error during user unban.", + }; + throw err; + }) + .then(() => { + toastAlert.current = { + message: "User succesfully unbanned", + }; + // close the modal + setUserModalConfig((prevConfig) => ({ ...prevConfig, show: false })); + }); + toast.promise(promise, { + loading: "Unbanning...", + success: () => `${toastAlert.current.message}`, + error: () => `${toastAlert.current.message}`, + }); + }; + + const handleBan = (id: string, reason: string) => { + const promise = mutationBanUser + .mutateAsync({ user_id: id, reason: reason }) + .catch((err) => { + toastAlert.current = { + message: "Error during user ban.", + }; + throw err; + }) + .then(() => { + toastAlert.current = { + message: "Subject succesfully banned.", + }; + // close the modal + setUserModalConfig((prevConfig) => ({ ...prevConfig, show: false })); + }); + toast.promise(promise, { + loading: "Banning...", + success: () => `${toastAlert.current.message}`, + error: () => `${toastAlert.current.message}`, + }); + }; + const cols = useMemo[]>( () => [ { @@ -28,12 +191,59 @@ function Users() { header: () => Registered On, // footer: props => props.column.id, }, + { + accessorFn: (row) => row, + enableColumnFilter: false, + id: "Account Status", + cell: (info) => { + const item: UserProfile = info.getValue() as UserProfile; + return item.banned ? ( +
+ banned + { + setUserModalConfig({ + id: item.id, + mode: UserModalMode.Unban, + show: true, + }); + }} + > + + +
+ ) : ( +
+ active + { + setUserModalConfig({ + id: item.id, + mode: UserModalMode.Ban, + show: true, + }); + }} + > + + +
+ ); + }, + }, ], [], ); return (
diff --git a/src/types/assessment.ts b/src/types/assessment.ts index e4e81700..ba017446 100644 --- a/src/types/assessment.ts +++ b/src/types/assessment.ts @@ -210,3 +210,9 @@ export interface AssessmentFiltersType { subject_name: string; subject_type: string; } + +export enum AssessmentEditMode { + Create = "create", + Edit = "edit", + Import = "import", +} From e656701204f58efaf6b6e6214336fcdbcb0335aa Mon Sep 17 00:00:00 2001 From: Konstantinos Kagkelidis Date: Wed, 10 Apr 2024 16:05:33 +0300 Subject: [PATCH 43/51] CAT-352 fix export and import issues --- src/pages/assessments/AssessmentEdit.tsx | 155 ++++++++++++++++++---- src/pages/assessments/Assessments.tsx | 23 ++-- src/pages/assessments/AssessmentsList.tsx | 13 +- 3 files changed, 148 insertions(+), 43 deletions(-) diff --git a/src/pages/assessments/AssessmentEdit.tsx b/src/pages/assessments/AssessmentEdit.tsx index bda7edc3..8731cfcb 100644 --- a/src/pages/assessments/AssessmentEdit.tsx +++ b/src/pages/assessments/AssessmentEdit.tsx @@ -19,12 +19,15 @@ import { AssessmentEditMode, } from "@/types"; import { useParams } from "react-router"; -import { Card, Nav, Tab, Button, Form } from "react-bootstrap"; +import { Card, Nav, Tab, Button, Form, Alert } from "react-bootstrap"; import { AssessmentInfo, CriteriaTabs } from "@/pages/assessments/components"; import { FaCheckCircle, + FaDownload, FaExclamationCircle, FaFileImport, + FaHandPointRight, + FaTimesCircle, } from "react-icons/fa"; import { evalAssessment, evalMetric } from "@/utils"; @@ -85,6 +88,7 @@ const AssessmentEdit = ({ const validationID = valID !== undefined ? valID : ""; const [vldid, setVldid] = useState(); + const [importInfo, setImportInfo] = useState(); const qValidation = useGetValidationDetails({ validation_id: vldid!, token: keycloak?.token || "", @@ -147,17 +151,13 @@ const AssessmentEdit = ({ ActorOrganisationMapping[] >([]); useEffect(() => { - console.log( - "$$$", - assessment?.actor.id, - mode === AssessmentEditMode.Import && Boolean(assessment?.actor.id), - ); const filt = validations // We only allow assessment creation for APPROVED validations and // specific actors .filter((v: ValidationResponse) => { - return mode === AssessmentEditMode.Import && assessment?.actor.id - ? v["status"] === "APPROVED" && v["actor_id"] === assessment?.actor.id + return mode === AssessmentEditMode.Import && importInfo?.actor?.id + ? v["status"] === "APPROVED" && + v["actor_id"] === importInfo?.actor?.id : v["status"] === "APPROVED" && Object.values(allowedActors).includes(v["actor_id"]); }) @@ -172,7 +172,7 @@ const AssessmentEdit = ({ }) .sort((a, b) => (a.actor_id > b.actor_id ? 1 : -1)); setActorsOrgsMap(filt); - }, [validations, mode, assessment]); + }, [validations, mode, assessment, importInfo]); const mutationCreateAssessment = useCreateAssessment(keycloak?.token || ""); @@ -202,7 +202,6 @@ const AssessmentEdit = ({ } function handleCreateAssessment() { - console.log("template", templateId, "val", vldid); if (templateId && vldid && assessment && checkRequiredFields(assessment)) { const promise = mutationCreateAssessment .mutateAsync({ @@ -242,7 +241,10 @@ const AssessmentEdit = ({ } function handleNextTab() { - if (activeTab < 3) { + if ( + (mode === AssessmentEditMode.Import && activeTab < 4) || + activeTab < 3 + ) { handleChangeTab(activeTab + 1); } } @@ -411,10 +413,8 @@ const AssessmentEdit = ({ reader.onload = () => { try { const contents = JSON.parse(reader.result as string); - contents.assessment_doc.id = undefined; - contents.assessment_doc.timestamp = ""; - setAssessment(contents.assessment_doc); - console.log("Assessment set"); + setImportInfo(contents); + setAssessment({ ...contents, id: undefined, timestamp: "" }); } catch (error) { console.error("Error parsing JSON file:", error); } @@ -502,9 +502,16 @@ const AssessmentEdit = ({ const extraTab = mode === AssessmentEditMode.Import ? 1 : 0; let importDone = true; if (mode === AssessmentEditMode.Import) { - importDone = Boolean(assessment?.actor?.id); + importDone = Boolean(importInfo?.actor?.id); } + const nextEnabled = + mode === AssessmentEditMode.Import + ? importDone + ? activeTab === 1 || (wizardTabActive && activeTab <= 3) + : false + : wizardTabActive && activeTab < 3; + return ( <>

@@ -590,10 +597,40 @@ const AssessmentEdit = ({ {mode === AssessmentEditMode.Import && (
-
- Select File for - Import -
+
+ CAT Toolkit instances give the ability to{" "} + export and{" "} + import Assessments as{" "} + *.json format + files that adhere to a specific schema. +
+ +
+ + + You can export an existing + assessment by going to{" "} + your assement list and + clicking the {" "} + button in the actions column + + +
+ +
+ You can import an External Assessment that exists as a + json file in your filesystem and use it as a basis to + create a new one. +
+ +
+ {" "} + {" "} + + Select External Assessment file (*.json) for importing: + {" "} +
+ + {importDone && importInfo !== undefined && ( + <> + +
+ + + Valid assessment imported + +
+
+ {importInfo.timestamp && ( +
+ + timestamp:{" "} + {importInfo.timestamp} + +
+ )} + {importInfo.id && ( +
+ + id: {importInfo.id} + +
+ )} + {importInfo.name && ( +
+ + name: {importInfo.name} + +
+ )} + {importInfo.actor && ( +
+ + actor: {importInfo.actor.name}{" "} + - [id: {importInfo.actor.id}] + +
+ )} + {importInfo.organisation && ( +
+ + organisation:{" "} + {importInfo?.organisation?.name} - [id:{" "} + {importInfo?.organisation?.id}] + +
+ )} +
+
+ Please proceed to the next step to select Actor +
+ + )} + {!importDone && importInfo !== undefined && ( + <> + +
+ + + + Invalid Assessment! + + - Please try to import a different file... + +
+
+ + )}
)} @@ -631,8 +738,8 @@ const AssessmentEdit = ({ templateData?.actor || { id: 0, name: "" } } type={assessment?.assessment_type?.name || ""} - org={assessment?.organisation.name || ""} - orgId={assessment?.organisation.id || ""} + org={assessment?.organisation?.name || ""} + orgId={assessment?.organisation?.id || ""} subject={ assessment?.subject || templateData?.subject || { id: "", name: "", type: "" } @@ -680,9 +787,9 @@ const AssessmentEdit = ({

+ setUserModalConfig({ ...userModalConfig, show: false })} + onBan={handleBan} + onUnban={handleUnban} + />

users diff --git a/src/types/common.ts b/src/types/common.ts index 2795e415..3c28b7f2 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -99,3 +99,8 @@ export type Subject = { name: string; type: string; }; + +export type UserAccess = { + user_id: string; + reason?: string; +}; diff --git a/src/types/profile.ts b/src/types/profile.ts index ac7fb26d..120b3c25 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -19,6 +19,7 @@ export interface UserProfile { surname: string; email: string; updated_on: string; + banned: boolean; } export type UserResponse = UserProfile; From 7d70f1d819ecae6f465a9aa6e8ecc0769ba92740 Mon Sep 17 00:00:00 2001 From: Konstantinos Kagkelidis Date: Thu, 21 Mar 2024 13:13:35 +0200 Subject: [PATCH 39/51] CAT-333 Fix ban/unban methods. Add email info on user table. Fix notification position --- src/App.tsx | 2 +- src/pages/Users.tsx | 126 ++++++++++++++++++++++++++++---------------- 2 files changed, 82 insertions(+), 46 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 85163fab..c5ee50ac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,7 +40,7 @@ function App() { // Define default options className: " ", duration: 2000, - position: "top-right", + position: "top-center", style: { background: "#363636", color: "#fff", diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx index d9b05719..413a9e05 100644 --- a/src/pages/Users.tsx +++ b/src/pages/Users.tsx @@ -1,4 +1,4 @@ -import { useContext, useMemo, useRef, useState } from "react"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { ColumnDef } from "@tanstack/react-table"; import { useGetAdminUsers, useUnbanUser, useBanUser } from "@/api"; import { CustomTable } from "@/components"; @@ -24,13 +24,21 @@ type UserModalBasicConfig = { // Props for subject modal type UserModalProps = UserModalBasicConfig & { onHide: () => void; - onUnban: (id: string) => void; - onBan: (id: string, reason: string) => void; + handleUnban: (id: string, reason: string) => void; + handleBan: (id: string, reason: string) => void; }; // Creates a modal with a small form to create/edit a subject export function UserModal(props: UserModalProps) { const [reason, setReason] = useState(""); + const [error, setError] = useState(false); + + useEffect(() => { + if (props.show) { + setReason(""); + setError(false); + } + }, [props.show]); return ( @@ -51,13 +59,11 @@ export function UserModal(props: UserModalProps) { {props.mode == UserModalMode.Ban && ( Ban user - id: {props.id} )} {props.mode == UserModalMode.Unban && ( Unban user - id: {props.id} )} @@ -69,12 +75,17 @@ export function UserModal(props: UserModalProps) { {props.id} ?

- Reason + + Reason (*) + setReason(e.target.value)} /> + {error &&

You must specify a reason

}
@@ -85,7 +96,13 @@ export function UserModal(props: UserModalProps) { {props.mode == UserModalMode.Ban && ( @@ -94,7 +111,13 @@ export function UserModal(props: UserModalProps) {
{!listPublic && ( - - Create New - + <> + + Create New + + + Import + + )}

, diff --git a/src/pages/assessments/components/tests/TestBinaryForm.tsx b/src/pages/assessments/components/tests/TestBinaryForm.tsx index d0047e3e..f4daaac8 100644 --- a/src/pages/assessments/components/tests/TestBinaryForm.tsx +++ b/src/pages/assessments/components/tests/TestBinaryForm.tsx @@ -6,13 +6,13 @@ import { Button, Col, Form, Row } from "react-bootstrap"; import { EvidenceURLS } from "./EvidenceURLS"; import { AssessmentTest, TestBinary } from "@/types"; -import { useState } from "react"; -import { FaCaretLeft, FaCaretRight, FaRegQuestionCircle } from "react-icons/fa"; +import { FaRegQuestionCircle } from "react-icons/fa"; interface AssessmentTestProps { test: TestBinary; principleId: string; criterionId: string; + handleGuide(id: string, title: string, text: string): void; onTestChange( principleId: string, criterionId: string, @@ -21,8 +21,6 @@ interface AssessmentTestProps { } export const TestBinaryForm = (props: AssessmentTestProps) => { - const [showHelp, setShowHelp] = useState(false); - const handleValueChange = (event: React.ChangeEvent) => { let result: 0 | 1 = 0; let value: boolean = false; @@ -49,30 +47,26 @@ export const TestBinaryForm = (props: AssessmentTestProps) => { {props.test.id} {props.test.name} - - - - + + +
@@ -110,16 +104,6 @@ export const TestBinaryForm = (props: AssessmentTestProps) => { )} - - {/* Show guidance */} - {showHelp && props.test.guidance && ( - -

Guidance {props.test.guidance.id}

-

- {props.test.guidance.description} -

- - )}
); diff --git a/src/pages/assessments/components/tests/TestValueForm.tsx b/src/pages/assessments/components/tests/TestValueForm.tsx index 57e1f4f3..41bba99c 100644 --- a/src/pages/assessments/components/tests/TestValueForm.tsx +++ b/src/pages/assessments/components/tests/TestValueForm.tsx @@ -6,13 +6,13 @@ import { Button, Col, Form, InputGroup, Row } from "react-bootstrap"; import { EvidenceURLS } from "./EvidenceURLS"; import { AssessmentTest, TestValue } from "@/types"; -import { useState } from "react"; -import { FaCaretLeft, FaCaretRight, FaRegQuestionCircle } from "react-icons/fa"; +import { FaRegQuestionCircle } from "react-icons/fa"; interface AssessmentTestProps { test: TestValue; principleId: string; criterionId: string; + handleGuide(id: string, title: string, text: string): void; onTestChange( principleId: string, criterionId: string, @@ -26,8 +26,6 @@ enum TestValueEventType { } export const TestValueForm = (props: AssessmentTestProps) => { - const [showHelp, setShowHelp] = useState(false); - const handleValueChange = ( eventType: TestValueEventType, event: React.ChangeEvent, @@ -78,30 +76,26 @@ export const TestValueForm = (props: AssessmentTestProps) => { {props.test.id} {props.test.name} - - - - + + +
@@ -170,16 +164,6 @@ export const TestValueForm = (props: AssessmentTestProps) => { */} - - {/* Show guidance */} - {showHelp && props.test.guidance && ( - -

Guidance {props.test.guidance.id}

-

- {props.test.guidance.description} -

- - )}
); From 71e04b85b453fbc35a5be78ec095a55c1f616239 Mon Sep 17 00:00:00 2001 From: Konstantinos Kagkelidis Date: Mon, 15 Apr 2024 20:28:38 +0300 Subject: [PATCH 45/51] CAT-352 Minor fix in import assessment help text --- src/pages/assessments/AssessmentEdit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/assessments/AssessmentEdit.tsx b/src/pages/assessments/AssessmentEdit.tsx index 8731cfcb..33c4f182 100644 --- a/src/pages/assessments/AssessmentEdit.tsx +++ b/src/pages/assessments/AssessmentEdit.tsx @@ -598,7 +598,7 @@ const AssessmentEdit = ({
- CAT Toolkit instances give the ability to{" "} + CAT Toolkit gives the ability to{" "} export and{" "} import Assessments as{" "} *.json format From 7d793bf19ab49cfec6eb5e6494b73d2660c5615d Mon Sep 17 00:00:00 2001 From: Konstantinos Kagkelidis Date: Mon, 22 Apr 2024 05:00:26 +0300 Subject: [PATCH 46/51] CAT-343 redesign admin users view --- src/App.css | 4 + src/App.tsx | 5 +- src/api/services/users.ts | 36 +- src/pages/Users.tsx | 6 +- src/pages/admin/AdminUsers.tsx | 604 +++++++++++++++++++++++++++++++++ src/types/common.ts | 7 + 6 files changed, 653 insertions(+), 9 deletions(-) create mode 100644 src/pages/admin/AdminUsers.tsx diff --git a/src/App.css b/src/App.css index 6f25e4e5..7ed38e25 100644 --- a/src/App.css +++ b/src/App.css @@ -12,6 +12,10 @@ border-bottom: solid 2px #919191; } +.cat-cursor-pointer { + cursor: pointer; +} + .cat-list-bullet-image { list-style: url("/fclogo.png"); } diff --git a/src/App.tsx b/src/App.tsx index 3dd6c6a3..f551323d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,7 @@ import { Profile, RequestValidation, ValidationList, - Users, + // Users, ProfileUpdate, ValidationDetails, } from "@/pages"; @@ -25,6 +25,7 @@ import { Toaster } from "react-hot-toast"; import Subjects from "./pages/Subjects"; import About from "./pages/About"; import { AssessmentEditMode } from "./types"; +import AdminUsers from "./pages/admin/AdminUsers"; const queryClient = new QueryClient(); @@ -128,7 +129,7 @@ function App() { } /> }> - } /> + } /> { const response = await APIClient(token).get( - `/admin/users?size=${size}&page=${page}&sortby=${sortBy}`, + `/admin/users?size=${size}&page=${page}&sort=${sortBy}`, ); return response.data; }, @@ -41,6 +41,34 @@ export const useGetAdminUsers = ({ enabled: !!token && isRegistered, }); +export const useAdminGetUsers = ({ + size, + page, + sortBy, + sortOrder, + token, + isRegistered, + search, + type, + status, +}: ApiUsers) => + useQuery({ + queryKey: ["users"], + queryFn: async () => { + let url = `/admin/users?size=${size}&page=${page}&sort=${sortBy}&order=${sortOrder}`; + search ? (url = `${url}&search=${search}`) : null; + type ? (url = `${url}&type=${type}`) : null; + status ? (url = `${url}&status=${status}`) : null; + + const response = await APIClient(token).get(url); + return response.data; + }, + onError: (error: AxiosError) => { + return handleBackendError(error); + }, + enabled: !!token && isRegistered, + }); + export const useUserRegister = () => useMutation( async (token: string) => { @@ -67,7 +95,7 @@ export const useUserRegister = () => }, ); -export function useBanUser(token: string) { +export function useDeleteUser(token: string) { const queryClient = useQueryClient(); return useMutation( async (data: UserAccess) => { @@ -89,7 +117,7 @@ export function useBanUser(token: string) { ); } -export function useUnbanUser(token: string) { +export function useRestoreUser(token: string) { const queryClient = useQueryClient(); return useMutation( async (data: UserAccess) => { diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx index 413a9e05..1a17aeb4 100644 --- a/src/pages/Users.tsx +++ b/src/pages/Users.tsx @@ -1,6 +1,6 @@ import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { ColumnDef } from "@tanstack/react-table"; -import { useGetAdminUsers, useUnbanUser, useBanUser } from "@/api"; +import { useGetAdminUsers, useRestoreUser, useDeleteUser } from "@/api"; import { CustomTable } from "@/components"; import { FaLock, FaLockOpen, FaUnlock, FaUsers } from "react-icons/fa"; import { AlertInfo, UserProfile } from "@/types"; @@ -143,8 +143,8 @@ function Users() { const { keycloak } = useContext(AuthContext)!; // hooks for handling subjects in through the backend - const mutationBanUser = useBanUser(keycloak?.token || ""); - const mutationUnbanUser = useUnbanUser(keycloak?.token || ""); + const mutationBanUser = useDeleteUser(keycloak?.token || ""); + const mutationUnbanUser = useRestoreUser(keycloak?.token || ""); const handleUnban = (id: string, reason: string) => { const promise = mutationUnbanUser diff --git a/src/pages/admin/AdminUsers.tsx b/src/pages/admin/AdminUsers.tsx new file mode 100644 index 00000000..55e2a3f5 --- /dev/null +++ b/src/pages/admin/AdminUsers.tsx @@ -0,0 +1,604 @@ +import { useAdminGetUsers, useDeleteUser, useRestoreUser } from "@/api"; +import { AuthContext } from "@/auth"; +import { AlertInfo, UserProfile } from "@/types"; +import { useContext, useEffect, useRef, useState } from "react"; +import { + Alert, + Button, + Col, + Form, + Modal, + OverlayTrigger, + Row, + Table, + Tooltip, +} from "react-bootstrap"; +import { + FaArrowDown, + FaArrowLeft, + FaArrowRight, + FaArrowUp, + FaArrowsAltV, + FaBars, + FaCheckCircle, + FaExclamationTriangle, + FaPencilAlt, + FaShieldAlt, + FaTrashAlt, + FaTrashRestoreAlt, + FaUser, + FaUserCircle, +} from "react-icons/fa"; +import toast from "react-hot-toast"; + +type UserState = { + sortOrder: string; + sortBy: string; + type: string; + page: number; + size: number; + search: string; + status: string; +}; + +// Modes under which UserModal operates +enum UserModalMode { + Delete, + Restore, +} + +// Basic configuration for subject modal +type UserModalBasicConfig = { + mode: UserModalMode; + id: string; + show: boolean; +}; + +// Props for subject modal +type UserModalProps = UserModalBasicConfig & { + onHide: () => void; + handleRestore: (id: string, reason: string) => void; + handleDelete: (id: string, reason: string) => void; +}; + +// Creates a modal with a small form to create/edit a subject +export function UserModal(props: UserModalProps) { + const [reason, setReason] = useState(""); + const [error, setError] = useState(false); + + useEffect(() => { + if (props.show) { + setReason(""); + setError(false); + } + }, [props.show]); + + return ( + + + + {props.mode == UserModalMode.Delete && ( + + Delete user + + )} + {props.mode == UserModalMode.Restore && ( + + Restore user + + )} + + + +
+

+ Are you sure you want to delete user with id:{" "} + {props.id} ? +

+ + + Reason (*) + + setReason(e.target.value)} + /> + {error &&

You must specify a reason

} +
+
+
+ + + {props.mode == UserModalMode.Delete && ( + + )} + {props.mode == UserModalMode.Restore && ( + + )} + +
+ ); +} + +const idToColor = (id: string) => { + // generate hash from id + const hash = Array.from(id).reduce( + (acc, char) => acc + char.charCodeAt(0), + 0, + ); + + // define color palette + const palette = [ + "#ea7286", + "#eab281", + "#e3e19f", + "#a9c484", + "#5d937b", + "#58525a", + "#a07ca7", + "#f4a4bf", + "#f5d1b6", + "#eeede3", + "#d6cec2", + "#a2a6a9", + "#777f8f", + "#a3b2d2", + "#bfded8", + "#bf796d", + ]; + + // select the color + return palette[hash % palette.length]; +}; + +// trim the field if it is too big +const trimField = (value: string, length: number) => { + if (value.length > length) { + return value.slice(0, length) + "..."; + } + return value; +}; + +// create an up/down arrow to designate sorting in a column +const SortMarker = (field: string, sortField: string, sortOrder: string) => { + if (field === sortField) { + if (sortOrder === "DESC") return ; + else if (sortOrder === "ASC") return ; + } + return ; +}; + +// create a user status badge for deleted and active +const UserStatusBadge = (banned: boolean) => { + if (banned) + return ( + + Deleted + + ); + return Active; +}; + +// create a user type badge for identified, admin and verified +const UserTypeBadge = (userType: string) => { + const userTypeLower = userType.toLowerCase(); + if (userTypeLower === "identified") { + return Identified; + } else if (userTypeLower === "admin") { + return ( + + Admin + + ); + } else if (userTypeLower === "validated") { + return ( + + Validated + + ); + } else { + return null; + } +}; + +// create the tooltips +const tooltipDelete = Delete User; +const tooltipRestore = Restore User; +const tooltipEdit = Edit User Details; +const tooltipView = View User Details; + +// the main component that lists the admin users in a table +export default function AdminUsers() { + // toast alert reference used in notification messaging + const toastAlert = useRef({ + message: "", + }); + + const { keycloak, registered } = useContext(AuthContext)!; + + const [userModalConfig, setUserModalConfig] = useState({ + mode: UserModalMode.Delete, + show: false, + id: "", + }); + + // hooks for handling subjects in through the backend + const mutationBanUser = useDeleteUser(keycloak?.token || ""); + const mutationUnbanUser = useRestoreUser(keycloak?.token || ""); + + const handleRestore = (id: string, reason: string) => { + const promise = mutationUnbanUser + .mutateAsync({ user_id: id, reason: reason }) + .catch((err) => { + toastAlert.current = { + message: "Error during user restore.", + }; + throw err; + }) + .then(() => { + toastAlert.current = { + message: "User succesfully restored", + }; + // close the modal + setUserModalConfig((prevConfig) => ({ ...prevConfig, show: false })); + }); + toast.promise(promise, { + loading: "Restoring User...", + success: () => `${toastAlert.current.message}`, + error: () => `${toastAlert.current.message}`, + }); + }; + + const handleDelete = (id: string, reason: string) => { + const promise = mutationBanUser + .mutateAsync({ user_id: id, reason: reason }) + .catch((err) => { + toastAlert.current = { + message: "Error during user ban.", + }; + throw err; + }) + .then(() => { + toastAlert.current = { + message: "Subject succesfully banned.", + }; + // close the modal + setUserModalConfig((prevConfig) => ({ ...prevConfig, show: false })); + }); + toast.promise(promise, { + loading: "Banning...", + success: () => `${toastAlert.current.message}`, + error: () => `${toastAlert.current.message}`, + }); + }; + + const [opts, setOpts] = useState({ + sortBy: "name", + sortOrder: "ASC", + type: "", + page: 1, + size: 20, + search: "", + status: "", + }); + + // handler for changing page size + const handleChangePageSize = (evt: { target: { value: string } }) => { + setOpts({ ...opts, page: 1, size: parseInt(evt.target.value) }); + }; + + // handler for clicking to sort + const handleSortClick = (field: string) => { + if (field === opts.sortBy) { + if (opts.sortOrder === "ASC") { + setOpts({ ...opts, sortOrder: "DESC" }); + } else { + setOpts({ ...opts, sortOrder: "ASC" }); + } + } else { + setOpts({ ...opts, sortOrder: "ASC", sortBy: field }); + } + }; + + // data get admin users + const { isLoading, data, refetch } = useAdminGetUsers({ + size: opts.size, + page: opts.page, + sortBy: opts.sortBy, + sortOrder: opts.sortOrder, + token: keycloak?.token || "", + isRegistered: registered, + search: opts.search, + type: opts.type, + status: opts.status, + }); + + // refetch users when parameters change + useEffect(() => { + refetch(); + }, [opts, refetch]); + + // get the user data to create the table + const users: UserProfile[] = data ? data?.content : []; + + return ( +
+ setUserModalConfig({ ...userModalConfig, show: false })} + handleDelete={handleDelete} + handleRestore={handleRestore} + /> +

+ Users +

+
+ + + { + setOpts({ ...opts, type: e.target.value }); + }} + value={opts.type} + > + + + + + + + + { + setOpts({ ...opts, status: e.target.value }); + }} + value={opts.status} + > + + + + + + +
+ { + setOpts({ ...opts, search: e.target.value }); + }} + value={opts.search} + /> + +
+ +
+
+ + + + + + + + + + + + + + {users.length > 0 ? ( + + {users.map((item) => { + return ( + + + + + + + + + ); + })} + + ) : null} +
+ handleSortClick("name")} + className="cat-cursor-pointer" + > + Name {SortMarker("name", opts.sortBy, opts.sortOrder)} + + + handleSortClick("email")} + className="cat-cursor-pointer" + > + Email {SortMarker("email", opts.sortBy, opts.sortOrder)} + + + Registered + + User Type + + Status +
+
+
+ +
+
+
{item.name ||
}
+
+ + id: {trimField(item.id, 20)} + +
+
+
+
{item.email} + {item.registered_on.split("T")[0]} + + {UserTypeBadge(item.user_type)} + + {UserStatusBadge(item.banned)} + + + + + + + + {item.banned ? ( + + + + ) : ( + + + + )} +
+ {!isLoading && users.length === 0 && ( + +

+ +

+
No data found...
+
+ )} +
+
+ rows per page: + +
+ + {data && data.number_of_page && data.total_pages && ( +
+ + {(data.number_of_page - 1) * opts.size + 1} -{" "} + {(data.number_of_page - 1) * opts.size + data.size_of_page} of{" "} + {data.total_elements} + + { + setOpts({ ...opts, page: opts.page - 1 }); + }} + className={`ms-4 btn py-0 btn-light btn-small ${ + opts.page === 1 ? "disabled text-muted" : null + }`} + > + + + { + setOpts({ ...opts, page: opts.page + 1 }); + }} + className={`btn py-0 btn-light btn-small" ${ + data?.total_pages > data?.number_of_page + ? null + : "disabled text-muted" + }`} + > + + +
+ )} +
+
+
+ ); +} diff --git a/src/types/common.ts b/src/types/common.ts index 3c28b7f2..ac38ebf2 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -44,6 +44,7 @@ export interface ApiPaginationOptions { size?: number; page?: number; sortBy?: string; + sortOrder?: string; } export interface ApiPublicAssessmentOptions { @@ -104,3 +105,9 @@ export type UserAccess = { user_id: string; reason?: string; }; + +export type ApiUsers = ApiOptions & { + type: string; + search: string; + status: string; +}; From 5cc583ee7ecf1b17504dc720c72771c3f276e845 Mon Sep 17 00:00:00 2001 From: Konstantinos Kagkelidis Date: Mon, 22 Apr 2024 09:51:11 +0300 Subject: [PATCH 47/51] CAT-344 redesign admin view validations --- src/App.tsx | 3 +- src/api/services/validations.ts | 31 ++- src/pages/admin/AdminUsers.tsx | 371 ++++++++++++--------------- src/pages/admin/AdminValidations.tsx | 361 ++++++++++++++++++++++++++ src/types/common.ts | 6 + src/utils/admin.ts | 38 +++ 6 files changed, 601 insertions(+), 209 deletions(-) create mode 100644 src/pages/admin/AdminValidations.tsx create mode 100644 src/utils/admin.ts diff --git a/src/App.tsx b/src/App.tsx index f551323d..8e5c613d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import Subjects from "./pages/Subjects"; import About from "./pages/About"; import { AssessmentEditMode } from "./types"; import AdminUsers from "./pages/admin/AdminUsers"; +import AdminValidations from "./pages/admin/AdminValidations"; const queryClient = new QueryClient(); @@ -147,7 +148,7 @@ function App() { } /> }> - } /> + } /> + useQuery({ + queryKey: ["validations"], + queryFn: async () => { + let url = `/admin/validations?size=${size}&page=${page}`; + sortBy ? (url = `${url}&sort=${sortBy}`) : null; + sortOrder ? (url = `${url}&order=${sortOrder}`) : null; + type ? (url = `${url}&type=${type}`) : null; + status ? (url = `${url}&status=${status}`) : null; + search ? (url = `${url}&search=${search}`) : null; + const response = await APIClient(token).get(url); + return response.data; + }, + onError: (error: AxiosError) => { + return handleBackendError(error); + }, + enabled: !!token && isRegistered, + }); + export const useGetValidationList = ({ size, page, diff --git a/src/pages/admin/AdminUsers.tsx b/src/pages/admin/AdminUsers.tsx index 55e2a3f5..aeba14db 100644 --- a/src/pages/admin/AdminUsers.tsx +++ b/src/pages/admin/AdminUsers.tsx @@ -30,6 +30,7 @@ import { FaUserCircle, } from "react-icons/fa"; import toast from "react-hot-toast"; +import { idToColor, trimField } from "@/utils/admin"; type UserState = { sortOrder: string; @@ -161,45 +162,6 @@ export function UserModal(props: UserModalProps) { ); } -const idToColor = (id: string) => { - // generate hash from id - const hash = Array.from(id).reduce( - (acc, char) => acc + char.charCodeAt(0), - 0, - ); - - // define color palette - const palette = [ - "#ea7286", - "#eab281", - "#e3e19f", - "#a9c484", - "#5d937b", - "#58525a", - "#a07ca7", - "#f4a4bf", - "#f5d1b6", - "#eeede3", - "#d6cec2", - "#a2a6a9", - "#777f8f", - "#a3b2d2", - "#bfded8", - "#bf796d", - ]; - - // select the color - return palette[hash % palette.length]; -}; - -// trim the field if it is too big -const trimField = (value: string, length: number) => { - if (value.length > length) { - return value.slice(0, length) + "..."; - } - return value; -}; - // create an up/down arrow to designate sorting in a column const SortMarker = (field: string, sortField: string, sortOrder: string) => { if (field === sortField) { @@ -422,183 +384,178 @@ export default function AdminUsers() { - - - - - - - - - - - - - {users.length > 0 ? ( - - {users.map((item) => { - return ( - - + ); + })} + + ) : null} +
- handleSortClick("name")} - className="cat-cursor-pointer" - > - Name {SortMarker("name", opts.sortBy, opts.sortOrder)} - - - handleSortClick("email")} - className="cat-cursor-pointer" - > - Email {SortMarker("email", opts.sortBy, opts.sortOrder)} - - - Registered - - User Type - - Status -
-
+ + + + + + + + + + + + {users.length > 0 ? ( + + {users.map((item) => { + return ( + + - - - - - + + + + + - - ); - })} - - ) : null} -
+ handleSortClick("name")} + className="cat-cursor-pointer" + > + Name {SortMarker("name", opts.sortBy, opts.sortOrder)} + + + handleSortClick("email")} + className="cat-cursor-pointer" + > + Email {SortMarker("email", opts.sortBy, opts.sortOrder)} + + + Registered + + User Type + + Status +
+
+
+ +
+
+
{item.name ||
}
- -
-
-
{item.name ||
}
-
- - id: {trimField(item.id, 20)} - -
+ + id: {trimField(item.id, 20)} +
-
{item.email} - {item.registered_on.split("T")[0]} - - {UserTypeBadge(item.user_type)} - - {UserStatusBadge(item.banned)} - - - {item.email} + {item.registered_on.split("T")[0]} + + {UserTypeBadge(item.user_type)} + + {UserStatusBadge(item.banned)} + + + + + + + + {item.banned ? ( + + - - - {item.banned ? ( - - - - ) : ( - - - - )} -
- {!isLoading && users.length === 0 && ( - -

- -

-
No data found...
-
- )} -
-
- rows per page: -
+ {!isLoading && users.length === 0 && ( + +

+ +

+
No data found...
+
+ )} +
+
+ rows per page: + +
+ + {data && data.number_of_page && data.total_pages && ( +
+ + {(data.number_of_page - 1) * opts.size + 1} -{" "} + {(data.number_of_page - 1) * opts.size + data.size_of_page} of{" "} + {data.total_elements} + + { + setOpts({ ...opts, page: opts.page - 1 }); + }} + className={`ms-4 btn py-0 btn-light btn-small ${ + opts.page === 1 ? "disabled text-muted" : null + }`} + > + + + { + setOpts({ ...opts, page: opts.page + 1 }); + }} + className={`btn py-0 btn-light btn-small" ${ + data?.total_pages > data?.number_of_page + ? null + : "disabled text-muted" + }`} > - - - - - + +
- - {data && data.number_of_page && data.total_pages && ( -
- - {(data.number_of_page - 1) * opts.size + 1} -{" "} - {(data.number_of_page - 1) * opts.size + data.size_of_page} of{" "} - {data.total_elements} - - { - setOpts({ ...opts, page: opts.page - 1 }); - }} - className={`ms-4 btn py-0 btn-light btn-small ${ - opts.page === 1 ? "disabled text-muted" : null - }`} - > - - - { - setOpts({ ...opts, page: opts.page + 1 }); - }} - className={`btn py-0 btn-light btn-small" ${ - data?.total_pages > data?.number_of_page - ? null - : "disabled text-muted" - }`} - > - - -
- )} -
-
+ )} +
); } diff --git a/src/pages/admin/AdminValidations.tsx b/src/pages/admin/AdminValidations.tsx new file mode 100644 index 00000000..813e23b5 --- /dev/null +++ b/src/pages/admin/AdminValidations.tsx @@ -0,0 +1,361 @@ +import { useAdminGetValidations } from "@/api"; +import { AuthContext } from "@/auth"; +import { ValidationResponse } from "@/types"; +import { idToColor, trimField } from "@/utils/admin"; +import { useContext, useEffect, useState } from "react"; +import { + Alert, + Button, + Col, + Form, + OverlayTrigger, + Row, + Table, + Tooltip, +} from "react-bootstrap"; +import { + FaArrowDown, + FaArrowLeft, + FaArrowRight, + FaArrowUp, + FaArrowsAltV, + FaBars, + FaCheck, + FaCheckCircle, + FaExclamationTriangle, + FaGlasses, + FaTimes, + FaUserCircle, +} from "react-icons/fa"; +import { Link } from "react-router-dom"; + +type ValidationState = { + sortOrder: string; + sortBy: string; + type: string; + page: number; + size: number; + search: string; + status: string; +}; + +// create an up/down arrow to designate sorting in a column +export function SortMarker( + field: string, + sortField: string, + sortOrder: string, +) { + if (field === sortField) { + if (sortOrder === "DESC") return ; + else if (sortOrder === "ASC") return ; + } + return ; +} + +// create a validation status badge for approved, rejected, pending +const ValidationStatusBadge = (status: string) => { + console.log(status); + if (status === "APPROVED") { + return ( + + Approved + + ); + } else if (status === "REJECTED") { + return ( + + Rejected + + ); + } else if (status === "REVIEW") { + return ( + + Pending Review + + ); + } else { + return null; + } +}; + +// create the tooltips +const tooltipAccept = Accept Validation; +const tooltipReject = Reject Validation; +const tooltipView = View User Details; + +// the main component that lists the admin users in a table +export default function AdminUsers() { + const { keycloak, registered } = useContext(AuthContext)!; + + const [opts, setOpts] = useState({ + sortBy: "", + sortOrder: "", + type: "", + page: 1, + size: 20, + search: "", + status: "", + }); + + // handler for changing page size + const handleChangePageSize = (evt: { target: { value: string } }) => { + setOpts({ ...opts, page: 1, size: parseInt(evt.target.value) }); + }; + + // handler for clicking to sort + const handleSortClick = (field: string) => { + if (field === opts.sortBy) { + if (opts.sortOrder === "ASC") { + setOpts({ ...opts, sortOrder: "DESC" }); + } else { + setOpts({ ...opts, sortOrder: "ASC" }); + } + } else { + setOpts({ ...opts, sortOrder: "ASC", sortBy: field }); + } + }; + + // data get admin users + const { isLoading, data, refetch } = useAdminGetValidations({ + size: opts.size, + page: opts.page, + sortBy: opts.sortBy, + sortOrder: opts.sortOrder, + token: keycloak?.token || "", + isRegistered: registered, + search: opts.search, + type: opts.type, + status: opts.status, + }); + + // refetch users when parameters change + useEffect(() => { + refetch(); + }, [opts, refetch]); + + // get the validation data to create the table + const validations: ValidationResponse[] = data ? data?.content : []; + + return ( +
+

+ Validations +

+
+ + + { + setOpts({ ...opts, type: e.target.value }); + }} + value={opts.type} + > + + + + + + + + { + setOpts({ ...opts, status: e.target.value }); + }} + value={opts.status} + > + + + + + + + +
+ { + setOpts({ ...opts, search: e.target.value }); + }} + value={opts.search} + /> + +
+ +
+
+ + + + + + + + + + + + + {validations.length > 0 ? ( + + {validations.map((item) => { + return ( + + + + + + + + + ); + })} + + ) : null} +
+ Id + + Name + + handleSortClick("organisationName")} + className="cat-cursor-pointer" + > + Organization{" "} + {SortMarker("organisationName", opts.sortBy, opts.sortOrder)} + + + Actor Name + + Status +
{item.id} +
+
+ +
+
+
{item.user_name ||
}
+
+ + id: {trimField(item.user_id, 20)} + +
+
+
+
+
+
{item.organisation_name ||
}
+
+ + Role: {item.organisation_role} + +
+
+
{item.actor_name} + {ValidationStatusBadge(item.status)} + +
+ + + + + + {item.status === "REVIEW" ? ( + + + + + + ) : null} + {item.status === "REVIEW" ? ( + + + + + + ) : null} +
+
+ {!isLoading && validations.length === 0 && ( + +

+ +

+
No data found...
+
+ )} +
+
+ rows per page: + +
+ + {data && data.number_of_page && data.total_pages && ( +
+ + {(data.number_of_page - 1) * opts.size + 1} -{" "} + {(data.number_of_page - 1) * opts.size + data.size_of_page} of{" "} + {data.total_elements} + + { + setOpts({ ...opts, page: opts.page - 1 }); + }} + className={`ms-4 btn py-0 btn-light btn-small ${ + opts.page === 1 ? "disabled text-muted" : null + }`} + > + + + { + setOpts({ ...opts, page: opts.page + 1 }); + }} + className={`btn py-0 btn-light btn-small" ${ + data?.total_pages > data?.number_of_page + ? null + : "disabled text-muted" + }`} + > + + +
+ )} +
+
+ ); +} diff --git a/src/types/common.ts b/src/types/common.ts index ac38ebf2..1b231855 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -111,3 +111,9 @@ export type ApiUsers = ApiOptions & { search: string; status: string; }; + +export type ApiValidations = ApiOptions & { + type: string; + search: string; + status: string; +}; diff --git a/src/utils/admin.ts b/src/utils/admin.ts new file mode 100644 index 00000000..152dd6f8 --- /dev/null +++ b/src/utils/admin.ts @@ -0,0 +1,38 @@ +export function idToColor(id: string) { + // generate hash from id + const hash = Array.from(id).reduce( + (acc, char) => acc + char.charCodeAt(0), + 0, + ); + + // define color palette + const palette = [ + "#ea7286", + "#eab281", + "#e3e19f", + "#a9c484", + "#5d937b", + "#58525a", + "#a07ca7", + "#f4a4bf", + "#f5d1b6", + "#eeede3", + "#d6cec2", + "#a2a6a9", + "#777f8f", + "#a3b2d2", + "#bfded8", + "#bf796d", + ]; + + // select the color + return palette[hash % palette.length]; +} + +// trim the field if it is too big +export function trimField(value: string, length: number) { + if (value.length > length) { + return value.slice(0, length) + "..."; + } + return value; +} From 3fe150b24c47dcb48050f2b7cde080d5e9862043 Mon Sep 17 00:00:00 2001 From: Konstantinos Kagkelidis Date: Mon, 22 Apr 2024 15:50:04 +0300 Subject: [PATCH 48/51] CAT-344 Fix button layout in admin validations and admin user views --- src/pages/admin/AdminUsers.tsx | 8 ++++---- src/pages/admin/AdminValidations.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/admin/AdminUsers.tsx b/src/pages/admin/AdminUsers.tsx index aeba14db..aa91d5cc 100644 --- a/src/pages/admin/AdminUsers.tsx +++ b/src/pages/admin/AdminUsers.tsx @@ -453,19 +453,19 @@ export default function AdminUsers() { - - {item.banned ? (