diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index 130f360d2ef..f04045d0c72 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -14,6 +14,10 @@ jobs: permissions: write-all if: github.repository == 'coronasafe/care_fe' runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + containers: [1,2,3,4,5,6,7,8] steps: - name: Checkout 📥 uses: actions/checkout@v3 @@ -60,39 +64,14 @@ jobs: wait-on-timeout: 300 browser: chrome record: true + parallel: true env: CARE_API: http://localhost:9000 CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_OPTIONS: --max_old_space_size=4096 - - - name: Remove cypress passed label on failure 🏷️ - uses: actions-ecosystem/action-remove-labels@v1 - if: failure() - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - labels: cypress passed - - - name: Add cypress passed label on success 🏷️ - uses: actions-ecosystem/action-add-labels@v1 - if: success() - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - labels: cypress passed - - - name: Remove cypress failed label on success 🏷️ - uses: actions-ecosystem/action-remove-labels@v1 - if: success() - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - labels: cypress failed - - - name: Add cypress failed label on failure 🏷️ - uses: actions-ecosystem/action-add-labels@v1 - if: failure() - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - labels: cypress failed + COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}} + COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}} - name: Upload cypress screenshots on failure 📸 uses: actions/upload-artifact@v3 diff --git a/cypress.config.ts b/cypress.config.ts index 014eb1f4d07..66915f8a11b 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -19,7 +19,6 @@ export default defineConfig({ return null; }, }); - return config; }, baseUrl: "http://localhost:4000", diff --git a/cypress/e2e/assets_spec/asset_homepage.cy.ts b/cypress/e2e/assets_spec/asset_homepage.cy.ts index fa24adf5b21..8bc24c7370f 100644 --- a/cypress/e2e/assets_spec/asset_homepage.cy.ts +++ b/cypress/e2e/assets_spec/asset_homepage.cy.ts @@ -5,6 +5,7 @@ import { AssetSearchPage } from "../../pageobject/Asset/AssetSearch"; import { AssetQRScanPage } from "../../pageobject/Asset/AssetQRScan"; import { AssetPagination } from "../../pageobject/Asset/AssetPagination"; import { AssetFilters } from "../../pageobject/Asset/AssetFilters"; +import { AssetPage } from "../../pageobject/Asset/AssetCreation"; import LoginPage from "../../pageobject/Login/LoginPage"; import { v4 as uuidv4 } from "uuid"; @@ -13,6 +14,7 @@ describe("Asset Tab", () => { const assetQRScanPage = new AssetQRScanPage(); const assetPagination = new AssetPagination(); const assetFilters = new AssetFilters(); + const assetPage = new AssetPage(); const loginPage = new LoginPage(); const assetName = "Dummy Camera 10"; const qrCode = uuidv4(); @@ -83,6 +85,29 @@ describe("Asset Tab", () => { assetPagination.navigateToPreviousPage(); }); + it("Import new asset", () => { + assetPage.selectassetimportbutton(); + assetPage.selectImportOption(); + assetPage.selectImportFacility("Dummy Facility 1"); + assetPage.importAssetFile(); + assetPage.selectImportLocation("Camera Locations"); + assetPage.clickImportAsset(); + assetPage.verifySuccessNotification("Assets imported successfully"); + }); + + it("verify imported asset", () => { + assetSearchPage.typeSearchKeyword("New Test Asset"); + assetSearchPage.pressEnter(); + assetSearchPage.verifyAssetIsPresent("New Test Asset"); + }); + + it("Export asset", () => { + assetPage.selectassetimportbutton(); + assetPage.selectjsonexportbutton(); + assetPage.selectassetimportbutton(); + assetPage.selectcsvexportbutton(); + }); + afterEach(() => { cy.saveLocalStorage(); }); diff --git a/cypress/e2e/assets_spec/assets_creation.cy.ts b/cypress/e2e/assets_spec/assets_creation.cy.ts new file mode 100644 index 00000000000..3e7b24eeca6 --- /dev/null +++ b/cypress/e2e/assets_spec/assets_creation.cy.ts @@ -0,0 +1,178 @@ +/// +import { afterEach, before, beforeEach, cy, describe, it } from "local-cypress"; +import { AssetPage } from "../../pageobject/Asset/AssetCreation"; +import { v4 as uuidv4 } from "uuid"; +import LoginPage from "../../pageobject/Login/LoginPage"; +import { AssetSearchPage } from "../../pageobject/Asset/AssetSearch"; + +describe("Asset", () => { + const assetPage = new AssetPage(); + const assetSearchPage = new AssetSearchPage(); + const loginPage = new LoginPage(); + const phone_number = "9999999999"; + const serialNumber = Math.floor(Math.random() * 10 ** 10).toString(); + + before(() => { + loginPage.loginAsDisctrictAdmin(); + cy.saveLocalStorage(); + }); + + beforeEach(() => { + cy.restoreLocalStorage(); + cy.awaitUrl("/assets"); + }); + + it("Verify asset creation fields throws error if empty", () => { + assetPage.createAsset(); + assetPage.selectFacility("Dummy Facility 1"); + assetPage.clickCreateAsset(); + + assetPage.verifyEmptyAssetNameError(); + assetPage.verifyEmptyAssetTypeError(); + assetPage.verifyEmptyLocationError(); + assetPage.verifyEmptyStatusError(); + assetPage.verifyEmptyPhoneError(); + }); + + //Create an asset + + it("Create an Asset", () => { + assetPage.createAsset(); + assetPage.selectFacility("Dummy Facility 1"); + assetPage.selectLocation("Camera Loc"); + assetPage.selectAssetType("Internal"); + assetPage.selectAssetClass("ONVIF Camera"); + + const qr_id_1 = uuidv4(); + + assetPage.enterAssetDetails( + "New Test Asset 1", + "Test Description", + "Working", + qr_id_1, + "Manufacturer's Name", + "2025-12-25", + "Customer Support's Name", + phone_number, + "email@support.com", + "Vendor's Name", + serialNumber, + "25122021", + "Test note for asset creation!" + ); + + assetPage.clickCreateAddMore(); + assetPage.verifySuccessNotification("Asset created successfully"); + + const qr_id_2 = uuidv4(); + + assetPage.selectLocation("Camera Loc"); + assetPage.selectAssetType("Internal"); + assetPage.selectAssetClass("ONVIF Camera"); + assetPage.enterAssetDetails( + "New Test Asset 2", + "Test Description", + "Working", + qr_id_2, + "Manufacturer's Name", + "2025-12-25", + "Customer Support's Name", + phone_number, + "email@support.com", + "Vendor's Name", + serialNumber, + "25122021", + "Test note for asset creation!" + ); + + assetPage.interceptAssetCreation(); + assetPage.clickCreateAsset(); + assetPage.verifyAssetCreation(); + assetPage.verifySuccessNotification("Asset created successfully"); + + assetSearchPage.typeSearchKeyword("New Test Asset 2"); + assetSearchPage.pressEnter(); + assetSearchPage.verifyAssetIsPresent("New Test Asset 2"); + }); + + it("Edit an Asset", () => { + assetPage.openCreatedAsset(); + + const qr_id = uuidv4(); + + assetPage.editAssetDetails( + "New Test Asset Edited", + "Test Description Edited", + qr_id, + "Manufacturer's Name Edited", + "Customer Support's Name Edited", + "Vendor's Name Edited", + "Test note for asset creation edited!", + "25122021" + ); + + assetPage.clickUpdateAsset(); + + assetPage.verifySuccessNotification("Asset updated successfully"); + }); + + it("Verify Editted Asset", () => { + assetSearchPage.typeSearchKeyword("New Test Asset Edited"); + assetSearchPage.pressEnter(); + assetSearchPage.verifyAssetIsPresent("New Test Asset Edited"); + }); + + it("Configure an asset", () => { + assetPage.openCreatedAsset(); + assetPage.spyAssetConfigureApi(); + assetPage.configureAsset( + "Host name", + "192.168.1.64", + "remote_user", + "2jCkrCRSeahzKEU", + "d5694af2-21e2-4a39-9bad-2fb98d9818bd" + ); + assetPage.clickConfigureAsset(); + assetPage.verifyAssetConfiguration(200); + }); + + it("Add an vital monitor asset and configure it", () => { + assetPage.createAsset(); + assetPage.selectFacility("Dummy Facility 1"); + assetPage.selectLocation("Camera Loc"); + assetPage.selectAssetType("Internal"); + assetPage.selectAssetClass("HL7 Vitals Monitor"); + + const qr_id_1 = uuidv4(); + + assetPage.enterAssetDetails( + "New Test Asset Vital", + "Test Description", + "Working", + qr_id_1, + "Manufacturer's Name", + "2025-12-25", + "Customer Support's Name", + phone_number, + "email@support.com", + "Vendor's Name", + serialNumber, + "25122021", + "Test note for asset creation!" + ); + assetPage.interceptAssetCreation(); + assetPage.clickCreateAsset(); + assetPage.verifyAssetCreation(); + + assetSearchPage.typeSearchKeyword("New Test Asset Vital"); + assetSearchPage.pressEnter(); + + assetPage.openCreatedAsset(); + assetPage.configureVitalAsset("Host name", "192.168.1.64"); + assetPage.clickConfigureVital(); + }); + + afterEach(() => { + cy.saveLocalStorage(); + }); +}); diff --git a/cypress/e2e/assets_spec/assets_manage.cy.ts b/cypress/e2e/assets_spec/assets_manage.cy.ts index af3efb917dd..f27fd302a0c 100644 --- a/cypress/e2e/assets_spec/assets_manage.cy.ts +++ b/cypress/e2e/assets_spec/assets_manage.cy.ts @@ -1,16 +1,10 @@ -/// import { afterEach, before, beforeEach, cy, describe, it } from "local-cypress"; import { AssetPage } from "../../pageobject/Asset/AssetCreation"; -import { v4 as uuidv4 } from "uuid"; import LoginPage from "../../pageobject/Login/LoginPage"; -import { AssetSearchPage } from "../../pageobject/Asset/AssetSearch"; describe("Asset", () => { const assetPage = new AssetPage(); - const assetSearchPage = new AssetSearchPage(); const loginPage = new LoginPage(); - const phone_number = "9999999999"; - const serialNumber = Math.floor(Math.random() * 10 ** 10).toString(); before(() => { loginPage.loginAsDisctrictAdmin(); @@ -22,150 +16,6 @@ describe("Asset", () => { cy.awaitUrl("/assets"); }); - it("Verify asset creation fields throws error if empty", () => { - assetPage.createAsset(); - assetPage.selectFacility("Dummy Facility 1"); - assetPage.clickCreateAsset(); - - assetPage.verifyEmptyAssetNameError(); - assetPage.verifyEmptyAssetTypeError(); - assetPage.verifyEmptyLocationError(); - assetPage.verifyEmptyStatusError(); - assetPage.verifyEmptyPhoneError(); - }); - - //Create an asset - - it("Create an Asset", () => { - assetPage.createAsset(); - assetPage.selectFacility("Dummy Facility 1"); - assetPage.selectLocation("Camera Loc"); - assetPage.selectAssetType("Internal"); - assetPage.selectAssetClass("ONVIF Camera"); - - const qr_id_1 = uuidv4(); - - assetPage.enterAssetDetails( - "New Test Asset 1", - "Test Description", - "Working", - qr_id_1, - "Manufacturer's Name", - "2025-12-25", - "Customer Support's Name", - phone_number, - "email@support.com", - "Vendor's Name", - serialNumber, - "25122021", - "Test note for asset creation!" - ); - - assetPage.clickCreateAddMore(); - assetPage.verifySuccessNotification("Asset created successfully"); - - const qr_id_2 = uuidv4(); - - assetPage.selectLocation("Camera Loc"); - assetPage.selectAssetType("Internal"); - assetPage.selectAssetClass("ONVIF Camera"); - assetPage.enterAssetDetails( - "New Test Asset 2", - "Test Description", - "Working", - qr_id_2, - "Manufacturer's Name", - "2025-12-25", - "Customer Support's Name", - phone_number, - "email@support.com", - "Vendor's Name", - serialNumber, - "25122021", - "Test note for asset creation!" - ); - - assetPage.interceptAssetCreation(); - assetPage.clickCreateAsset(); - assetPage.verifyAssetCreation(); - assetPage.verifySuccessNotification("Asset created successfully"); - - assetSearchPage.typeSearchKeyword("New Test Asset 2"); - assetSearchPage.pressEnter(); - assetSearchPage.verifyAssetIsPresent("New Test Asset 2"); - }); - - it("Edit an Asset", () => { - assetPage.openCreatedAsset(); - - const qr_id = uuidv4(); - - assetPage.editAssetDetails( - "New Test Asset Edited", - "Test Description Edited", - qr_id, - "Manufacturer's Name Edited", - "Customer Support's Name Edited", - "Vendor's Name Edited", - "Test note for asset creation edited!", - "25122021" - ); - - assetPage.clickUpdateAsset(); - - assetPage.verifySuccessNotification("Asset updated successfully"); - }); - - it("Configure an asset", () => { - assetPage.openCreatedAsset(); - assetPage.spyAssetConfigureApi(); - assetPage.configureAsset( - "Host name", - "192.168.1.64", - "remote_user", - "2jCkrCRSeahzKEU", - "d5694af2-21e2-4a39-9bad-2fb98d9818bd" - ); - assetPage.clickConfigureAsset(); - assetPage.verifyAssetConfiguration(200); - }); - - it("Add an vital monitor asset and configure it", () => { - assetPage.createAsset(); - assetPage.selectFacility("Dummy Facility 1"); - assetPage.selectLocation("Camera Loc"); - assetPage.selectAssetType("Internal"); - assetPage.selectAssetClass("HL7 Vitals Monitor"); - - const qr_id_1 = uuidv4(); - - assetPage.enterAssetDetails( - "New Test Asset Vital", - "Test Description", - "Working", - qr_id_1, - "Manufacturer's Name", - "2025-12-25", - "Customer Support's Name", - phone_number, - "email@support.com", - "Vendor's Name", - serialNumber, - "25122021", - "Test note for asset creation!" - ); - assetPage.interceptAssetCreation(); - assetPage.clickCreateAsset(); - assetPage.verifyAssetCreation(); - - assetSearchPage.typeSearchKeyword("New Test Asset Vital"); - assetSearchPage.pressEnter(); - - assetPage.openCreatedAsset(); - assetPage.configureVitalAsset("Host name", "192.168.1.64"); - assetPage.clickConfigureVital(); - }); - it("Delete an Asset", () => { assetPage.openCreatedAsset(); assetPage.interceptDeleteAssetApi(); @@ -173,16 +23,6 @@ describe("Asset", () => { assetPage.verifyDeleteStatus(); }); - it("Import new asset", () => { - assetPage.selectImportOption(); - assetPage.selectImportFacility("Dummy Facility 1"); - assetPage.importAssetFile(); - assetPage.selectImportLocation("Camera Locations"); - assetPage.clickImportAsset(); - - assetPage.verifySuccessNotification("Assets imported successfully"); - }); - afterEach(() => { cy.saveLocalStorage(); }); diff --git a/cypress/e2e/facility_spec/facility.cy.ts b/cypress/e2e/facility_spec/facility.cy.ts index f8fe04b4f30..f2ff847c9d9 100644 --- a/cypress/e2e/facility_spec/facility.cy.ts +++ b/cypress/e2e/facility_spec/facility.cy.ts @@ -7,7 +7,6 @@ describe("Facility Creation", () => { let facilityUrl: string; const facilityPage = new FacilityPage(); const loginPage = new LoginPage(); - const phone_number = "9999999999"; before(() => { loginPage.loginAsDisctrictAdmin(); @@ -40,14 +39,9 @@ describe("Facility Creation", () => { facilityPage.selectAreaOfSpecialization("General Medicine"); facilityPage.fillDoctorCount("5"); facilityPage.saveAndExitDoctorForm(); - - cy.url().then((initialUrl) => { - cy.get("button#save-and-exit").should("not.exist"); - cy.url() - .should("not.equal", initialUrl) - .then((newUrl) => { - facilityUrl = newUrl; - }); + facilityPage.verifyfacilitynewurl(); + cy.url().then((newUrl) => { + facilityUrl = newUrl; }); }); @@ -75,24 +69,6 @@ describe("Facility Creation", () => { facilityPage.verifySuccessNotification("Facility updated successfully"); }); - it("Create a resource request", () => { - facilityPage.visitUpdateFacilityPage(facilityUrl); - facilityPage.clickManageFacilityDropdown(); - facilityPage.clickResourceRequestOption(); - facilityPage.fillResourceRequestDetails( - "Test User", - phone_number, - "cypress", - "Test title", - "10", - "Test description" - ); - facilityPage.clickSubmitRequestButton(); - facilityPage.verifySuccessNotification( - "Resource request created successfully" - ); - }); - it("Delete a facility", () => { facilityPage.visitUpdateFacilityPage(facilityUrl); facilityPage.clickManageFacilityDropdown(); diff --git a/cypress/e2e/resource_spec/resources.cy.ts b/cypress/e2e/resource_spec/resources.cy.ts index b3393d0942b..acf179db120 100644 --- a/cypress/e2e/resource_spec/resources.cy.ts +++ b/cypress/e2e/resource_spec/resources.cy.ts @@ -1,10 +1,14 @@ import { afterEach, before, beforeEach, cy, describe, it } from "local-cypress"; import LoginPage from "../../pageobject/Login/LoginPage"; import ResourcePage from "../../pageobject/Resource/ResourcePage"; +import FacilityPage from "../../pageobject/Facility/FacilityCreation"; describe("Resource Page", () => { + let createdResource: string; const loginPage = new LoginPage(); const resourcePage = new ResourcePage(); + const facilityPage = new FacilityPage(); + const phone_number = "9999999999"; before(() => { loginPage.loginAsDisctrictAdmin(); @@ -34,8 +38,34 @@ describe("Resource Page", () => { resourcePage.clickBoardViewButton(); }); + it("Create a resource request", () => { + cy.visit("/facility"); + cy.get("#search").click().type("dummy facility 1"); + cy.intercept("GET", "**/api/v1/facility/**").as("loadFacilities"); + cy.get("#facility-details").click(); + cy.wait("@loadFacilities").its("response.statusCode").should("eq", 200); + facilityPage.clickManageFacilityDropdown(); + facilityPage.clickResourceRequestOption(); + facilityPage.fillResourceRequestDetails( + "Test User", + phone_number, + "Dummy", + "Test title", + "10", + "Test description" + ); + facilityPage.clickSubmitRequestButton(); + facilityPage.verifySuccessNotification( + "Resource request created successfully" + ); + facilityPage.verifyresourcenewurl(); + cy.url().then((url) => { + createdResource = url; + }); + }); + it("Update the status of resource", () => { - resourcePage.openAlreadyCreatedResource(); + cy.visit(createdResource); resourcePage.clickUpdateStatus(); resourcePage.updateStatus("APPROVED"); resourcePage.clickSubmitButton(); @@ -45,7 +75,7 @@ describe("Resource Page", () => { }); it("Post comment for a resource", () => { - resourcePage.openAlreadyCreatedResource(); + cy.visit(createdResource); resourcePage.addCommentForResource("Test comment"); resourcePage.clickPostCommentButton(); resourcePage.verifySuccessNotification("Comment added successfully"); diff --git a/cypress/pageobject/Asset/AssetCreation.ts b/cypress/pageobject/Asset/AssetCreation.ts index fdaf802d4b7..b939c31405f 100644 --- a/cypress/pageobject/Asset/AssetCreation.ts +++ b/cypress/pageobject/Asset/AssetCreation.ts @@ -234,8 +234,29 @@ export class AssetPage { }); } - selectImportOption() { + selectassetimportbutton() { cy.get("[data-testid=import-asset-button]").click(); + } + + selectjsonexportbutton() { + cy.intercept("GET", "**/api/v1/asset/?json=true**").as("getJsonexport"); + cy.get("#export-json-option").click(); + cy.wait("@getJsonexport").then(({ request, response }) => { + expect(response.statusCode).to.eq(200); + expect(request.url).to.include("json=true"); + }); + } + + selectcsvexportbutton() { + cy.intercept("GET", "**/api/v1/asset/?csv=true**").as("getCsvexport"); + cy.get("#export-csv-option").click(); + cy.wait("@getCsvexport").then(({ request, response }) => { + expect(response.statusCode).to.eq(200); + expect(request.url).to.include("csv=true"); + }); + } + + selectImportOption() { cy.get(".import-assets-button").click(); } diff --git a/cypress/pageobject/Facility/FacilityCreation.ts b/cypress/pageobject/Facility/FacilityCreation.ts index aa46d4f1105..ef4e65781e0 100644 --- a/cypress/pageobject/Facility/FacilityCreation.ts +++ b/cypress/pageobject/Facility/FacilityCreation.ts @@ -100,7 +100,9 @@ class FacilityPage { } saveAndExitDoctorForm() { + cy.intercept("GET", "**/api/v1/facility/**").as("createFacilities"); cy.get("button#save-and-exit").click(); + cy.wait("@createFacilities").its("response.statusCode").should("eq", 200); } clickManageFacilityDropdown() { @@ -131,6 +133,14 @@ class FacilityPage { cy.get("#delete-facility").contains("Delete Facility").click(); } + verifyfacilitynewurl() { + cy.url().should("match", /facility\/[a-z\d-]+/); + } + + verifyresourcenewurl() { + cy.url().should("match", /resource\/[a-z\d-]+/); + } + confirmDeleteFacility() { cy.intercept("DELETE", "**/api/v1/facility/**").as("deleteFacility"); cy.get("#submit").contains("Delete").click(); diff --git a/cypress/pageobject/Resource/ResourcePage.ts b/cypress/pageobject/Resource/ResourcePage.ts index 1fecf86b9e1..f7feac925bf 100644 --- a/cypress/pageobject/Resource/ResourcePage.ts +++ b/cypress/pageobject/Resource/ResourcePage.ts @@ -46,10 +46,6 @@ class ResourcePage { cy.contains("Board View").click(); } - openAlreadyCreatedResource() { - cy.get("[data-testid='resource-details']").first().click(); - } - clickUpdateStatus() { cy.get("[data-testid='update-status']").click(); } diff --git a/package-lock.json b/package-lock.json index dda8ac9dd79..db11b8c5220 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11127,9 +11127,9 @@ } }, "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -13742,8 +13742,6 @@ }, "node_modules/npm/node_modules/@colors/colors": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -13754,8 +13752,6 @@ }, "node_modules/npm/node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "inBundle": true, "license": "ISC", @@ -13773,8 +13769,6 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, "inBundle": true, "license": "MIT", @@ -13787,16 +13781,12 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "inBundle": true, "license": "MIT", @@ -13814,8 +13804,6 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14068,8 +14056,6 @@ }, "node_modules/npm/node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14154,8 +14140,6 @@ }, "node_modules/npm/node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14182,8 +14166,6 @@ }, "node_modules/npm/node_modules/aggregate-error": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "inBundle": true, "license": "MIT", @@ -14197,8 +14179,6 @@ }, "node_modules/npm/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14208,8 +14188,6 @@ }, "node_modules/npm/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14225,8 +14203,6 @@ }, "node_modules/npm/node_modules/aproba": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true, "inBundle": true, "license": "ISC" @@ -14252,16 +14228,12 @@ }, "node_modules/npm/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { @@ -14306,10 +14278,7 @@ }, "node_modules/npm/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "inBundle": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -14385,8 +14354,6 @@ }, "node_modules/npm/node_modules/chownr": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -14423,8 +14390,6 @@ }, "node_modules/npm/node_modules/clean-stack": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, "inBundle": true, "license": "MIT", @@ -14462,8 +14427,6 @@ }, "node_modules/npm/node_modules/clone": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14482,8 +14445,6 @@ }, "node_modules/npm/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14496,8 +14457,6 @@ }, "node_modules/npm/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "inBundle": true, "license": "MIT" @@ -14532,24 +14491,18 @@ }, "node_modules/npm/node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/console-control-strings": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/cross-spawn": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "inBundle": true, "license": "MIT", @@ -14581,8 +14534,6 @@ }, "node_modules/npm/node_modules/cssesc": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14595,8 +14546,6 @@ }, "node_modules/npm/node_modules/debug": { "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -14614,16 +14563,12 @@ }, "node_modules/npm/node_modules/debug/node_modules/ms": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/defaults": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dev": true, "inBundle": true, "license": "MIT", @@ -14636,8 +14581,6 @@ }, "node_modules/npm/node_modules/delegates": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true, "inBundle": true, "license": "MIT" @@ -14662,16 +14605,12 @@ }, "node_modules/npm/node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "inBundle": true, "license": "MIT" @@ -14712,8 +14651,6 @@ }, "node_modules/npm/node_modules/events": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "inBundle": true, "license": "MIT", @@ -14738,8 +14675,6 @@ }, "node_modules/npm/node_modules/foreground-child": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, "inBundle": true, "license": "ISC", @@ -14768,16 +14703,12 @@ }, "node_modules/npm/node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/function-bind": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true, "inBundle": true, "license": "MIT" @@ -14831,8 +14762,6 @@ }, "node_modules/npm/node_modules/has": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "inBundle": true, "license": "MIT", @@ -14845,8 +14774,6 @@ }, "node_modules/npm/node_modules/has-unicode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "dev": true, "inBundle": true, "license": "ISC" @@ -14885,8 +14812,6 @@ }, "node_modules/npm/node_modules/https-proxy-agent": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "inBundle": true, "license": "MIT", @@ -14954,8 +14879,6 @@ }, "node_modules/npm/node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "inBundle": true, "license": "MIT", @@ -14965,8 +14888,6 @@ }, "node_modules/npm/node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "inBundle": true, "license": "MIT", @@ -14976,8 +14897,6 @@ }, "node_modules/npm/node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "inBundle": true, "license": "ISC", @@ -14988,8 +14907,6 @@ }, "node_modules/npm/node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "inBundle": true, "license": "ISC" @@ -15023,8 +14940,6 @@ }, "node_modules/npm/node_modules/ip": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", "dev": true, "inBundle": true, "license": "MIT" @@ -15064,8 +14979,6 @@ }, "node_modules/npm/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "inBundle": true, "license": "MIT", @@ -15081,8 +14994,6 @@ }, "node_modules/npm/node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "inBundle": true, "license": "ISC" @@ -15349,8 +15260,6 @@ }, "node_modules/npm/node_modules/minimatch": { "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "inBundle": true, "license": "ISC", @@ -15364,6 +15273,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/npm/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/npm/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -15389,8 +15307,6 @@ }, "node_modules/npm/node_modules/minipass-collect/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15432,8 +15348,6 @@ }, "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15456,8 +15370,6 @@ }, "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15482,8 +15394,6 @@ }, "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15508,8 +15418,6 @@ }, "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15522,8 +15430,6 @@ }, "node_modules/npm/node_modules/minizlib": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "inBundle": true, "license": "MIT", @@ -15537,8 +15443,6 @@ }, "node_modules/npm/node_modules/minizlib/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15551,8 +15455,6 @@ }, "node_modules/npm/node_modules/mkdirp": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, "inBundle": true, "license": "MIT", @@ -15633,8 +15535,6 @@ }, "node_modules/npm/node_modules/node-gyp/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "inBundle": true, "license": "MIT", @@ -15664,8 +15564,6 @@ }, "node_modules/npm/node_modules/node-gyp/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "inBundle": true, "license": "ISC", @@ -15686,8 +15584,6 @@ }, "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "inBundle": true, "license": "ISC", @@ -15730,8 +15626,6 @@ }, "node_modules/npm/node_modules/node-gyp/node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "inBundle": true, "license": "MIT", @@ -15746,8 +15640,6 @@ }, "node_modules/npm/node_modules/node-gyp/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "inBundle": true, "license": "ISC" @@ -15938,8 +15830,6 @@ }, "node_modules/npm/node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "inBundle": true, "license": "ISC", @@ -15949,8 +15839,6 @@ }, "node_modules/npm/node_modules/p-map": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -16012,8 +15900,6 @@ }, "node_modules/npm/node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "inBundle": true, "license": "MIT", @@ -16023,8 +15909,6 @@ }, "node_modules/npm/node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "inBundle": true, "license": "MIT", @@ -16081,8 +15965,6 @@ }, "node_modules/npm/node_modules/process": { "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, "inBundle": true, "license": "MIT", @@ -16222,8 +16104,6 @@ }, "node_modules/npm/node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "inBundle": true, "license": "ISC", @@ -16239,8 +16119,6 @@ }, "node_modules/npm/node_modules/rimraf/node_modules/brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "inBundle": true, "license": "MIT", @@ -16251,8 +16129,6 @@ }, "node_modules/npm/node_modules/rimraf/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "inBundle": true, "license": "ISC", @@ -16273,8 +16149,6 @@ }, "node_modules/npm/node_modules/rimraf/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "inBundle": true, "license": "ISC", @@ -16287,8 +16161,6 @@ }, "node_modules/npm/node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -16316,8 +16188,6 @@ }, "node_modules/npm/node_modules/semver": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "inBundle": true, "license": "ISC", @@ -16333,8 +16203,6 @@ }, "node_modules/npm/node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "inBundle": true, "license": "ISC", @@ -16347,16 +16215,12 @@ }, "node_modules/npm/node_modules/set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true, "inBundle": true, "license": "ISC" }, "node_modules/npm/node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "inBundle": true, "license": "MIT", @@ -16369,8 +16233,6 @@ }, "node_modules/npm/node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "inBundle": true, "license": "MIT", @@ -16380,8 +16242,6 @@ }, "node_modules/npm/node_modules/signal-exit": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", "dev": true, "inBundle": true, "license": "ISC", @@ -16449,8 +16309,6 @@ }, "node_modules/npm/node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -16461,16 +16319,12 @@ }, "node_modules/npm/node_modules/spdx-exceptions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true, "inBundle": true, "license": "CC-BY-3.0" }, "node_modules/npm/node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "inBundle": true, "license": "MIT", @@ -16481,8 +16335,6 @@ }, "node_modules/npm/node_modules/spdx-license-ids": { "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true, "inBundle": true, "license": "CC0-1.0" @@ -16501,8 +16353,6 @@ }, "node_modules/npm/node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "inBundle": true, "license": "MIT", @@ -16512,8 +16362,6 @@ }, "node_modules/npm/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "inBundle": true, "license": "MIT", @@ -16529,8 +16377,6 @@ "node_modules/npm/node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "inBundle": true, "license": "MIT", @@ -16545,8 +16391,6 @@ }, "node_modules/npm/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "inBundle": true, "license": "MIT", @@ -16560,8 +16404,6 @@ "node_modules/npm/node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "inBundle": true, "license": "MIT", @@ -16605,8 +16447,6 @@ }, "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, "inBundle": true, "license": "ISC", @@ -16619,8 +16459,6 @@ }, "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "inBundle": true, "license": "ISC", @@ -16800,8 +16638,6 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, "inBundle": true, "license": "MIT", @@ -16814,8 +16650,6 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "inBundle": true, "license": "MIT", @@ -16828,16 +16662,12 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "inBundle": true, "license": "MIT", @@ -16855,8 +16685,6 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "inBundle": true, "license": "MIT", @@ -19114,12 +18942,12 @@ "integrity": "sha512-PdjHqho8+kI7AIC3DlLgD99H8zzHphzpIhyv2skVtWaSJGK819+ZqWMC3mHEtSjlcFoYaLXliNt8sb6Taa2Mpg==" }, "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "dev": true, "dependencies": { - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, diff --git a/src/CAREUI/misc/PaginatedList.tsx b/src/CAREUI/misc/PaginatedList.tsx new file mode 100644 index 00000000000..28ee17650ad --- /dev/null +++ b/src/CAREUI/misc/PaginatedList.tsx @@ -0,0 +1,167 @@ +import { createContext, useContext, useState } from "react"; +import { PaginatedResponse, QueryRoute } from "../../Utils/request/types"; +import useQuery, { QueryOptions } from "../../Utils/request/useQuery"; +import ButtonV2, { + CommonButtonProps, +} from "../../Components/Common/components/ButtonV2"; +import CareIcon from "../icons/CareIcon"; +import { classNames } from "../../Utils/utils"; +import Pagination from "../../Components/Common/Pagination"; + +const DEFAULT_PER_PAGE_LIMIT = 14; + +interface PaginatedListContext + extends ReturnType>> { + items: TItem[]; + perPage: number; + currentPage: number; + setPage: (page: number) => void; +} + +const context = createContext | null>(null); + +function useContextualized() { + const ctx = useContext(context); + + if (ctx === null) { + throw new Error("PaginatedList must be used within a PaginatedList"); + } + + return ctx as PaginatedListContext; +} + +interface Props extends QueryOptions { + route: QueryRoute>; + perPage?: number; + children: (ctx: PaginatedListContext) => JSX.Element | JSX.Element[]; +} + +export default function PaginatedList({ + children, + route, + perPage = DEFAULT_PER_PAGE_LIMIT, + ...queryOptions +}: Props) { + const query = useQuery(route, { + ...queryOptions, + query: { ...queryOptions.query, limit: perPage }, + }); + const [currentPage, setPage] = useState(1); + + const items = query.data?.results ?? []; + + return ( + + + {(ctx) => children(ctx as PaginatedListContext)} + + + ); +} + +interface WhenEmptyProps { + className?: string; + children: JSX.Element | JSX.Element[]; +} + +const WhenEmpty = (props: WhenEmptyProps) => { + const { items, loading } = useContextualized(); + + if (loading || items.length > 0) { + return null; + } + + return
{props.children}
; +}; + +PaginatedList.WhenEmpty = WhenEmpty; + +const WhenLoading = (props: WhenEmptyProps) => { + const { loading } = useContextualized(); + + if (!loading) { + return null; + } + + return
{props.children}
; +}; + +PaginatedList.WhenLoading = WhenLoading; + +const Refresh = ({ label = "Refresh", ...props }: CommonButtonProps) => { + const { loading, refetch } = useContextualized(); + + return ( + refetch()} + disabled={loading} + > + + {label} + + ); +}; + +PaginatedList.Refresh = Refresh; + +interface ItemsProps { + className?: string; + children: (item: TItem) => JSX.Element | JSX.Element[]; + shimmer?: JSX.Element; + shimmerCount?: number; +} + +const Items = (props: ItemsProps) => { + const { loading, items } = useContextualized(); + + return ( +
    + {loading && props.shimmer + ? Array.from({ length: props.shimmerCount ?? 8 }).map((_, i) => ( +
  • + {props.shimmer} +
  • + )) + : items.map((item, index) => ( +
  • + {props.children(item)} +
  • + ))} +
+ ); +}; + +PaginatedList.Items = Items; + +interface PaginatorProps { + className?: string; + hideIfSinglePage?: boolean; +} + +const Paginator = ({ className, hideIfSinglePage }: PaginatorProps) => { + const { data, perPage, currentPage, setPage } = useContextualized(); + + if (hideIfSinglePage && (data?.count ?? 0) <= perPage) { + return null; + } + + return ( + + ); +}; + +PaginatedList.Paginator = Paginator; diff --git a/src/Common/hooks/useAppHistory.ts b/src/Common/hooks/useAppHistory.ts index 70ad92692de..f4ee2f11f78 100644 --- a/src/Common/hooks/useAppHistory.ts +++ b/src/Common/hooks/useAppHistory.ts @@ -10,12 +10,13 @@ export default function useAppHistory() { const resetHistory = useContext(ResetHistoryContext); const goBack = (fallbackUrl?: string) => { - if (fallbackUrl) - // use provided fallback url if provided. - return navigate(fallbackUrl); if (history.length > 1) // Otherwise, navigate to history present in the app navigation history stack. return navigate(history[1]); + + if (fallbackUrl) + // use provided fallback url if provided. + return navigate(fallbackUrl); // Otherwise, fallback to browser's go back behaviour. window.history.back(); }; diff --git a/src/Common/hooks/useHLSPlayer.ts b/src/Common/hooks/useHLSPlayer.ts index 7cb5302e76f..32a52043fe7 100644 --- a/src/Common/hooks/useHLSPlayer.ts +++ b/src/Common/hooks/useHLSPlayer.ts @@ -4,7 +4,7 @@ import { IOptions } from "./useMSEplayer"; export const useHLSPLayer = (ref: ReactPlayer | null) => { const startStream = ({ onSuccess, onError }: IOptions = {}) => { try { - ref && ref.forceUpdate(); + ref?.setState({ url: ref?.props.url + "&t=" + Date.now() }); onSuccess && onSuccess(undefined); } catch (err) { onError && onError(err); diff --git a/src/Components/Assets/AssetManage.tsx b/src/Components/Assets/AssetManage.tsx index 25f879ddd09..1df9fbde9a6 100644 --- a/src/Components/Assets/AssetManage.tsx +++ b/src/Components/Assets/AssetManage.tsx @@ -360,6 +360,7 @@ const AssetManage = (props: AssetManageProps) => { {asset?.name} { const [refreshPresetsHash, setRefreshPresetsHash] = useState( Number(new Date()) ); + const [refreshHash, setRefreshHash] = useState(Number(new Date())); const dispatch = useDispatch(); useEffect(() => { @@ -89,7 +90,7 @@ const ONVIFCamera = (props: ONVIFCameraProps) => { Notification.Success({ msg: "Asset Configured Successfully", }); - window.location.reload(); + setRefreshHash(Number(new Date())); } else { Notification.Error({ msg: "Something went wrong..!", @@ -200,6 +201,7 @@ const ONVIFCamera = (props: ONVIFCameraProps) => { {assetType === "ONVIF" ? ( { options: { icon: , disabled: totalCount === 0 || !authorizedForImportExport, + id: "export-json-option", }, }, { @@ -353,6 +354,7 @@ const AssetsList = () => { options: { icon: , disabled: totalCount === 0 || !authorizedForImportExport, + id: "export-csv-option", }, }, ]} diff --git a/src/Components/Common/components/ButtonV2.tsx b/src/Components/Common/components/ButtonV2.tsx index 4c09445217d..2f3d3002451 100644 --- a/src/Components/Common/components/ButtonV2.tsx +++ b/src/Components/Common/components/ButtonV2.tsx @@ -161,7 +161,7 @@ export default ButtonV2; // Common buttons -type CommonButtonProps = ButtonProps & { label?: string }; +export type CommonButtonProps = ButtonProps & { label?: string }; export const Submit = ({ label = "Submit", ...props }: CommonButtonProps) => { const { t } = useTranslation(); diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx index 07fd6f31739..84b3e62ef9e 100644 --- a/src/Components/Facility/Consultations/Feed.tsx +++ b/src/Components/Facility/Consultations/Feed.tsx @@ -56,6 +56,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { const [precision, setPrecision] = useState(1); const [cameraState, setCameraState] = useState(null); const [isFullscreen, setFullscreen] = useFullscreen(); + const [videoStartTime, setVideoStartTime] = useState(null); const authUser = useAuthUser(); useEffect(() => { @@ -197,6 +198,16 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { dispatch, }); + const calculateVideoLiveDelay = () => { + const video = liveFeedPlayerRef.current as HTMLVideoElement; + if (!video || !videoStartTime) return 0; + + const timeDifference = + (new Date().getTime() - videoStartTime.getTime()) / 1000; + + return timeDifference - video.currentTime; + }; + const getBedPresets = async (asset: any) => { if (asset.id && bed) { const bedAssets = await dispatch(listAssetBeds({ asset: asset.id, bed })); @@ -240,7 +251,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { }, []); useEffect(() => { - if (streamStatus === StreamStatus.Playing) { + if (!currentPreset && streamStatus === StreamStatus.Playing) { setLoading(CAMERA_STATES.MOVING.GENERIC); const preset = bedPresets?.find( @@ -296,6 +307,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { }, reset: () => { setStreamStatus(StreamStatus.Loading); + setVideoStartTime(null); startStream({ onSuccess: () => setStreamStatus(StreamStatus.Playing), onError: () => setStreamStatus(StreamStatus.Offline), @@ -437,10 +449,16 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { playsinline={true} playing={true} muted={true} + onPlay={() => { + setVideoStartTime(() => new Date()); + }} width="100%" height="100%" onBuffer={() => { - setStreamStatus(StreamStatus.Loading); + const delay = calculateVideoLiveDelay(); + if (delay > 5) { + setStreamStatus(StreamStatus.Loading); + } }} onError={(e: any, _: any, hlsInstance: any) => { if (e === "hlsError") { @@ -459,6 +477,15 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { muted playsInline className="max-h-full max-w-full" + onPlay={() => { + setVideoStartTime(() => new Date()); + }} + onWaiting={() => { + const delay = calculateVideoLiveDelay(); + if (delay > 5) { + setStreamStatus(StreamStatus.Loading); + } + }} ref={liveFeedPlayerRef as any} /> )} @@ -506,7 +533,6 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => {
{["fullScreen", "reset", "updatePreset", "zoomIn", "zoomOut"].map( (button, index) => { - if (isIOS && button === "reset") return null; const option = cameraPTZ.find( (option) => option.action === button ); @@ -531,6 +557,13 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { clickAction={() => cameraPTZ[4].callback()} />
+ {streamStatus === StreamStatus.Playing && + calculateVideoLiveDelay() > 3 && ( +
+ + Slow Network Detected +
+ )}
{[ false, diff --git a/src/Components/Facility/LocationManagement.tsx b/src/Components/Facility/LocationManagement.tsx index 01e0c246ecb..38dcfc1f389 100644 --- a/src/Components/Facility/LocationManagement.tsx +++ b/src/Components/Facility/LocationManagement.tsx @@ -1,176 +1,99 @@ -import { useCallback, useState, ReactElement, lazy } from "react"; - -import { useDispatch } from "react-redux"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { listFacilityAssetLocation, getAnyFacility } from "../../Redux/actions"; -import Pagination from "../Common/Pagination"; -import { LocationModel } from "./models"; +import { lazy } from "react"; import ButtonV2 from "../Common/components/ButtonV2"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; import CareIcon from "../../CAREUI/icons/CareIcon"; import Page from "../Common/components/Page"; -const Loading = lazy(() => import("../Common/Loading")); +import routes from "../../Redux/api"; +import PaginatedList from "../../CAREUI/misc/PaginatedList"; +import { LocationModel } from "./models"; -interface LocationManagementProps { - facilityId: string; -} +const Loading = lazy(() => import("../Common/Loading")); -interface LocationRowProps { - id: string; +interface Props { facilityId: string; - name: string; - description: string; } -const LocationRow = (props: LocationRowProps) => { - const { id, facilityId, name, description } = props; - +export default function LocationManagement({ facilityId }: Props) { return ( -
-
-
-

{name}

-

{description}

-
-
-
- - - Edit - - ( + + + Add New Location + + } > - - Manage Beds - -
-
- ); -}; - -export const LocationManagement = (props: LocationManagementProps) => { - const { facilityId } = props; - const dispatchAction: any = useDispatch(); - const [isLoading, setIsLoading] = useState(false); - let location: ReactElement | null = null; - let locationsList: ReactElement[] | ReactElement = []; - const [locations, setLocations] = useState([]); - const [offset, setOffset] = useState(0); - const [currentPage, setCurrentPage] = useState(1); - const [totalCount, setTotalCount] = useState(0); - const [facilityName, setFacilityName] = useState(""); - const limit = 14; - - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - const facility = await dispatchAction(getAnyFacility(facilityId)); - - setFacilityName(facility?.data?.name || ""); - - const res = await dispatchAction( - listFacilityAssetLocation( - { limit, offset }, - { facility_external_id: facilityId } - ) - ); - if (!status.aborted) { - if (res?.data) { - setLocations(res.data.results); - setTotalCount(res.data.count); - } - setIsLoading(false); - } - }, - [dispatchAction, offset, facilityId] - ); - - useAbortableEffect( - (status: statusType) => { - fetchData(status); - }, - [fetchData] - ); +
+ + + Add New Location + +
+ + No locations available + - const handlePagination = (page: number, limit: number) => { - const offset = (page - 1) * limit; - setCurrentPage(page); - setOffset(offset); - }; + + + - if (locations?.length) { - locationsList = locations.map((locationItem: LocationModel) => ( - - )); - } else if (locations && locations.length === 0) { - locationsList = ( -

- No locations available -

- ); - } + className="my-8 flex grow flex-col gap-3 lg:mx-8"> + {(item) => } + - if (locations) { - location = ( - <> -
- {locationsList} -
- {totalCount > limit && ( -
- +
+
- )} - - ); - } - - if (isLoading || !locations) { - return ; - } + + )} + + ); +} - return ( - -
-
- - - Add New Location - -
- {location} +const Location = ({ name, description, id }: LocationModel) => ( +
+
+
+

{name}

+

{description}

- - ); -}; +
+ +
+ + + Edit + + + + Manage Beds + +
+
+); diff --git a/src/Components/VitalsMonitor/HL7DeviceClient.ts b/src/Components/VitalsMonitor/HL7DeviceClient.ts index 72f6406193a..7e13622b31f 100644 --- a/src/Components/VitalsMonitor/HL7DeviceClient.ts +++ b/src/Components/VitalsMonitor/HL7DeviceClient.ts @@ -1,11 +1,31 @@ import { EventEmitter } from "events"; import { VitalsDataBase, VitalsValueBase, VitalsWaveformBase } from "./types"; -const WAVEFORM_KEY_MAP: Record = { - II: "ecg-waveform", - Pleth: "pleth-waveform", - Respiration: "spo2-waveform", -}; +const ECG_WAVENAME_KEYS = [ + "I", + "II", + "III", + "aVR", + "aVL", + "aVF", + "V1", + "V2", + "V3", + "V4", + "V5", + "V6", +] as const; + +const WAVEFORM_KEY_MAP: Record = + { + Pleth: "pleth-waveform", + Respiration: "spo2-waveform", + + // Maps each ECG wave name to the event "ecg-waveform" + ...(Object.fromEntries( + ECG_WAVENAME_KEYS.map((key) => [key, "ecg-waveform"]) + ) as Record), + }; /** * Provides the API for connecting to the Vitals Monitor WebSocket and emitting @@ -74,8 +94,10 @@ export interface HL7VitalsValueData extends VitalsDataBase, VitalsValueBase { | "body-temperature2"; } +type EcgWaveName = (typeof ECG_WAVENAME_KEYS)[number]; + export interface HL7VitalsWaveformData extends VitalsWaveformBase { - "wave-name": "II" | "Pleth" | "Respiration"; + "wave-name": EcgWaveName | "Pleth" | "Respiration"; } export interface HL7VitalsBloodPressureData extends VitalsDataBase { diff --git a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx index a340c21e28b..c0b089df8b0 100644 --- a/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx +++ b/src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx @@ -88,7 +88,7 @@ export default function HL7PatientVitalsMonitor(props: IVitalsComponentProps) { {/* Pulse Rate */} ❤️ diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 01a91d4ce3a..b98a099f439 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -1,14 +1,15 @@ -interface Route { - path: string; - method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; - noAuth?: boolean; +import { LocationModel } from "../Components/Facility/models"; +import { PaginatedResponse } from "../Utils/request/types"; + +/** + * A fake function that returns an empty object casted to type T + * @returns Empty object as type T + */ +function Res(): T { + return {} as T; } -interface Routes { - [name: string]: Route; -} - -const routes: Routes = { +const routes = { config: { path: import.meta.env.REACT_APP_CONFIG ?? "/config.json", method: "GET", @@ -187,6 +188,7 @@ const routes: Routes = { listFacilityAssetLocation: { path: "/api/v1/facility/{facility_external_id}/asset_location/", method: "GET", + TRes: Res>(), }, createFacilityAssetLocation: { path: "/api/v1/facility/{facility_external_id}/asset_location/", @@ -1065,6 +1067,6 @@ const routes: Routes = { path: "/api/v1/hcx/make_claim/", method: "POST", }, -}; +} as const; export default routes; diff --git a/src/Router/AppRouter.tsx b/src/Router/AppRouter.tsx index 24b24cfdf09..f1449f13bc2 100644 --- a/src/Router/AppRouter.tsx +++ b/src/Router/AppRouter.tsx @@ -49,7 +49,7 @@ import ShowPushNotification from "../Components/Notifications/ShowPushNotificati import { NoticeBoard } from "../Components/Notifications/NoticeBoard"; import { AddLocationForm } from "../Components/Facility/AddLocationForm"; import { AddBedForm } from "../Components/Facility/AddBedForm"; -import { LocationManagement } from "../Components/Facility/LocationManagement"; +import LocationManagement from "../Components/Facility/LocationManagement"; import { BedManagement } from "../Components/Facility/BedManagement"; import AssetsList from "../Components/Assets/AssetsList"; import AssetManage from "../Components/Assets/AssetManage"; diff --git a/src/Utils/request/request.ts b/src/Utils/request/request.ts new file mode 100644 index 00000000000..2dc938fa6f1 --- /dev/null +++ b/src/Utils/request/request.ts @@ -0,0 +1,27 @@ +import { RequestOptions, Route } from "./types"; +import { makeHeaders, makeUrl } from "./utils"; + +interface Options extends RequestOptions { + controller?: AbortController; +} + +export default async function request( + { path, method, noAuth }: Route, + { query, body, pathParams, controller }: Options = {} +) { + const signal = controller?.signal; + + const headers = makeHeaders(noAuth ?? false); + const url = makeUrl(path, query, pathParams); + + const options: RequestInit = { headers, method, signal }; + + if (body) { + options.body = JSON.stringify(body); + } + + const res = await fetch(url, options); + const data: TData = await res.json(); + + return { res, data }; +} diff --git a/src/Utils/request/types.ts b/src/Utils/request/types.ts new file mode 100644 index 00000000000..e7f0f9544a3 --- /dev/null +++ b/src/Utils/request/types.ts @@ -0,0 +1,32 @@ +type QueryParamValue = string | number | boolean | null | undefined; + +export type QueryParams = Record; + +interface RouteBase { + path: string; + TRes: TData; + noAuth?: boolean; +} + +export interface QueryRoute extends RouteBase { + method?: "GET"; +} + +export interface MutationRoute extends RouteBase { + method: "POST" | "PUT" | "PATCH" | "DELETE"; +} + +export type Route = QueryRoute | MutationRoute; + +export interface RequestOptions { + query?: QueryParams; + body?: object; + pathParams?: Record; +} + +export interface PaginatedResponse { + count: number; + next: string | null; + previous: string | null; + results: TItem[]; +} diff --git a/src/Utils/request/useQuery.ts b/src/Utils/request/useQuery.ts new file mode 100644 index 00000000000..e459a579e3e --- /dev/null +++ b/src/Utils/request/useQuery.ts @@ -0,0 +1,70 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { QueryRoute, RequestOptions } from "./types"; +import request from "./request"; +import { mergeRequestOptions } from "./utils"; + +export interface QueryOptions extends RequestOptions { + prefetch?: boolean; + refetchOnWindowFocus?: boolean; +} + +export default function useQuery( + route: QueryRoute, + options?: QueryOptions +) { + const [res, setRes] = useState(); + const [data, setData] = useState(); + const [error, setError] = useState(); + const [loading, setLoading] = useState(false); + + const controllerRef = useRef(); + + const runQuery = useCallback( + async (overrides?: QueryOptions) => { + controllerRef.current?.abort(); + + const controller = new AbortController(); + controllerRef.current = controller; + + const resolvedOptions = + options && overrides + ? mergeRequestOptions(options, overrides) + : options; + + setLoading(true); + + try { + const { res, data } = await request(route, resolvedOptions); + + setRes(res); + setData(res.ok ? data : undefined); + setError(res.ok ? undefined : data); + } catch (error) { + console.error(error); + setData(undefined); + setError(error); + } finally { + setLoading(false); + } + }, + [route, JSON.stringify(options)] + ); + + useEffect(() => { + if (options?.prefetch ?? true) { + runQuery(); + } + }, [runQuery, options?.prefetch]); + + useEffect(() => { + if (options?.refetchOnWindowFocus) { + const onFocus = () => runQuery(); + + window.addEventListener("focus", onFocus); + + return () => window.removeEventListener("focus", onFocus); + } + }, [runQuery, options?.refetchOnWindowFocus]); + + return { res, data, error, loading, refetch: runQuery }; +} diff --git a/src/Utils/request/utils.ts b/src/Utils/request/utils.ts new file mode 100644 index 00000000000..21236a8145f --- /dev/null +++ b/src/Utils/request/utils.ts @@ -0,0 +1,81 @@ +import { LocalStorageKeys } from "../../Common/constants"; +import { QueryParams, RequestOptions } from "./types"; + +export function makeUrl( + path: string, + query?: QueryParams, + pathParams?: Record +) { + if (pathParams) { + path = Object.entries(pathParams).reduce( + (acc, [key, value]) => acc.replace(`{${key}}`, value), + path + ); + } + + ensurePathNotMissingReplacements(path); + + if (query) { + path += `?${makeQueryParams(query)}`; + } + + return path; +} + +const makeQueryParams = (query: QueryParams) => { + const qParams = new URLSearchParams(); + + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined) { + qParams.set(key, `${value}`); + } + }); + + return qParams.toString(); +}; + +const ensurePathNotMissingReplacements = (path: string) => { + const missingParams = path.match(/\{.*\}/g); + + if (missingParams) { + throw new Error(`Missing path params: ${missingParams.join(", ")}`); + } +}; + +export function makeHeaders(noAuth: boolean) { + const headers = new Headers({ + "Content-Type": "application/json", + Accept: "application/json", + }); + + if (!noAuth) { + const token = getAuthorizationHeader(); + + if (token) { + headers.append("Authorization", token); + } + } + + return headers; +} + +export function getAuthorizationHeader() { + const bearerToken = localStorage.getItem(LocalStorageKeys.accessToken); + + if (bearerToken) { + return `Bearer ${bearerToken}`; + } + + return null; +} + +export function mergeRequestOptions( + options: RequestOptions, + overrides: RequestOptions +): RequestOptions { + return { + query: { ...options.query, ...overrides.query }, + body: { ...options.body, ...overrides.body }, + pathParams: { ...options.pathParams, ...overrides.pathParams }, + }; +}