Merge branch 'release/2025-03-28' into feature/IO-3176-IntelliPay-Payment-Mapping
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
This commit is contained in:
@@ -9,6 +9,6 @@ VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
|||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_INSTANCE=IMEX
|
VITE_APP_INSTANCE=IMEX
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
|||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
VITE_APP_AXIOS_BASE_API_URL=/api/
|
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_COUNTRY=USA
|
VITE_APP_COUNTRY=USA
|
||||||
VITE_APP_INSTANCE=ROME
|
VITE_APP_INSTANCE=ROME
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
|||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BN2GcDPjipR5MTEosO5dT4CfQ3cmrdBIsI4juoOQrRijn_5aRiHlwj1mlq0W145mOusx6xynEKl_tvYJhpCc9lo'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BN2GcDPjipR5MTEosO5dT4CfQ3cmrdBIsI4juoOQrRijn_5aRiHlwj1mlq0W145mOusx6xynEKl_tvYJhpCc9lo'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
VITE_APP_AXIOS_BASE_API_URL=https://api.test.imex.online/
|
VITE_APP_AXIOS_BASE_API_URL=https://api.test.imex.online/
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
|
||||||
VITE_APP_IS_TEST=true
|
VITE_APP_IS_TEST=true
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_INSTANCE=IMEX
|
VITE_APP_INSTANCE=IMEX
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
const { defineConfig } = require("cypress");
|
|
||||||
|
|
||||||
module.exports = defineConfig({
|
|
||||||
experimentalStudio: true,
|
|
||||||
env: {
|
|
||||||
FIREBASE_USERNAME: "cypress@imex.test",
|
|
||||||
FIREBASE_PASSWORD: "cypress"
|
|
||||||
},
|
|
||||||
e2e: {
|
|
||||||
// We've imported your old cypress plugins here.
|
|
||||||
// You may want to clean this up later by importing these.
|
|
||||||
setupNodeEvents(on, config) {
|
|
||||||
return require("./cypress/plugins/index.js")(on, config);
|
|
||||||
},
|
|
||||||
baseUrl: "https://localhost:3000"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/// <reference types="Cypress" />
|
|
||||||
const { FIREBASE_USERNAME, FIREBASE_PASSWORcD } = Cypress.env();
|
|
||||||
describe("Renders the General Page", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("/");
|
|
||||||
});
|
|
||||||
it("Renders Correctly", () => {});
|
|
||||||
it("Has the Slogan", () => {
|
|
||||||
cy.findByText("A whole x22new kind of shop management system.").should("exist");
|
|
||||||
/* ==== Generated with Cypress Studio ==== */
|
|
||||||
cy.get(".ant-menu-item-active > .ant-menu-title-content > .header0-item-block").click();
|
|
||||||
cy.get("#email").clear();
|
|
||||||
cy.get("#email").type("patrick@imex.dev");
|
|
||||||
cy.get("#password").clear();
|
|
||||||
cy.get("#password").type("patrick123{enter}");
|
|
||||||
cy.get(".ant-form > .ant-btn").click();
|
|
||||||
/* ==== End Cypress Studio ==== */
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
// Welcome to Cypress!
|
|
||||||
//
|
|
||||||
// This spec file contains a variety of sample tests
|
|
||||||
// for a todo list app that are designed to demonstrate
|
|
||||||
// the power of writing tests in Cypress.
|
|
||||||
//
|
|
||||||
// To learn more about how Cypress works and
|
|
||||||
// what makes it such an awesome testing tool,
|
|
||||||
// please read our getting started guide:
|
|
||||||
// https://on.cypress.io/introduction-to-cypress
|
|
||||||
|
|
||||||
describe("example to-do app", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Cypress starts out with a blank slate for each test
|
|
||||||
// so we must tell it to visit our website with the `cy.visit()` command.
|
|
||||||
// Since we want to visit the same URL at the start of all our tests,
|
|
||||||
// we include it in our beforeEach function so that it runs before each test
|
|
||||||
cy.visit("https://example.cypress.io/todo");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays two todo items by default", () => {
|
|
||||||
// We use the `cy.get()` command to get all elements that match the selector.
|
|
||||||
// Then, we use `should` to assert that there are two matched items,
|
|
||||||
// which are the two default items.
|
|
||||||
cy.get(".todo-list li").should("have.length", 2);
|
|
||||||
|
|
||||||
// We can go even further and check that the default todos each contain
|
|
||||||
// the correct text. We use the `first` and `last` functions
|
|
||||||
// to get just the first and last matched elements individually,
|
|
||||||
// and then perform an assertion with `should`.
|
|
||||||
cy.get(".todo-list li").first().should("have.text", "Pay electric bill");
|
|
||||||
cy.get(".todo-list li").last().should("have.text", "Walk the dog");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can add new todo items", () => {
|
|
||||||
// We'll store our item text in a variable so we can reuse it
|
|
||||||
const newItem = "Feed the cat";
|
|
||||||
|
|
||||||
// Let's get the input element and use the `type` command to
|
|
||||||
// input our new list item. After typing the content of our item,
|
|
||||||
// we need to type the enter key as well in order to submit the input.
|
|
||||||
// This input has a data-test attribute so we'll use that to select the
|
|
||||||
// element in accordance with best practices:
|
|
||||||
// https://on.cypress.io/selecting-elements
|
|
||||||
cy.get("[data-test=new-todo]").type(`${newItem}{enter}`);
|
|
||||||
|
|
||||||
// Now that we've typed our new item, let's check that it actually was added to the list.
|
|
||||||
// Since it's the newest item, it should exist as the last element in the list.
|
|
||||||
// In addition, with the two default items, we should have a total of 3 elements in the list.
|
|
||||||
// Since assertions yield the element that was asserted on,
|
|
||||||
// we can chain both of these assertions together into a single statement.
|
|
||||||
cy.get(".todo-list li").should("have.length", 3).last().should("have.text", newItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can check off an item as completed", () => {
|
|
||||||
// In addition to using the `get` command to get an element by selector,
|
|
||||||
// we can also use the `contains` command to get an element by its contents.
|
|
||||||
// However, this will yield the <label>, which is lowest-level element that contains the text.
|
|
||||||
// In order to check the item, we'll find the <input> element for this <label>
|
|
||||||
// by traversing up the dom to the parent element. From there, we can `find`
|
|
||||||
// the child checkbox <input> element and use the `check` command to check it.
|
|
||||||
cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check();
|
|
||||||
|
|
||||||
// Now that we've checked the button, we can go ahead and make sure
|
|
||||||
// that the list element is now marked as completed.
|
|
||||||
// Again we'll use `contains` to find the <label> element and then use the `parents` command
|
|
||||||
// to traverse multiple levels up the dom until we find the corresponding <li> element.
|
|
||||||
// Once we get that element, we can assert that it has the completed class.
|
|
||||||
cy.contains("Pay electric bill").parents("li").should("have.class", "completed");
|
|
||||||
});
|
|
||||||
|
|
||||||
context("with a checked task", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// We'll take the command we used above to check off an element
|
|
||||||
// Since we want to perform multiple tests that start with checking
|
|
||||||
// one element, we put it in the beforeEach hook
|
|
||||||
// so that it runs at the start of every test.
|
|
||||||
cy.contains("Pay electric bill").parent().find("input[type=checkbox]").check();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can filter for uncompleted tasks", () => {
|
|
||||||
// We'll click on the "active" button in order to
|
|
||||||
// display only incomplete items
|
|
||||||
cy.contains("Active").click();
|
|
||||||
|
|
||||||
// After filtering, we can assert that there is only the one
|
|
||||||
// incomplete item in the list.
|
|
||||||
cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Walk the dog");
|
|
||||||
|
|
||||||
// For good measure, let's also assert that the task we checked off
|
|
||||||
// does not exist on the page.
|
|
||||||
cy.contains("Pay electric bill").should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can filter for completed tasks", () => {
|
|
||||||
// We can perform similar steps as the test above to ensure
|
|
||||||
// that only completed tasks are shown
|
|
||||||
cy.contains("Completed").click();
|
|
||||||
|
|
||||||
cy.get(".todo-list li").should("have.length", 1).first().should("have.text", "Pay electric bill");
|
|
||||||
|
|
||||||
cy.contains("Walk the dog").should("not.exist");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can delete all completed tasks", () => {
|
|
||||||
// First, let's click the "Clear completed" button
|
|
||||||
// `contains` is actually serving two purposes here.
|
|
||||||
// First, it's ensuring that the button exists within the dom.
|
|
||||||
// This button only appears when at least one task is checked
|
|
||||||
// so this command is implicitly verifying that it does exist.
|
|
||||||
// Second, it selects the button so we can click it.
|
|
||||||
cy.contains("Clear completed").click();
|
|
||||||
|
|
||||||
// Then we can make sure that there is only one element
|
|
||||||
// in the list and our element does not exist
|
|
||||||
cy.get(".todo-list li").should("have.length", 1).should("not.have.text", "Pay electric bill");
|
|
||||||
|
|
||||||
// Finally, make sure that the clear button no longer exists.
|
|
||||||
cy.contains("Clear completed").should("not.exist");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Actions", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/actions");
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://on.cypress.io/interacting-with-elements
|
|
||||||
|
|
||||||
it(".type() - type into a DOM element", () => {
|
|
||||||
// https://on.cypress.io/type
|
|
||||||
cy.get(".action-email")
|
|
||||||
.type("fake@email.com")
|
|
||||||
.should("have.value", "fake@email.com")
|
|
||||||
|
|
||||||
// .type() with special character sequences
|
|
||||||
.type("{leftarrow}{rightarrow}{uparrow}{downarrow}")
|
|
||||||
.type("{del}{selectall}{backspace}")
|
|
||||||
|
|
||||||
// .type() with key modifiers
|
|
||||||
.type("{alt}{option}") //these are equivalent
|
|
||||||
.type("{ctrl}{control}") //these are equivalent
|
|
||||||
.type("{meta}{command}{cmd}") //these are equivalent
|
|
||||||
.type("{shift}")
|
|
||||||
|
|
||||||
// Delay each keypress by 0.1 sec
|
|
||||||
.type("slow.typing@email.com", { delay: 100 })
|
|
||||||
.should("have.value", "slow.typing@email.com");
|
|
||||||
|
|
||||||
cy.get(".action-disabled")
|
|
||||||
// Ignore error checking prior to type
|
|
||||||
// like whether the input is visible or disabled
|
|
||||||
.type("disabled error checking", { force: true })
|
|
||||||
.should("have.value", "disabled error checking");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".focus() - focus on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/focus
|
|
||||||
cy.get(".action-focus").focus().should("have.class", "focus").prev().should("have.attr", "style", "color: orange;");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".blur() - blur off a DOM element", () => {
|
|
||||||
// https://on.cypress.io/blur
|
|
||||||
cy.get(".action-blur")
|
|
||||||
.type("About to blur")
|
|
||||||
.blur()
|
|
||||||
.should("have.class", "error")
|
|
||||||
.prev()
|
|
||||||
.should("have.attr", "style", "color: red;");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".clear() - clears an input or textarea element", () => {
|
|
||||||
// https://on.cypress.io/clear
|
|
||||||
cy.get(".action-clear")
|
|
||||||
.type("Clear this text")
|
|
||||||
.should("have.value", "Clear this text")
|
|
||||||
.clear()
|
|
||||||
.should("have.value", "");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".submit() - submit a form", () => {
|
|
||||||
// https://on.cypress.io/submit
|
|
||||||
cy.get(".action-form").find('[type="text"]').type("HALFOFF");
|
|
||||||
|
|
||||||
cy.get(".action-form").submit().next().should("contain", "Your form has been submitted!");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".click() - click on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/click
|
|
||||||
cy.get(".action-btn").click();
|
|
||||||
|
|
||||||
// You can click on 9 specific positions of an element:
|
|
||||||
// -----------------------------------
|
|
||||||
// | topLeft top topRight |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | left center right |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | bottomLeft bottom bottomRight |
|
|
||||||
// -----------------------------------
|
|
||||||
|
|
||||||
// clicking in the center of the element is the default
|
|
||||||
cy.get("#action-canvas").click();
|
|
||||||
|
|
||||||
cy.get("#action-canvas").click("topLeft");
|
|
||||||
cy.get("#action-canvas").click("top");
|
|
||||||
cy.get("#action-canvas").click("topRight");
|
|
||||||
cy.get("#action-canvas").click("left");
|
|
||||||
cy.get("#action-canvas").click("right");
|
|
||||||
cy.get("#action-canvas").click("bottomLeft");
|
|
||||||
cy.get("#action-canvas").click("bottom");
|
|
||||||
cy.get("#action-canvas").click("bottomRight");
|
|
||||||
|
|
||||||
// .click() accepts an x and y coordinate
|
|
||||||
// that controls where the click occurs :)
|
|
||||||
|
|
||||||
cy.get("#action-canvas")
|
|
||||||
.click(80, 75) // click 80px on x coord and 75px on y coord
|
|
||||||
.click(170, 75)
|
|
||||||
.click(80, 165)
|
|
||||||
.click(100, 185)
|
|
||||||
.click(125, 190)
|
|
||||||
.click(150, 185)
|
|
||||||
.click(170, 165);
|
|
||||||
|
|
||||||
// click multiple elements by passing multiple: true
|
|
||||||
cy.get(".action-labels>.label").click({ multiple: true });
|
|
||||||
|
|
||||||
// Ignore error checking prior to clicking
|
|
||||||
cy.get(".action-opacity>.btn").click({ force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".dblclick() - double click on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/dblclick
|
|
||||||
|
|
||||||
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
|
||||||
// that hides the div and shows an input on double click
|
|
||||||
cy.get(".action-div").dblclick().should("not.be.visible");
|
|
||||||
cy.get(".action-input-hidden").should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".rightclick() - right click on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/rightclick
|
|
||||||
|
|
||||||
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
|
||||||
// that hides the div and shows an input on right click
|
|
||||||
cy.get(".rightclick-action-div").rightclick().should("not.be.visible");
|
|
||||||
cy.get(".rightclick-action-input-hidden").should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".check() - check a checkbox or radio element", () => {
|
|
||||||
// https://on.cypress.io/check
|
|
||||||
|
|
||||||
// By default, .check() will check all
|
|
||||||
// matching checkbox or radio elements in succession, one after another
|
|
||||||
cy.get('.action-checkboxes [type="checkbox"]').not("[disabled]").check().should("be.checked");
|
|
||||||
|
|
||||||
cy.get('.action-radios [type="radio"]').not("[disabled]").check().should("be.checked");
|
|
||||||
|
|
||||||
// .check() accepts a value argument
|
|
||||||
cy.get('.action-radios [type="radio"]').check("radio1").should("be.checked");
|
|
||||||
|
|
||||||
// .check() accepts an array of values
|
|
||||||
cy.get('.action-multiple-checkboxes [type="checkbox"]').check(["checkbox1", "checkbox2"]).should("be.checked");
|
|
||||||
|
|
||||||
// Ignore error checking prior to checking
|
|
||||||
cy.get(".action-checkboxes [disabled]").check({ force: true }).should("be.checked");
|
|
||||||
|
|
||||||
cy.get('.action-radios [type="radio"]').check("radio3", { force: true }).should("be.checked");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".uncheck() - uncheck a checkbox element", () => {
|
|
||||||
// https://on.cypress.io/uncheck
|
|
||||||
|
|
||||||
// By default, .uncheck() will uncheck all matching
|
|
||||||
// checkbox elements in succession, one after another
|
|
||||||
cy.get('.action-check [type="checkbox"]').not("[disabled]").uncheck().should("not.be.checked");
|
|
||||||
|
|
||||||
// .uncheck() accepts a value argument
|
|
||||||
cy.get('.action-check [type="checkbox"]').check("checkbox1").uncheck("checkbox1").should("not.be.checked");
|
|
||||||
|
|
||||||
// .uncheck() accepts an array of values
|
|
||||||
cy.get('.action-check [type="checkbox"]')
|
|
||||||
.check(["checkbox1", "checkbox3"])
|
|
||||||
.uncheck(["checkbox1", "checkbox3"])
|
|
||||||
.should("not.be.checked");
|
|
||||||
|
|
||||||
// Ignore error checking prior to unchecking
|
|
||||||
cy.get(".action-check [disabled]").uncheck({ force: true }).should("not.be.checked");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".select() - select an option in a <select> element", () => {
|
|
||||||
// https://on.cypress.io/select
|
|
||||||
|
|
||||||
// at first, no option should be selected
|
|
||||||
cy.get(".action-select").should("have.value", "--Select a fruit--");
|
|
||||||
|
|
||||||
// Select option(s) with matching text content
|
|
||||||
cy.get(".action-select").select("apples");
|
|
||||||
// confirm the apples were selected
|
|
||||||
// note that each value starts with "fr-" in our HTML
|
|
||||||
cy.get(".action-select").should("have.value", "fr-apples");
|
|
||||||
|
|
||||||
cy.get(".action-select-multiple")
|
|
||||||
.select(["apples", "oranges", "bananas"])
|
|
||||||
// when getting multiple values, invoke "val" method first
|
|
||||||
.invoke("val")
|
|
||||||
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
|
|
||||||
|
|
||||||
// Select option(s) with matching value
|
|
||||||
cy.get(".action-select")
|
|
||||||
.select("fr-bananas")
|
|
||||||
// can attach an assertion right away to the element
|
|
||||||
.should("have.value", "fr-bananas");
|
|
||||||
|
|
||||||
cy.get(".action-select-multiple")
|
|
||||||
.select(["fr-apples", "fr-oranges", "fr-bananas"])
|
|
||||||
.invoke("val")
|
|
||||||
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
|
|
||||||
|
|
||||||
// assert the selected values include oranges
|
|
||||||
cy.get(".action-select-multiple").invoke("val").should("include", "fr-oranges");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".scrollIntoView() - scroll an element into view", () => {
|
|
||||||
// https://on.cypress.io/scrollintoview
|
|
||||||
|
|
||||||
// normally all of these buttons are hidden,
|
|
||||||
// because they're not within
|
|
||||||
// the viewable area of their parent
|
|
||||||
// (we need to scroll to see them)
|
|
||||||
cy.get("#scroll-horizontal button").should("not.be.visible");
|
|
||||||
|
|
||||||
// scroll the button into view, as if the user had scrolled
|
|
||||||
cy.get("#scroll-horizontal button").scrollIntoView().should("be.visible");
|
|
||||||
|
|
||||||
cy.get("#scroll-vertical button").should("not.be.visible");
|
|
||||||
|
|
||||||
// Cypress handles the scroll direction needed
|
|
||||||
cy.get("#scroll-vertical button").scrollIntoView().should("be.visible");
|
|
||||||
|
|
||||||
cy.get("#scroll-both button").should("not.be.visible");
|
|
||||||
|
|
||||||
// Cypress knows to scroll to the right and down
|
|
||||||
cy.get("#scroll-both button").scrollIntoView().should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".trigger() - trigger an event on a DOM element", () => {
|
|
||||||
// https://on.cypress.io/trigger
|
|
||||||
|
|
||||||
// To interact with a range input (slider)
|
|
||||||
// we need to set its value & trigger the
|
|
||||||
// event to signal it changed
|
|
||||||
|
|
||||||
// Here, we invoke jQuery's val() method to set
|
|
||||||
// the value and trigger the 'change' event
|
|
||||||
cy.get(".trigger-input-range")
|
|
||||||
.invoke("val", 25)
|
|
||||||
.trigger("change")
|
|
||||||
.get("input[type=range]")
|
|
||||||
.siblings("p")
|
|
||||||
.should("have.text", "25");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.scrollTo() - scroll the window or element to a position", () => {
|
|
||||||
// https://on.cypress.io/scrollto
|
|
||||||
|
|
||||||
// You can scroll to 9 specific positions of an element:
|
|
||||||
// -----------------------------------
|
|
||||||
// | topLeft top topRight |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | left center right |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | bottomLeft bottom bottomRight |
|
|
||||||
// -----------------------------------
|
|
||||||
|
|
||||||
// if you chain .scrollTo() off of cy, we will
|
|
||||||
// scroll the entire window
|
|
||||||
cy.scrollTo("bottom");
|
|
||||||
|
|
||||||
cy.get("#scrollable-horizontal").scrollTo("right");
|
|
||||||
|
|
||||||
// or you can scroll to a specific coordinate:
|
|
||||||
// (x axis, y axis) in pixels
|
|
||||||
cy.get("#scrollable-vertical").scrollTo(250, 250);
|
|
||||||
|
|
||||||
// or you can scroll to a specific percentage
|
|
||||||
// of the (width, height) of the element
|
|
||||||
cy.get("#scrollable-both").scrollTo("75%", "25%");
|
|
||||||
|
|
||||||
// control the easing of the scroll (default is 'swing')
|
|
||||||
cy.get("#scrollable-vertical").scrollTo("center", { easing: "linear" });
|
|
||||||
|
|
||||||
// control the duration of the scroll (in ms)
|
|
||||||
cy.get("#scrollable-both").scrollTo("center", { duration: 2000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Aliasing", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/aliasing");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".as() - alias a DOM element for later use", () => {
|
|
||||||
// https://on.cypress.io/as
|
|
||||||
|
|
||||||
// Alias a DOM element for use later
|
|
||||||
// We don't have to traverse to the element
|
|
||||||
// later in our code, we reference it with @
|
|
||||||
|
|
||||||
cy.get(".as-table").find("tbody>tr").first().find("td").first().find("button").as("firstBtn");
|
|
||||||
|
|
||||||
// when we reference the alias, we place an
|
|
||||||
// @ in front of its name
|
|
||||||
cy.get("@firstBtn").click();
|
|
||||||
|
|
||||||
cy.get("@firstBtn").should("have.class", "btn-success").and("contain", "Changed");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".as() - alias a route for later use", () => {
|
|
||||||
// Alias the route to wait for its response
|
|
||||||
cy.intercept("GET", "**/comments/*").as("getComment");
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-btn").click();
|
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
|
||||||
cy.wait("@getComment").its("response.statusCode").should("eq", 200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Assertions", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/assertions");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Implicit Assertions", () => {
|
|
||||||
it(".should() - make an assertion about the current subject", () => {
|
|
||||||
// https://on.cypress.io/should
|
|
||||||
cy.get(".assertion-table")
|
|
||||||
.find("tbody tr:last")
|
|
||||||
.should("have.class", "success")
|
|
||||||
.find("td")
|
|
||||||
.first()
|
|
||||||
// checking the text of the <td> element in various ways
|
|
||||||
.should("have.text", "Column content")
|
|
||||||
.should("contain", "Column content")
|
|
||||||
.should("have.html", "Column content")
|
|
||||||
// chai-jquery uses "is()" to check if element matches selector
|
|
||||||
.should("match", "td")
|
|
||||||
// to match text content against a regular expression
|
|
||||||
// first need to invoke jQuery method text()
|
|
||||||
// and then match using regular expression
|
|
||||||
.invoke("text")
|
|
||||||
.should("match", /column content/i);
|
|
||||||
|
|
||||||
// a better way to check element's text content against a regular expression
|
|
||||||
// is to use "cy.contains"
|
|
||||||
// https://on.cypress.io/contains
|
|
||||||
cy.get(".assertion-table")
|
|
||||||
.find("tbody tr:last")
|
|
||||||
// finds first <td> element with text content matching regular expression
|
|
||||||
.contains("td", /column content/i)
|
|
||||||
.should("be.visible");
|
|
||||||
|
|
||||||
// for more information about asserting element's text
|
|
||||||
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".and() - chain multiple assertions together", () => {
|
|
||||||
// https://on.cypress.io/and
|
|
||||||
cy.get(".assertions-link").should("have.class", "active").and("have.attr", "href").and("include", "cypress.io");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Explicit Assertions", () => {
|
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
it("expect - make an assertion about a specified subject", () => {
|
|
||||||
// We can use Chai's BDD style assertions
|
|
||||||
expect(true).to.be.true;
|
|
||||||
const o = { foo: "bar" };
|
|
||||||
|
|
||||||
expect(o).to.equal(o);
|
|
||||||
expect(o).to.deep.equal({ foo: "bar" });
|
|
||||||
// matching text using regular expression
|
|
||||||
expect("FooBar").to.match(/bar$/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("pass your own callback function to should()", () => {
|
|
||||||
// Pass a function to should that can have any number
|
|
||||||
// of explicit assertions within it.
|
|
||||||
// The ".should(cb)" function will be retried
|
|
||||||
// automatically until it passes all your explicit assertions or times out.
|
|
||||||
cy.get(".assertions-p")
|
|
||||||
.find("p")
|
|
||||||
.should(($p) => {
|
|
||||||
// https://on.cypress.io/$
|
|
||||||
// return an array of texts from all of the p's
|
|
||||||
// @ts-ignore TS6133 unused variable
|
|
||||||
const texts = $p.map((i, el) => Cypress.$(el).text());
|
|
||||||
|
|
||||||
// jquery map returns jquery object
|
|
||||||
// and .get() convert this to simple array
|
|
||||||
const paragraphs = texts.get();
|
|
||||||
|
|
||||||
// array should have length of 3
|
|
||||||
expect(paragraphs, "has 3 paragraphs").to.have.length(3);
|
|
||||||
|
|
||||||
// use second argument to expect(...) to provide clear
|
|
||||||
// message with each assertion
|
|
||||||
expect(paragraphs, "has expected text in each paragraph").to.deep.eq([
|
|
||||||
"Some text from first p",
|
|
||||||
"More text from second p",
|
|
||||||
"And even more text from third p"
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds element by class name regex", () => {
|
|
||||||
cy.get(".docs-header")
|
|
||||||
.find("div")
|
|
||||||
// .should(cb) callback function will be retried
|
|
||||||
.should(($div) => {
|
|
||||||
expect($div).to.have.length(1);
|
|
||||||
|
|
||||||
const className = $div[0].className;
|
|
||||||
|
|
||||||
expect(className).to.match(/heading-/);
|
|
||||||
})
|
|
||||||
// .then(cb) callback is not retried,
|
|
||||||
// it either passes or fails
|
|
||||||
.then(($div) => {
|
|
||||||
expect($div, "text content").to.have.text("Introduction");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can throw any error", () => {
|
|
||||||
cy.get(".docs-header")
|
|
||||||
.find("div")
|
|
||||||
.should(($div) => {
|
|
||||||
if ($div.length !== 1) {
|
|
||||||
// you can throw your own errors
|
|
||||||
throw new Error("Did not find 1 element");
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = $div[0].className;
|
|
||||||
|
|
||||||
if (!className.match(/heading-/)) {
|
|
||||||
throw new Error(`Could not find class "heading-" in ${className}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches unknown text between two elements", () => {
|
|
||||||
/**
|
|
||||||
* Text from the first element.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
let text;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes passed text,
|
|
||||||
* useful before comparing text with spaces and different capitalization.
|
|
||||||
* @param {string} s Text to normalize
|
|
||||||
*/
|
|
||||||
const normalizeText = (s) => s.replace(/\s/g, "").toLowerCase();
|
|
||||||
|
|
||||||
cy.get(".two-elements")
|
|
||||||
.find(".first")
|
|
||||||
.then(($first) => {
|
|
||||||
// save text from the first element
|
|
||||||
text = normalizeText($first.text());
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(".two-elements")
|
|
||||||
.find(".second")
|
|
||||||
.should(($div) => {
|
|
||||||
// we can massage text before comparing
|
|
||||||
const secondText = normalizeText($div.text());
|
|
||||||
|
|
||||||
expect(secondText, "second text").to.equal(text);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("assert - assert shape of an object", () => {
|
|
||||||
const person = {
|
|
||||||
name: "Joe",
|
|
||||||
age: 20
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.isObject(person, "value is object");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("retries the should callback until assertions pass", () => {
|
|
||||||
cy.get("#random-number").should(($div) => {
|
|
||||||
const n = parseFloat($div.text());
|
|
||||||
|
|
||||||
expect(n).to.be.gte(1).and.be.lte(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Connectors", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/connectors");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".each() - iterate over an array of elements", () => {
|
|
||||||
// https://on.cypress.io/each
|
|
||||||
cy.get(".connectors-each-ul>li").each(($el, index, $list) => {
|
|
||||||
console.log($el, index, $list);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".its() - get properties on the current subject", () => {
|
|
||||||
// https://on.cypress.io/its
|
|
||||||
cy.get(".connectors-its-ul>li")
|
|
||||||
// calls the 'length' property yielding that value
|
|
||||||
.its("length")
|
|
||||||
.should("be.gt", 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".invoke() - invoke a function on the current subject", () => {
|
|
||||||
// our div is hidden in our script.js
|
|
||||||
// $('.connectors-div').hide()
|
|
||||||
|
|
||||||
// https://on.cypress.io/invoke
|
|
||||||
cy.get(".connectors-div")
|
|
||||||
.should("be.hidden")
|
|
||||||
// call the jquery method 'show' on the 'div.container'
|
|
||||||
.invoke("show")
|
|
||||||
.should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".spread() - spread an array as individual args to callback function", () => {
|
|
||||||
// https://on.cypress.io/spread
|
|
||||||
const arr = ["foo", "bar", "baz"];
|
|
||||||
|
|
||||||
cy.wrap(arr).spread((foo, bar, baz) => {
|
|
||||||
expect(foo).to.eq("foo");
|
|
||||||
expect(bar).to.eq("bar");
|
|
||||||
expect(baz).to.eq("baz");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(".then()", () => {
|
|
||||||
it("invokes a callback function with the current subject", () => {
|
|
||||||
// https://on.cypress.io/then
|
|
||||||
cy.get(".connectors-list > li").then(($lis) => {
|
|
||||||
expect($lis, "3 items").to.have.length(3);
|
|
||||||
expect($lis.eq(0), "first item").to.contain("Walk the dog");
|
|
||||||
expect($lis.eq(1), "second item").to.contain("Feed the cat");
|
|
||||||
expect($lis.eq(2), "third item").to.contain("Write JavaScript");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("yields the returned value to the next command", () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1);
|
|
||||||
|
|
||||||
return 2;
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("yields the original subject without return", () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1);
|
|
||||||
// note that nothing is returned from this callback
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
// this callback receives the original unchanged value 1
|
|
||||||
expect(num).to.equal(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("yields the value yielded by the last Cypress command inside", () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1);
|
|
||||||
// note how we run a Cypress command
|
|
||||||
// the result yielded by this Cypress command
|
|
||||||
// will be passed to the second ".then"
|
|
||||||
cy.wrap(2);
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
// this callback receives the value yielded by "cy.wrap(2)"
|
|
||||||
expect(num).to.equal(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Cookies", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Cypress.Cookies.debug(true);
|
|
||||||
|
|
||||||
cy.visit("https://example.cypress.io/commands/cookies");
|
|
||||||
|
|
||||||
// clear cookies again after visiting to remove
|
|
||||||
// any 3rd party cookies picked up such as cloudflare
|
|
||||||
cy.clearCookies();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.getCookie() - get a browser cookie", () => {
|
|
||||||
// https://on.cypress.io/getcookie
|
|
||||||
cy.get("#getCookie .set-a-cookie").click();
|
|
||||||
|
|
||||||
// cy.getCookie() yields a cookie object
|
|
||||||
cy.getCookie("token").should("have.property", "value", "123ABC");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.getCookies() - get browser cookies", () => {
|
|
||||||
// https://on.cypress.io/getcookies
|
|
||||||
cy.getCookies().should("be.empty");
|
|
||||||
|
|
||||||
cy.get("#getCookies .set-a-cookie").click();
|
|
||||||
|
|
||||||
// cy.getCookies() yields an array of cookies
|
|
||||||
cy.getCookies()
|
|
||||||
.should("have.length", 1)
|
|
||||||
.should((cookies) => {
|
|
||||||
// each cookie has these properties
|
|
||||||
expect(cookies[0]).to.have.property("name", "token");
|
|
||||||
expect(cookies[0]).to.have.property("value", "123ABC");
|
|
||||||
expect(cookies[0]).to.have.property("httpOnly", false);
|
|
||||||
expect(cookies[0]).to.have.property("secure", false);
|
|
||||||
expect(cookies[0]).to.have.property("domain");
|
|
||||||
expect(cookies[0]).to.have.property("path");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.setCookie() - set a browser cookie", () => {
|
|
||||||
// https://on.cypress.io/setcookie
|
|
||||||
cy.getCookies().should("be.empty");
|
|
||||||
|
|
||||||
cy.setCookie("foo", "bar");
|
|
||||||
|
|
||||||
// cy.getCookie() yields a cookie object
|
|
||||||
cy.getCookie("foo").should("have.property", "value", "bar");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.clearCookie() - clear a browser cookie", () => {
|
|
||||||
// https://on.cypress.io/clearcookie
|
|
||||||
cy.getCookie("token").should("be.null");
|
|
||||||
|
|
||||||
cy.get("#clearCookie .set-a-cookie").click();
|
|
||||||
|
|
||||||
cy.getCookie("token").should("have.property", "value", "123ABC");
|
|
||||||
|
|
||||||
// cy.clearCookies() yields null
|
|
||||||
cy.clearCookie("token").should("be.null");
|
|
||||||
|
|
||||||
cy.getCookie("token").should("be.null");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.clearCookies() - clear browser cookies", () => {
|
|
||||||
// https://on.cypress.io/clearcookies
|
|
||||||
cy.getCookies().should("be.empty");
|
|
||||||
|
|
||||||
cy.get("#clearCookies .set-a-cookie").click();
|
|
||||||
|
|
||||||
cy.getCookies().should("have.length", 1);
|
|
||||||
|
|
||||||
// cy.clearCookies() yields null
|
|
||||||
cy.clearCookies();
|
|
||||||
|
|
||||||
cy.getCookies().should("be.empty");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Cypress.Commands", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
|
|
||||||
it(".add() - create a custom command", () => {
|
|
||||||
Cypress.Commands.add(
|
|
||||||
"console",
|
|
||||||
{
|
|
||||||
prevSubject: true
|
|
||||||
},
|
|
||||||
(subject, method) => {
|
|
||||||
// the previous subject is automatically received
|
|
||||||
// and the commands arguments are shifted
|
|
||||||
|
|
||||||
// allow us to change the console method used
|
|
||||||
method = method || "log";
|
|
||||||
|
|
||||||
// log the subject to the console
|
|
||||||
// @ts-ignore TS7017
|
|
||||||
console[method]("The subject is", subject);
|
|
||||||
|
|
||||||
// whatever we return becomes the new subject
|
|
||||||
// we don't want to change the subject so
|
|
||||||
// we return whatever was passed in
|
|
||||||
return subject;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// @ts-ignore TS2339
|
|
||||||
cy.get("button")
|
|
||||||
.console("info")
|
|
||||||
.then(($button) => {
|
|
||||||
// subject is still $button
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.Cookies", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://on.cypress.io/cookies
|
|
||||||
it(".debug() - enable or disable debugging", () => {
|
|
||||||
Cypress.Cookies.debug(true);
|
|
||||||
|
|
||||||
// Cypress will now log in the console when
|
|
||||||
// cookies are set or cleared
|
|
||||||
cy.setCookie("fakeCookie", "123ABC");
|
|
||||||
cy.clearCookie("fakeCookie");
|
|
||||||
cy.setCookie("fakeCookie", "123ABC");
|
|
||||||
cy.clearCookie("fakeCookie");
|
|
||||||
cy.setCookie("fakeCookie", "123ABC");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".preserveOnce() - preserve cookies by key", () => {
|
|
||||||
// normally cookies are reset after each test
|
|
||||||
cy.getCookie("fakeCookie").should("not.be.ok");
|
|
||||||
|
|
||||||
// preserving a cookie will not clear it when
|
|
||||||
// the next test starts
|
|
||||||
cy.setCookie("lastCookie", "789XYZ");
|
|
||||||
Cypress.Cookies.preserveOnce("lastCookie");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".defaults() - set defaults for all cookies", () => {
|
|
||||||
// now any cookie with the name 'session_id' will
|
|
||||||
// not be cleared before each new test runs
|
|
||||||
Cypress.Cookies.defaults({
|
|
||||||
preserve: "session_id"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.arch", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get CPU architecture name of underlying OS", () => {
|
|
||||||
// https://on.cypress.io/arch
|
|
||||||
expect(Cypress.arch).to.exist;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.config()", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get and set configuration options", () => {
|
|
||||||
// https://on.cypress.io/config
|
|
||||||
let myConfig = Cypress.config();
|
|
||||||
|
|
||||||
expect(myConfig).to.have.property("animationDistanceThreshold", 5);
|
|
||||||
expect(myConfig).to.have.property("baseUrl", null);
|
|
||||||
expect(myConfig).to.have.property("defaultCommandTimeout", 4000);
|
|
||||||
expect(myConfig).to.have.property("requestTimeout", 5000);
|
|
||||||
expect(myConfig).to.have.property("responseTimeout", 30000);
|
|
||||||
expect(myConfig).to.have.property("viewportHeight", 660);
|
|
||||||
expect(myConfig).to.have.property("viewportWidth", 1000);
|
|
||||||
expect(myConfig).to.have.property("pageLoadTimeout", 60000);
|
|
||||||
expect(myConfig).to.have.property("waitForAnimations", true);
|
|
||||||
|
|
||||||
expect(Cypress.config("pageLoadTimeout")).to.eq(60000);
|
|
||||||
|
|
||||||
// this will change the config for the rest of your tests!
|
|
||||||
Cypress.config("pageLoadTimeout", 20000);
|
|
||||||
|
|
||||||
expect(Cypress.config("pageLoadTimeout")).to.eq(20000);
|
|
||||||
|
|
||||||
Cypress.config("pageLoadTimeout", 60000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.dom", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://on.cypress.io/dom
|
|
||||||
it(".isHidden() - determine if a DOM element is hidden", () => {
|
|
||||||
let hiddenP = Cypress.$(".dom-p p.hidden").get(0);
|
|
||||||
let visibleP = Cypress.$(".dom-p p.visible").get(0);
|
|
||||||
|
|
||||||
// our first paragraph has css class 'hidden'
|
|
||||||
expect(Cypress.dom.isHidden(hiddenP)).to.be.true;
|
|
||||||
expect(Cypress.dom.isHidden(visibleP)).to.be.false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.env()", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
// We can set environment variables for highly dynamic values
|
|
||||||
|
|
||||||
// https://on.cypress.io/environment-variables
|
|
||||||
it("Get environment variables", () => {
|
|
||||||
// https://on.cypress.io/env
|
|
||||||
// set multiple environment variables
|
|
||||||
Cypress.env({
|
|
||||||
host: "veronica.dev.local",
|
|
||||||
api_server: "http://localhost:8888/v1/"
|
|
||||||
});
|
|
||||||
|
|
||||||
// get environment variable
|
|
||||||
expect(Cypress.env("host")).to.eq("veronica.dev.local");
|
|
||||||
|
|
||||||
// set environment variable
|
|
||||||
Cypress.env("api_server", "http://localhost:8888/v2/");
|
|
||||||
expect(Cypress.env("api_server")).to.eq("http://localhost:8888/v2/");
|
|
||||||
|
|
||||||
// get all environment variable
|
|
||||||
expect(Cypress.env()).to.have.property("host", "veronica.dev.local");
|
|
||||||
expect(Cypress.env()).to.have.property("api_server", "http://localhost:8888/v2/");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.log", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Control what is printed to the Command Log", () => {
|
|
||||||
// https://on.cypress.io/cypress-log
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.platform", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get underlying OS name", () => {
|
|
||||||
// https://on.cypress.io/platform
|
|
||||||
expect(Cypress.platform).to.be.exist;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.version", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get current version of Cypress being run", () => {
|
|
||||||
// https://on.cypress.io/version
|
|
||||||
expect(Cypress.version).to.be.exist;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.spec", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/cypress-api");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get current spec information", () => {
|
|
||||||
// https://on.cypress.io/spec
|
|
||||||
// wrap the object so we can inspect it easily by clicking in the command log
|
|
||||||
cy.wrap(Cypress.spec).should("include.keys", ["name", "relative", "absolute"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
/// JSON fixture file can be loaded directly using
|
|
||||||
// the built-in JavaScript bundler
|
|
||||||
// @ts-ignore
|
|
||||||
const requiredExample = require("../../fixtures/example");
|
|
||||||
|
|
||||||
context("Files", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/files");
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// load example.json fixture file and store
|
|
||||||
// in the test context object
|
|
||||||
cy.fixture("example.json").as("example");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.fixture() - load a fixture", () => {
|
|
||||||
// https://on.cypress.io/fixture
|
|
||||||
|
|
||||||
// Instead of writing a response inline you can
|
|
||||||
// use a fixture file's content.
|
|
||||||
|
|
||||||
// when application makes an Ajax request matching "GET **/comments/*"
|
|
||||||
// Cypress will intercept it and reply with the object in `example.json` fixture
|
|
||||||
cy.intercept("GET", "**/comments/*", { fixture: "example.json" }).as("getComment");
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".fixture-btn").click();
|
|
||||||
|
|
||||||
cy.wait("@getComment")
|
|
||||||
.its("response.body")
|
|
||||||
.should("have.property", "name")
|
|
||||||
.and("include", "Using fixtures to represent data");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.fixture() or require - load a fixture", function () {
|
|
||||||
// we are inside the "function () { ... }"
|
|
||||||
// callback and can use test context object "this"
|
|
||||||
// "this.example" was loaded in "beforeEach" function callback
|
|
||||||
expect(this.example, "fixture in the test context").to.deep.equal(requiredExample);
|
|
||||||
|
|
||||||
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
|
||||||
cy.wrap(this.example).should("deep.equal", requiredExample);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.readFile() - read file contents", () => {
|
|
||||||
// https://on.cypress.io/readfile
|
|
||||||
|
|
||||||
// You can read a file and yield its contents
|
|
||||||
// The filePath is relative to your project's root.
|
|
||||||
cy.readFile("cypress.json").then((json) => {
|
|
||||||
expect(json).to.be.an("object");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.writeFile() - write to a file", () => {
|
|
||||||
// https://on.cypress.io/writefile
|
|
||||||
|
|
||||||
// You can write to a file
|
|
||||||
|
|
||||||
// Use a response from a request to automatically
|
|
||||||
// generate a fixture file for use later
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
|
|
||||||
cy.writeFile("cypress/fixtures/users.json", response.body);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.fixture("users").should((users) => {
|
|
||||||
expect(users[0].name).to.exist;
|
|
||||||
});
|
|
||||||
|
|
||||||
// JavaScript arrays and objects are stringified
|
|
||||||
// and formatted into text.
|
|
||||||
cy.writeFile("cypress/fixtures/profile.json", {
|
|
||||||
id: 8739,
|
|
||||||
name: "Jane",
|
|
||||||
email: "jane@example.com"
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.fixture("profile").should((profile) => {
|
|
||||||
expect(profile.name).to.eq("Jane");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Local Storage", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/local-storage");
|
|
||||||
});
|
|
||||||
// Although local storage is automatically cleared
|
|
||||||
// in between tests to maintain a clean state
|
|
||||||
// sometimes we need to clear the local storage manually
|
|
||||||
|
|
||||||
it("cy.clearLocalStorage() - clear all data in local storage", () => {
|
|
||||||
// https://on.cypress.io/clearlocalstorage
|
|
||||||
cy.get(".ls-btn")
|
|
||||||
.click()
|
|
||||||
.should(() => {
|
|
||||||
expect(localStorage.getItem("prop1")).to.eq("red");
|
|
||||||
expect(localStorage.getItem("prop2")).to.eq("blue");
|
|
||||||
expect(localStorage.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
|
|
||||||
// clearLocalStorage() yields the localStorage object
|
|
||||||
cy.clearLocalStorage().should((ls) => {
|
|
||||||
expect(ls.getItem("prop1")).to.be.null;
|
|
||||||
expect(ls.getItem("prop2")).to.be.null;
|
|
||||||
expect(ls.getItem("prop3")).to.be.null;
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(".ls-btn")
|
|
||||||
.click()
|
|
||||||
.should(() => {
|
|
||||||
expect(localStorage.getItem("prop1")).to.eq("red");
|
|
||||||
expect(localStorage.getItem("prop2")).to.eq("blue");
|
|
||||||
expect(localStorage.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear key matching string in Local Storage
|
|
||||||
cy.clearLocalStorage("prop1").should((ls) => {
|
|
||||||
expect(ls.getItem("prop1")).to.be.null;
|
|
||||||
expect(ls.getItem("prop2")).to.eq("blue");
|
|
||||||
expect(ls.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(".ls-btn")
|
|
||||||
.click()
|
|
||||||
.should(() => {
|
|
||||||
expect(localStorage.getItem("prop1")).to.eq("red");
|
|
||||||
expect(localStorage.getItem("prop2")).to.eq("blue");
|
|
||||||
expect(localStorage.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear keys matching regex in Local Storage
|
|
||||||
cy.clearLocalStorage(/prop1|2/).should((ls) => {
|
|
||||||
expect(ls.getItem("prop1")).to.be.null;
|
|
||||||
expect(ls.getItem("prop2")).to.be.null;
|
|
||||||
expect(ls.getItem("prop3")).to.eq("magenta");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Location", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/location");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.hash() - get the current URL hash", () => {
|
|
||||||
// https://on.cypress.io/hash
|
|
||||||
cy.hash().should("be.empty");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.location() - get window.location", () => {
|
|
||||||
// https://on.cypress.io/location
|
|
||||||
cy.location().should((location) => {
|
|
||||||
expect(location.hash).to.be.empty;
|
|
||||||
expect(location.href).to.eq("https://example.cypress.io/commands/location");
|
|
||||||
expect(location.host).to.eq("example.cypress.io");
|
|
||||||
expect(location.hostname).to.eq("example.cypress.io");
|
|
||||||
expect(location.origin).to.eq("https://example.cypress.io");
|
|
||||||
expect(location.pathname).to.eq("/commands/location");
|
|
||||||
expect(location.port).to.eq("");
|
|
||||||
expect(location.protocol).to.eq("https:");
|
|
||||||
expect(location.search).to.be.empty;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.url() - get the current URL", () => {
|
|
||||||
// https://on.cypress.io/url
|
|
||||||
cy.url().should("eq", "https://example.cypress.io/commands/location");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Misc", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/misc");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".end() - end the command chain", () => {
|
|
||||||
// https://on.cypress.io/end
|
|
||||||
|
|
||||||
// cy.end is useful when you want to end a chain of commands
|
|
||||||
// and force Cypress to re-query from the root element
|
|
||||||
cy.get(".misc-table").within(() => {
|
|
||||||
// ends the current chain and yields null
|
|
||||||
cy.contains("Cheryl").click().end();
|
|
||||||
|
|
||||||
// queries the entire table again
|
|
||||||
cy.contains("Charles").click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.exec() - execute a system command", () => {
|
|
||||||
// execute a system command.
|
|
||||||
// so you can take actions necessary for
|
|
||||||
// your test outside the scope of Cypress.
|
|
||||||
// https://on.cypress.io/exec
|
|
||||||
|
|
||||||
// we can use Cypress.platform string to
|
|
||||||
// select appropriate command
|
|
||||||
// https://on.cypress/io/platform
|
|
||||||
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`);
|
|
||||||
|
|
||||||
// on CircleCI Windows build machines we have a failure to run bash shell
|
|
||||||
// https://github.com/cypress-io/cypress/issues/5169
|
|
||||||
// so skip some of the tests by passing flag "--env circle=true"
|
|
||||||
const isCircleOnWindows = Cypress.platform === "win32" && Cypress.env("circle");
|
|
||||||
|
|
||||||
if (isCircleOnWindows) {
|
|
||||||
cy.log("Skipping test on CircleCI");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cy.exec problem on Shippable CI
|
|
||||||
// https://github.com/cypress-io/cypress/issues/6718
|
|
||||||
const isShippable = Cypress.platform === "linux" && Cypress.env("shippable");
|
|
||||||
|
|
||||||
if (isShippable) {
|
|
||||||
cy.log("Skipping test on ShippableCI");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.exec("echo Jane Lane").its("stdout").should("contain", "Jane Lane");
|
|
||||||
|
|
||||||
if (Cypress.platform === "win32") {
|
|
||||||
cy.exec("print cypress.json").its("stderr").should("be.empty");
|
|
||||||
} else {
|
|
||||||
cy.exec("cat cypress.json").its("stderr").should("be.empty");
|
|
||||||
|
|
||||||
cy.exec("pwd").its("code").should("eq", 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.focused() - get the DOM element that has focus", () => {
|
|
||||||
// https://on.cypress.io/focused
|
|
||||||
cy.get(".misc-form").find("#name").click();
|
|
||||||
cy.focused().should("have.id", "name");
|
|
||||||
|
|
||||||
cy.get(".misc-form").find("#description").click();
|
|
||||||
cy.focused().should("have.id", "description");
|
|
||||||
});
|
|
||||||
|
|
||||||
context("Cypress.Screenshot", function () {
|
|
||||||
it("cy.screenshot() - take a screenshot", () => {
|
|
||||||
// https://on.cypress.io/screenshot
|
|
||||||
cy.screenshot("my-image");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.Screenshot.defaults() - change default config of screenshots", function () {
|
|
||||||
Cypress.Screenshot.defaults({
|
|
||||||
blackout: [".foo"],
|
|
||||||
capture: "viewport",
|
|
||||||
clip: { x: 0, y: 0, width: 200, height: 200 },
|
|
||||||
scale: false,
|
|
||||||
disableTimersAndAnimations: true,
|
|
||||||
screenshotOnRunFailure: true,
|
|
||||||
onBeforeScreenshot() {},
|
|
||||||
onAfterScreenshot() {}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.wrap() - wrap an object", () => {
|
|
||||||
// https://on.cypress.io/wrap
|
|
||||||
cy.wrap({ foo: "bar" }).should("have.property", "foo").and("include", "bar");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Navigation", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io");
|
|
||||||
cy.get(".navbar-nav").contains("Commands").click();
|
|
||||||
cy.get(".dropdown-menu").contains("Navigation").click();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.go() - go back or forward in the browser's history", () => {
|
|
||||||
// https://on.cypress.io/go
|
|
||||||
|
|
||||||
cy.location("pathname").should("include", "navigation");
|
|
||||||
|
|
||||||
cy.go("back");
|
|
||||||
cy.location("pathname").should("not.include", "navigation");
|
|
||||||
|
|
||||||
cy.go("forward");
|
|
||||||
cy.location("pathname").should("include", "navigation");
|
|
||||||
|
|
||||||
// clicking back
|
|
||||||
cy.go(-1);
|
|
||||||
cy.location("pathname").should("not.include", "navigation");
|
|
||||||
|
|
||||||
// clicking forward
|
|
||||||
cy.go(1);
|
|
||||||
cy.location("pathname").should("include", "navigation");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.reload() - reload the page", () => {
|
|
||||||
// https://on.cypress.io/reload
|
|
||||||
cy.reload();
|
|
||||||
|
|
||||||
// reload the page without using the cache
|
|
||||||
cy.reload(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.visit() - visit a remote url", () => {
|
|
||||||
// https://on.cypress.io/visit
|
|
||||||
|
|
||||||
// Visit any sub-domain of your current domain
|
|
||||||
|
|
||||||
// Pass options to the visit
|
|
||||||
cy.visit("https://example.cypress.io/commands/navigation", {
|
|
||||||
timeout: 50000, // increase total time for the visit to resolve
|
|
||||||
onBeforeLoad(contentWindow) {
|
|
||||||
// contentWindow is the remote page's window object
|
|
||||||
expect(typeof contentWindow === "object").to.be.true;
|
|
||||||
},
|
|
||||||
onLoad(contentWindow) {
|
|
||||||
// contentWindow is the remote page's window object
|
|
||||||
expect(typeof contentWindow === "object").to.be.true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Network Requests", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/network-requests");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manage HTTP requests in your app
|
|
||||||
|
|
||||||
it("cy.request() - make an XHR request", () => {
|
|
||||||
// https://on.cypress.io/request
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/comments").should((response) => {
|
|
||||||
expect(response.status).to.eq(200);
|
|
||||||
// the server sometimes gets an extra comment posted from another machine
|
|
||||||
// which gets returned as 1 extra object
|
|
||||||
expect(response.body).to.have.property("length").and.be.oneOf([500, 501]);
|
|
||||||
expect(response).to.have.property("headers");
|
|
||||||
expect(response).to.have.property("duration");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.request() - verify response using BDD syntax", () => {
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/comments").then((response) => {
|
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
expect(response).property("status").to.equal(200);
|
|
||||||
expect(response).property("body").to.have.property("length").and.be.oneOf([500, 501]);
|
|
||||||
expect(response).to.include.keys("headers", "duration");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.request() with query parameters", () => {
|
|
||||||
// will execute request
|
|
||||||
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
|
||||||
cy.request({
|
|
||||||
url: "https://jsonplaceholder.cypress.io/comments",
|
|
||||||
qs: {
|
|
||||||
postId: 1,
|
|
||||||
id: 3
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.its("body")
|
|
||||||
.should("be.an", "array")
|
|
||||||
.and("have.length", 1)
|
|
||||||
.its("0") // yields first element of the array
|
|
||||||
.should("contain", {
|
|
||||||
postId: 1,
|
|
||||||
id: 3
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.request() - pass result to the second request", () => {
|
|
||||||
// first, let's find out the userId of the first user we have
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
|
|
||||||
.its("body") // yields the response object
|
|
||||||
.its("0") // yields the first element of the returned list
|
|
||||||
// the above two commands its('body').its('0')
|
|
||||||
// can be written as its('body.0')
|
|
||||||
// if you do not care about TypeScript checks
|
|
||||||
.then((user) => {
|
|
||||||
expect(user).property("id").to.be.a("number");
|
|
||||||
// make a new post on behalf of the user
|
|
||||||
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
|
|
||||||
userId: user.id,
|
|
||||||
title: "Cypress Test Runner",
|
|
||||||
body: "Fast, easy and reliable testing for anything that runs in a browser."
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// note that the value here is the returned value of the 2nd request
|
|
||||||
// which is the new post object
|
|
||||||
.then((response) => {
|
|
||||||
expect(response).property("status").to.equal(201); // new entity created
|
|
||||||
expect(response).property("body").to.contain({
|
|
||||||
title: "Cypress Test Runner"
|
|
||||||
});
|
|
||||||
|
|
||||||
// we don't know the exact post id - only that it will be > 100
|
|
||||||
// since JSONPlaceholder has built-in 100 posts
|
|
||||||
expect(response.body).property("id").to.be.a("number").and.to.be.gt(100);
|
|
||||||
|
|
||||||
// we don't know the user id here - since it was in above closure
|
|
||||||
// so in this test just confirm that the property is there
|
|
||||||
expect(response.body).property("userId").to.be.a("number");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.request() - save response in the shared test context", () => {
|
|
||||||
// https://on.cypress.io/variables-and-aliases
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
|
|
||||||
.its("body")
|
|
||||||
.its("0") // yields the first element of the returned list
|
|
||||||
.as("user") // saves the object in the test context
|
|
||||||
.then(function () {
|
|
||||||
// NOTE 👀
|
|
||||||
// By the time this callback runs the "as('user')" command
|
|
||||||
// has saved the user object in the test context.
|
|
||||||
// To access the test context we need to use
|
|
||||||
// the "function () { ... }" callback form,
|
|
||||||
// otherwise "this" points at a wrong or undefined object!
|
|
||||||
cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
|
|
||||||
userId: this.user.id,
|
|
||||||
title: "Cypress Test Runner",
|
|
||||||
body: "Fast, easy and reliable testing for anything that runs in a browser."
|
|
||||||
})
|
|
||||||
.its("body")
|
|
||||||
.as("post"); // save the new post from the response
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
// When this callback runs, both "cy.request" API commands have finished
|
|
||||||
// and the test context has "user" and "post" objects set.
|
|
||||||
// Let's verify them.
|
|
||||||
expect(this.post, "post has the right user id").property("userId").to.equal(this.user.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.intercept() - route responses to matching requests", () => {
|
|
||||||
// https://on.cypress.io/intercept
|
|
||||||
|
|
||||||
let message = "whoa, this comment does not exist";
|
|
||||||
|
|
||||||
// Listen to GET to comments/1
|
|
||||||
cy.intercept("GET", "**/comments/*").as("getComment");
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-btn").click();
|
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
|
||||||
cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
|
|
||||||
|
|
||||||
// Listen to POST to comments
|
|
||||||
cy.intercept("POST", "**/comments").as("postComment");
|
|
||||||
|
|
||||||
// we have code that posts a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-post").click();
|
|
||||||
cy.wait("@postComment").should(({ request, response }) => {
|
|
||||||
expect(request.body).to.include("email");
|
|
||||||
expect(request.headers).to.have.property("content-type");
|
|
||||||
expect(response && response.body).to.have.property("name", "Using POST in cy.intercept()");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stub a response to PUT comments/ ****
|
|
||||||
cy.intercept(
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
url: "**/comments/*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
statusCode: 404,
|
|
||||||
body: { error: message },
|
|
||||||
headers: { "access-control-allow-origin": "*" },
|
|
||||||
delayMs: 500
|
|
||||||
}
|
|
||||||
).as("putComment");
|
|
||||||
|
|
||||||
// we have code that puts a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-put").click();
|
|
||||||
|
|
||||||
cy.wait("@putComment");
|
|
||||||
|
|
||||||
// our 404 statusCode logic in scripts.js executed
|
|
||||||
cy.get(".network-put-comment").should("contain", message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Querying", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/querying");
|
|
||||||
});
|
|
||||||
|
|
||||||
// The most commonly used query is 'cy.get()', you can
|
|
||||||
// think of this like the '$' in jQuery
|
|
||||||
|
|
||||||
it("cy.get() - query DOM elements", () => {
|
|
||||||
// https://on.cypress.io/get
|
|
||||||
|
|
||||||
cy.get("#query-btn").should("contain", "Button");
|
|
||||||
|
|
||||||
cy.get(".query-btn").should("contain", "Button");
|
|
||||||
|
|
||||||
cy.get("#querying .well>button:first").should("contain", "Button");
|
|
||||||
// ↲
|
|
||||||
// Use CSS selectors just like jQuery
|
|
||||||
|
|
||||||
cy.get('[data-test-id="test-example"]').should("have.class", "example");
|
|
||||||
|
|
||||||
// 'cy.get()' yields jQuery object, you can get its attribute
|
|
||||||
// by invoking `.attr()` method
|
|
||||||
cy.get('[data-test-id="test-example"]').invoke("attr", "data-test-id").should("equal", "test-example");
|
|
||||||
|
|
||||||
// or you can get element's CSS property
|
|
||||||
cy.get('[data-test-id="test-example"]').invoke("css", "position").should("equal", "static");
|
|
||||||
|
|
||||||
// or use assertions directly during 'cy.get()'
|
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
cy.get('[data-test-id="test-example"]')
|
|
||||||
.should("have.attr", "data-test-id", "test-example")
|
|
||||||
.and("have.css", "position", "static");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.contains() - query DOM elements with matching content", () => {
|
|
||||||
// https://on.cypress.io/contains
|
|
||||||
cy.get(".query-list").contains("bananas").should("have.class", "third");
|
|
||||||
|
|
||||||
// we can pass a regexp to `.contains()`
|
|
||||||
cy.get(".query-list").contains(/^b\w+/).should("have.class", "third");
|
|
||||||
|
|
||||||
cy.get(".query-list").contains("apples").should("have.class", "first");
|
|
||||||
|
|
||||||
// passing a selector to contains will
|
|
||||||
// yield the selector containing the text
|
|
||||||
cy.get("#querying").contains("ul", "oranges").should("have.class", "query-list");
|
|
||||||
|
|
||||||
cy.get(".query-button").contains("Save Form").should("have.class", "btn");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".within() - query DOM elements within a specific element", () => {
|
|
||||||
// https://on.cypress.io/within
|
|
||||||
cy.get(".query-form").within(() => {
|
|
||||||
cy.get("input:first").should("have.attr", "placeholder", "Email");
|
|
||||||
cy.get("input:last").should("have.attr", "placeholder", "Password");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.root() - query the root DOM element", () => {
|
|
||||||
// https://on.cypress.io/root
|
|
||||||
|
|
||||||
// By default, root is the document
|
|
||||||
cy.root().should("match", "html");
|
|
||||||
|
|
||||||
cy.get(".query-ul").within(() => {
|
|
||||||
// In this within, the root is now the ul DOM element
|
|
||||||
cy.root().should("have.class", "query-ul");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("best practices - selecting elements", () => {
|
|
||||||
// https://on.cypress.io/best-practices#Selecting-Elements
|
|
||||||
cy.get("[data-cy=best-practices-selecting-elements]").within(() => {
|
|
||||||
// Worst - too generic, no context
|
|
||||||
cy.get("button").click();
|
|
||||||
|
|
||||||
// Bad. Coupled to styling. Highly subject to change.
|
|
||||||
cy.get(".btn.btn-large").click();
|
|
||||||
|
|
||||||
// Average. Coupled to the `name` attribute which has HTML semantics.
|
|
||||||
cy.get("[name=submission]").click();
|
|
||||||
|
|
||||||
// Better. But still coupled to styling or JS event listeners.
|
|
||||||
cy.get("#main").click();
|
|
||||||
|
|
||||||
// Slightly better. Uses an ID but also ensures the element
|
|
||||||
// has an ARIA role attribute
|
|
||||||
cy.get("#main[role=button]").click();
|
|
||||||
|
|
||||||
// Much better. But still coupled to text content that may change.
|
|
||||||
cy.contains("Submit").click();
|
|
||||||
|
|
||||||
// Best. Insulated from all changes.
|
|
||||||
cy.get("[data-cy=submit]").click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
// remove no check once Cypress.sinon is typed
|
|
||||||
// https://github.com/cypress-io/cypress/issues/6720
|
|
||||||
|
|
||||||
context("Spies, Stubs, and Clock", () => {
|
|
||||||
it("cy.spy() - wrap a method in a spy", () => {
|
|
||||||
// https://on.cypress.io/spy
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
foo() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const spy = cy.spy(obj, "foo").as("anyArgs");
|
|
||||||
|
|
||||||
obj.foo();
|
|
||||||
|
|
||||||
expect(spy).to.be.called;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.spy() retries until assertions pass", () => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
/**
|
|
||||||
* Prints the argument passed
|
|
||||||
* @param x {any}
|
|
||||||
*/
|
|
||||||
foo(x) {
|
|
||||||
console.log("obj.foo called with", x);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cy.spy(obj, "foo").as("foo");
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
obj.foo("first");
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
obj.foo("second");
|
|
||||||
}, 2500);
|
|
||||||
|
|
||||||
cy.get("@foo").should("have.been.calledTwice");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.stub() - create a stub and/or replace a function with stub", () => {
|
|
||||||
// https://on.cypress.io/stub
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
/**
|
|
||||||
* prints both arguments to the console
|
|
||||||
* @param a {string}
|
|
||||||
* @param b {string}
|
|
||||||
*/
|
|
||||||
foo(a, b) {
|
|
||||||
console.log("a", a, "b", b);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stub = cy.stub(obj, "foo").as("foo");
|
|
||||||
|
|
||||||
obj.foo("foo", "bar");
|
|
||||||
|
|
||||||
expect(stub).to.be.called;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.clock() - control time in the browser", () => {
|
|
||||||
// https://on.cypress.io/clock
|
|
||||||
|
|
||||||
// create the date in UTC so its always the same
|
|
||||||
// no matter what local timezone the browser is running in
|
|
||||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
|
|
||||||
|
|
||||||
cy.clock(now);
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
cy.get("#clock-div").click().should("have.text", "1489449600");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.tick() - move time in the browser", () => {
|
|
||||||
// https://on.cypress.io/tick
|
|
||||||
|
|
||||||
// create the date in UTC so its always the same
|
|
||||||
// no matter what local timezone the browser is running in
|
|
||||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime();
|
|
||||||
|
|
||||||
cy.clock(now);
|
|
||||||
cy.visit("https://example.cypress.io/commands/spies-stubs-clocks");
|
|
||||||
cy.get("#tick-div").click().should("have.text", "1489449600");
|
|
||||||
|
|
||||||
cy.tick(10000); // 10 seconds passed
|
|
||||||
cy.get("#tick-div").click().should("have.text", "1489449610");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.stub() matches depending on arguments", () => {
|
|
||||||
// see all possible matchers at
|
|
||||||
// https://sinonjs.org/releases/latest/matchers/
|
|
||||||
const greeter = {
|
|
||||||
/**
|
|
||||||
* Greets a person
|
|
||||||
* @param {string} name
|
|
||||||
*/
|
|
||||||
greet(name) {
|
|
||||||
return `Hello, ${name}!`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cy.stub(greeter, "greet")
|
|
||||||
.callThrough() // if you want non-matched calls to call the real method
|
|
||||||
.withArgs(Cypress.sinon.match.string)
|
|
||||||
.returns("Hi")
|
|
||||||
.withArgs(Cypress.sinon.match.number)
|
|
||||||
.throws(new Error("Invalid name"));
|
|
||||||
|
|
||||||
expect(greeter.greet("World")).to.equal("Hi");
|
|
||||||
// @ts-ignore
|
|
||||||
expect(() => greeter.greet(42)).to.throw("Invalid name");
|
|
||||||
expect(greeter.greet).to.have.been.calledTwice;
|
|
||||||
|
|
||||||
// non-matched calls goes the actual method
|
|
||||||
// @ts-ignore
|
|
||||||
expect(greeter.greet()).to.equal("Hello, undefined!");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches call arguments using Sinon matchers", () => {
|
|
||||||
// see all possible matchers at
|
|
||||||
// https://sinonjs.org/releases/latest/matchers/
|
|
||||||
const calculator = {
|
|
||||||
/**
|
|
||||||
* returns the sum of two arguments
|
|
||||||
* @param a {number}
|
|
||||||
* @param b {number}
|
|
||||||
*/
|
|
||||||
add(a, b) {
|
|
||||||
return a + b;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const spy = cy.spy(calculator, "add").as("add");
|
|
||||||
|
|
||||||
expect(calculator.add(2, 3)).to.equal(5);
|
|
||||||
|
|
||||||
// if we want to assert the exact values used during the call
|
|
||||||
expect(spy).to.be.calledWith(2, 3);
|
|
||||||
|
|
||||||
// let's confirm "add" method was called with two numbers
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number);
|
|
||||||
|
|
||||||
// alternatively, provide the value to match
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3));
|
|
||||||
|
|
||||||
// match any value
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);
|
|
||||||
|
|
||||||
// match any value from a list
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given number is event
|
|
||||||
* @param {number} x
|
|
||||||
*/
|
|
||||||
const isEven = (x) => x % 2 === 0;
|
|
||||||
|
|
||||||
// expect the value to pass a custom predicate function
|
|
||||||
// the second argument to "sinon.match(predicate, message)" is
|
|
||||||
// shown if the predicate does not pass and assertion fails
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, "isEven"), 3);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function that checks if a given number is larger than the limit
|
|
||||||
* @param {number} limit
|
|
||||||
* @returns {(x: number) => boolean}
|
|
||||||
*/
|
|
||||||
const isGreaterThan = (limit) => (x) => x > limit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function that checks if a given number is less than the limit
|
|
||||||
* @param {number} limit
|
|
||||||
* @returns {(x: number) => boolean}
|
|
||||||
*/
|
|
||||||
const isLessThan = (limit) => (x) => x < limit;
|
|
||||||
|
|
||||||
// you can combine several matchers using "and", "or"
|
|
||||||
expect(spy).to.be.calledWith(
|
|
||||||
Cypress.sinon.match.number,
|
|
||||||
Cypress.sinon.match(isGreaterThan(2), "> 2").and(Cypress.sinon.match(isLessThan(4), "< 4"))
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(spy).to.be.calledWith(
|
|
||||||
Cypress.sinon.match.number,
|
|
||||||
Cypress.sinon.match(isGreaterThan(200), "> 200").or(Cypress.sinon.match(3))
|
|
||||||
);
|
|
||||||
|
|
||||||
// matchers can be used from BDD assertions
|
|
||||||
cy.get("@add").should("have.been.calledWith", Cypress.sinon.match.number, Cypress.sinon.match(3));
|
|
||||||
|
|
||||||
// you can alias matchers for shorter test code
|
|
||||||
const { match: M } = Cypress.sinon;
|
|
||||||
|
|
||||||
cy.get("@add").should("have.been.calledWith", M.number, M(3));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Traversal", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/traversal");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".children() - get child DOM elements", () => {
|
|
||||||
// https://on.cypress.io/children
|
|
||||||
cy.get(".traversal-breadcrumb").children(".active").should("contain", "Data");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".closest() - get closest ancestor DOM element", () => {
|
|
||||||
// https://on.cypress.io/closest
|
|
||||||
cy.get(".traversal-badge").closest("ul").should("have.class", "list-group");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".eq() - get a DOM element at a specific index", () => {
|
|
||||||
// https://on.cypress.io/eq
|
|
||||||
cy.get(".traversal-list>li").eq(1).should("contain", "siamese");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".filter() - get DOM elements that match the selector", () => {
|
|
||||||
// https://on.cypress.io/filter
|
|
||||||
cy.get(".traversal-nav>li").filter(".active").should("contain", "About");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".find() - get descendant DOM elements of the selector", () => {
|
|
||||||
// https://on.cypress.io/find
|
|
||||||
cy.get(".traversal-pagination").find("li").find("a").should("have.length", 7);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".first() - get first DOM element", () => {
|
|
||||||
// https://on.cypress.io/first
|
|
||||||
cy.get(".traversal-table td").first().should("contain", "1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".last() - get last DOM element", () => {
|
|
||||||
// https://on.cypress.io/last
|
|
||||||
cy.get(".traversal-buttons .btn").last().should("contain", "Submit");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".next() - get next sibling DOM element", () => {
|
|
||||||
// https://on.cypress.io/next
|
|
||||||
cy.get(".traversal-ul").contains("apples").next().should("contain", "oranges");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".nextAll() - get all next sibling DOM elements", () => {
|
|
||||||
// https://on.cypress.io/nextall
|
|
||||||
cy.get(".traversal-next-all").contains("oranges").nextAll().should("have.length", 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".nextUntil() - get next sibling DOM elements until next el", () => {
|
|
||||||
// https://on.cypress.io/nextuntil
|
|
||||||
cy.get("#veggies").nextUntil("#nuts").should("have.length", 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".not() - remove DOM elements from set of DOM elements", () => {
|
|
||||||
// https://on.cypress.io/not
|
|
||||||
cy.get(".traversal-disabled .btn").not("[disabled]").should("not.contain", "Disabled");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".parent() - get parent DOM element from DOM elements", () => {
|
|
||||||
// https://on.cypress.io/parent
|
|
||||||
cy.get(".traversal-mark").parent().should("contain", "Morbi leo risus");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".parents() - get parent DOM elements from DOM elements", () => {
|
|
||||||
// https://on.cypress.io/parents
|
|
||||||
cy.get(".traversal-cite").parents().should("match", "blockquote");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => {
|
|
||||||
// https://on.cypress.io/parentsuntil
|
|
||||||
cy.get(".clothes-nav").find(".active").parentsUntil(".clothes-nav").should("have.length", 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".prev() - get previous sibling DOM element", () => {
|
|
||||||
// https://on.cypress.io/prev
|
|
||||||
cy.get(".birds").find(".active").prev().should("contain", "Lorikeets");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".prevAll() - get all previous sibling DOM elements", () => {
|
|
||||||
// https://on.cypress.io/prevall
|
|
||||||
cy.get(".fruits-list").find(".third").prevAll().should("have.length", 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".prevUntil() - get all previous sibling DOM elements until el", () => {
|
|
||||||
// https://on.cypress.io/prevuntil
|
|
||||||
cy.get(".foods-list").find("#nuts").prevUntil("#veggies").should("have.length", 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(".siblings() - get all sibling DOM elements", () => {
|
|
||||||
// https://on.cypress.io/siblings
|
|
||||||
cy.get(".traversal-pills .active").siblings().should("have.length", 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Utilities", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/utilities");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress._ - call a lodash method", () => {
|
|
||||||
// https://on.cypress.io/_
|
|
||||||
cy.request("https://jsonplaceholder.cypress.io/users").then((response) => {
|
|
||||||
let ids = Cypress._.chain(response.body).map("id").take(3).value();
|
|
||||||
|
|
||||||
expect(ids).to.deep.eq([1, 2, 3]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.$ - call a jQuery method", () => {
|
|
||||||
// https://on.cypress.io/$
|
|
||||||
let $li = Cypress.$(".utility-jquery li:first");
|
|
||||||
|
|
||||||
cy.wrap($li).should("not.have.class", "active").click().should("have.class", "active");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.Blob - blob utilities and base64 string conversion", () => {
|
|
||||||
// https://on.cypress.io/blob
|
|
||||||
cy.get(".utility-blob").then(($div) => {
|
|
||||||
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
|
||||||
// get the dataUrl string for the javascript-logo
|
|
||||||
return Cypress.Blob.imgSrcToDataURL(
|
|
||||||
"https://example.cypress.io/assets/img/javascript-logo.png",
|
|
||||||
undefined,
|
|
||||||
"anonymous"
|
|
||||||
).then((dataUrl) => {
|
|
||||||
// create an <img> element and set its src to the dataUrl
|
|
||||||
let img = Cypress.$("<img />", { src: dataUrl });
|
|
||||||
|
|
||||||
// need to explicitly return cy here since we are initially returning
|
|
||||||
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
|
||||||
// append the image
|
|
||||||
$div.append(img);
|
|
||||||
|
|
||||||
cy.get(".utility-blob img").click().should("have.attr", "src", dataUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.minimatch - test out glob patterns against strings", () => {
|
|
||||||
// https://on.cypress.io/minimatch
|
|
||||||
let matching = Cypress.minimatch("/users/1/comments", "/users/*/comments", {
|
|
||||||
matchBase: true
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(matching, "matching wildcard").to.be.true;
|
|
||||||
|
|
||||||
matching = Cypress.minimatch("/users/1/comments/2", "/users/*/comments", {
|
|
||||||
matchBase: true
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(matching, "comments").to.be.false;
|
|
||||||
|
|
||||||
// ** matches against all downstream path segments
|
|
||||||
matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/**", {
|
|
||||||
matchBase: true
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(matching, "comments").to.be.true;
|
|
||||||
|
|
||||||
// whereas * matches only the next path segment
|
|
||||||
|
|
||||||
matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/*", {
|
|
||||||
matchBase: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(matching, "comments").to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Cypress.Promise - instantiate a bluebird promise", () => {
|
|
||||||
// https://on.cypress.io/promise
|
|
||||||
let waited = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Bluebird<string>
|
|
||||||
*/
|
|
||||||
function waitOneSecond() {
|
|
||||||
// return a promise that resolves after 1 second
|
|
||||||
// @ts-ignore TS2351 (new Cypress.Promise)
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// set waited to true
|
|
||||||
waited = true;
|
|
||||||
|
|
||||||
// resolve with 'foo' string
|
|
||||||
resolve("foo");
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.then(() => {
|
|
||||||
// return a promise to cy.then() that
|
|
||||||
// is awaited until it resolves
|
|
||||||
// @ts-ignore TS7006
|
|
||||||
return waitOneSecond().then((str) => {
|
|
||||||
expect(str).to.eq("foo");
|
|
||||||
expect(waited).to.be.true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Viewport", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/viewport");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.viewport() - set the viewport size and dimension", () => {
|
|
||||||
// https://on.cypress.io/viewport
|
|
||||||
|
|
||||||
cy.get("#navbar").should("be.visible");
|
|
||||||
cy.viewport(320, 480);
|
|
||||||
|
|
||||||
// the navbar should have collapse since our screen is smaller
|
|
||||||
cy.get("#navbar").should("not.be.visible");
|
|
||||||
cy.get(".navbar-toggle").should("be.visible").click();
|
|
||||||
cy.get(".nav").find("a").should("be.visible");
|
|
||||||
|
|
||||||
// lets see what our app looks like on a super large screen
|
|
||||||
cy.viewport(2999, 2999);
|
|
||||||
|
|
||||||
// cy.viewport() accepts a set of preset sizes
|
|
||||||
// to easily set the screen to a device's width and height
|
|
||||||
|
|
||||||
// We added a cy.wait() between each viewport change so you can see
|
|
||||||
// the change otherwise it is a little too fast to see :)
|
|
||||||
|
|
||||||
cy.viewport("macbook-15");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("macbook-13");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("macbook-11");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("ipad-2");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("ipad-mini");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-6+");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-6");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-5");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-4");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-3");
|
|
||||||
cy.wait(200);
|
|
||||||
|
|
||||||
// cy.viewport() accepts an orientation for all presets
|
|
||||||
// the default orientation is 'portrait'
|
|
||||||
cy.viewport("ipad-2", "portrait");
|
|
||||||
cy.wait(200);
|
|
||||||
cy.viewport("iphone-4", "landscape");
|
|
||||||
cy.wait(200);
|
|
||||||
|
|
||||||
// The viewport will be reset back to the default dimensions
|
|
||||||
// in between tests (the default can be set in cypress.json)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Waiting", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/waiting");
|
|
||||||
});
|
|
||||||
// BE CAREFUL of adding unnecessary wait times.
|
|
||||||
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
|
||||||
it("cy.wait() - wait for a specific amount of time", () => {
|
|
||||||
cy.get(".wait-input1").type("Wait 1000ms after typing");
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get(".wait-input2").type("Wait 1000ms after typing");
|
|
||||||
cy.wait(1000);
|
|
||||||
cy.get(".wait-input3").type("Wait 1000ms after typing");
|
|
||||||
cy.wait(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.wait() - wait for a specific route", () => {
|
|
||||||
// Listen to GET to comments/1
|
|
||||||
cy.intercept("GET", "**/comments/*").as("getComment");
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get(".network-btn").click();
|
|
||||||
|
|
||||||
// wait for GET comments/1
|
|
||||||
cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Window", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("https://example.cypress.io/commands/window");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.window() - get the global window object", () => {
|
|
||||||
// https://on.cypress.io/window
|
|
||||||
cy.window().should("have.property", "top");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.document() - get the document object", () => {
|
|
||||||
// https://on.cypress.io/document
|
|
||||||
cy.document().should("have.property", "charset").and("eq", "UTF-8");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("cy.title() - get the title", () => {
|
|
||||||
// https://on.cypress.io/title
|
|
||||||
cy.title().should("include", "Kitchen Sink");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Using fixtures to represent data",
|
|
||||||
"email": "hello@cypress.io",
|
|
||||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"id": 8739,
|
|
||||||
"name": "Jane",
|
|
||||||
"email": "jane@example.com"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
// ***********************************************************
|
|
||||||
// This example plugins/index.jsx can be used to load plugins
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off loading
|
|
||||||
// the plugins file with the 'pluginsFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/plugins-guide
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// This function is called when a project is opened or re-opened (e.g. due to
|
|
||||||
// the project's config changing)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Cypress.PluginConfig}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
module.exports = (on, config) => {
|
|
||||||
// `on` is used to hook into various events Cypress emits
|
|
||||||
// `config` is the resolved Cypress config
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// ***********************************************
|
|
||||||
// This example commands.js shows you how to
|
|
||||||
// create various custom commands and overwrite
|
|
||||||
// existing commands.
|
|
||||||
//
|
|
||||||
// For more comprehensive examples of custom
|
|
||||||
// commands please read more here:
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a parent command --
|
|
||||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a child command --
|
|
||||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a dual command --
|
|
||||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This will overwrite an existing command --
|
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
|
||||||
|
|
||||||
import "@testing-library/cypress/add-commands";
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/index.jsx is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
|
||||||
// require('./commands')
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowJs": true,
|
|
||||||
"baseUrl": "../node_modules",
|
|
||||||
"types": ["cypress"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.*"]
|
|
||||||
}
|
|
||||||
2982
client/package-lock.json
generated
2982
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,29 +2,34 @@
|
|||||||
"name": "bodyshop",
|
"name": "bodyshop",
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.2"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/pro-layout": "^7.22.3",
|
"@ant-design/pro-layout": "^7.22.3",
|
||||||
"@apollo/client": "^3.13.1",
|
"@apollo/client": "^3.13.5",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
|
"@firebase/analytics": "^0.10.12",
|
||||||
|
"@firebase/app": "^0.11.3",
|
||||||
|
"@firebase/auth": "^1.9.1",
|
||||||
|
"@firebase/firestore": "^4.7.10",
|
||||||
|
"@firebase/messaging": "^0.12.17",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.6.0",
|
"@reduxjs/toolkit": "^2.6.1",
|
||||||
"@sentry/cli": "^2.42.2",
|
"@sentry/cli": "^2.42.4",
|
||||||
"@sentry/react": "^9.3.0",
|
"@sentry/react": "^9.9.0",
|
||||||
"@sentry/vite-plugin": "^3.2.2",
|
"@sentry/vite-plugin": "^3.2.2",
|
||||||
"@splitsoftware/splitio-react": "^1.13.0",
|
"@splitsoftware/splitio-react": "^2.0.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"antd": "^5.24.2",
|
"antd": "^5.24.5",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.1.0",
|
"apollo-link-sentry": "^4.2.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"css-box-model": "^1.2.1",
|
"css-box-model": "^1.2.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
@@ -33,14 +38,13 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^10.13.2",
|
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.10.0",
|
||||||
"i18next": "^23.15.1",
|
"i18next": "^24.2.3",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.4",
|
"libphonenumber-js": "^1.12.6",
|
||||||
"logrocket": "^8.1.2",
|
"logrocket": "^9.0.2",
|
||||||
"markerjs2": "^2.32.3",
|
"markerjs2": "^2.32.4",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
@@ -50,15 +54,15 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.18.0",
|
"react-big-calendar": "^1.18.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^7.2.2",
|
"react-cookie": "^8.0.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^14.1.3",
|
"react-i18next": "^15.4.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^10.1.0",
|
||||||
"react-number-format": "^5.4.3",
|
"react-number-format": "^5.4.3",
|
||||||
"react-popopo": "^2.1.9",
|
"react-popopo": "^2.1.9",
|
||||||
"react-product-fruits": "^2.2.61",
|
"react-product-fruits": "^2.2.61",
|
||||||
@@ -74,12 +78,11 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.85.1",
|
"sass": "^1.86.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.15",
|
"styled-components": "^6.1.16",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"use-memo-one": "^1.1.3",
|
"use-memo-one": "^1.1.3",
|
||||||
"userpilot": "^1.3.8",
|
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^3.5.2"
|
"web-vitals": "^3.5.2"
|
||||||
},
|
},
|
||||||
@@ -96,8 +99,6 @@
|
|||||||
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
|
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
|
||||||
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
||||||
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
|
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
|
||||||
"test": "cypress open",
|
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||||
"eulaize": "node src/utils/eulaize.js"
|
"eulaize": "node src/utils/eulaize.js"
|
||||||
},
|
},
|
||||||
@@ -120,23 +121,19 @@
|
|||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^5.6.1",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@dotenvx/dotenvx": "^1.38.3",
|
"@dotenvx/dotenvx": "^1.39.0",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.23.0",
|
||||||
"@sentry/webpack-plugin": "^3.2.2",
|
"@sentry/webpack-plugin": "^3.2.2",
|
||||||
"@testing-library/cypress": "^10.0.2",
|
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.4",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"cypress": "^13.17.0",
|
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"memfs": "^4.17.0",
|
"memfs": "^4.17.0",
|
||||||
@@ -144,11 +141,11 @@
|
|||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^6.2.0",
|
"vite": "^6.2.3",
|
||||||
"vite-plugin-babel": "^1.3.0",
|
"vite-plugin-babel": "^1.3.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^0.21.1",
|
"vite-plugin-pwa": "^0.21.2",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
import { ApolloProvider } from "@apollo/client";
|
import { ApolloProvider } from "@apollo/client";
|
||||||
import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
|
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||||
import { ConfigProvider } from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import React from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
import themeProvider from "./themeProvider";
|
import themeProvider from "./themeProvider";
|
||||||
import { Userpilot } from "userpilot";
|
|
||||||
|
|
||||||
// Initialize Userpilot
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
Userpilot.initialize("NX-69145f08");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Base Split configuration
|
||||||
const config = {
|
const config = {
|
||||||
core: {
|
core: {
|
||||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||||
key: "anon"
|
key: "anon" // Default key, overridden dynamically by SplitClientProvider
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const factory = SplitSdk(config);
|
|
||||||
|
// Custom provider to manage the Split client key based on imexshopid from Redux
|
||||||
|
function SplitClientProvider({ children }) {
|
||||||
|
const imexshopid = useSelector((state) => state.user.imexshopid); // Access imexshopid from Redux store
|
||||||
|
const splitClient = useSplitClient({ key: imexshopid || "anon" }); // Use imexshopid or fallback to "anon"
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (splitClient && imexshopid) {
|
||||||
|
// Log readiness for debugging; no need for ready() since isReady is available
|
||||||
|
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
||||||
|
}
|
||||||
|
}, [splitClient, imexshopid]);
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
function AppContainer() {
|
function AppContainer() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -31,7 +40,6 @@ function AppContainer() {
|
|||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
//componentSize="small"
|
|
||||||
input={{ autoComplete: "new-password" }}
|
input={{ autoComplete: "new-password" }}
|
||||||
locale={enLocale}
|
locale={enLocale}
|
||||||
theme={themeProvider}
|
theme={themeProvider}
|
||||||
@@ -43,8 +51,10 @@ function AppContainer() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GlobalLoadingBar />
|
<GlobalLoadingBar />
|
||||||
<SplitFactoryProvider factory={factory}>
|
<SplitFactoryProvider config={config}>
|
||||||
<App />
|
<SplitClientProvider>
|
||||||
|
<App />
|
||||||
|
</SplitClientProvider>
|
||||||
</SplitFactoryProvider>
|
</SplitFactoryProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { PictureFilled } from "@ant-design/icons";
|
import { PictureFilled } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Badge, Popover } from "antd";
|
import { Badge, Popover } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -9,6 +10,7 @@ import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||||
|
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
||||||
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
|
||||||
@@ -23,6 +25,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
|||||||
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const {
|
||||||
|
treatments: { Imgproxy }
|
||||||
|
} = useSplitTreatments({
|
||||||
|
attributes: {},
|
||||||
|
names: ["Imgproxy"],
|
||||||
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
@@ -42,6 +51,10 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
setSelectedMedia([]);
|
setSelectedMedia([]);
|
||||||
}, [setSelectedMedia, conversation]);
|
}, [setSelectedMedia, conversation]);
|
||||||
|
|
||||||
|
//Knowingly taking on the technical debt of poor implementation below. Done this way to avoid an edge case where no component may be displayed.
|
||||||
|
//Cloudinary will be removed once the migration is completed.
|
||||||
|
//If Imageproxy is on, rely only on the LMS selector
|
||||||
|
//If not on, use the old methods.
|
||||||
const content = (
|
const content = (
|
||||||
<div>
|
<div>
|
||||||
{loading && <LoadingSpinner />}
|
{loading && <LoadingSpinner />}
|
||||||
@@ -49,17 +62,37 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!bodyshop.uselocalmediaserver && data && (
|
|
||||||
<JobDocumentsGalleryExternal
|
{Imgproxy.treatment === "on" ? (
|
||||||
data={data ? data.documents : []}
|
<>
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
{!bodyshop.uselocalmediaserver && (
|
||||||
/>
|
<JobsDocumentImgproxyGalleryExternal
|
||||||
)}
|
jobId={conversation.job_conversations[0].jobid}
|
||||||
{bodyshop.uselocalmediaserver && open && (
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
<JobDocumentsLocalGalleryExternal
|
/>
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
)}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
/>
|
<JobDocumentsLocalGalleryExternal
|
||||||
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
|
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!bodyshop.uselocalmediaserver && data && (
|
||||||
|
<JobDocumentsGalleryExternal
|
||||||
|
data={data ? data.documents : []}
|
||||||
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
|
<JobDocumentsLocalGalleryExternal
|
||||||
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
|
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
132
client/src/components/dashboard-grid/componentList.js
Normal file
132
client/src/components/dashboard-grid/componentList.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import i18next from "i18next";
|
||||||
|
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
||||||
|
import {
|
||||||
|
DashboardTotalProductionHours,
|
||||||
|
DashboardTotalProductionHoursGql
|
||||||
|
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
|
||||||
|
import DashboardProjectedMonthlySales, {
|
||||||
|
DashboardProjectedMonthlySalesGql
|
||||||
|
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
|
||||||
|
import DashboardMonthlyRevenueGraph, {
|
||||||
|
DashboardMonthlyRevenueGraphGql
|
||||||
|
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
|
||||||
|
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
|
||||||
|
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
|
||||||
|
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
|
||||||
|
import DashboardMonthlyEmployeeEfficiency, {
|
||||||
|
DashboardMonthlyEmployeeEfficiencyGql
|
||||||
|
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx";
|
||||||
|
import DashboardScheduledInToday, {
|
||||||
|
DashboardScheduledInTodayGql
|
||||||
|
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx";
|
||||||
|
import DashboardScheduledOutToday, {
|
||||||
|
DashboardScheduledOutTodayGql
|
||||||
|
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx";
|
||||||
|
import JobLifecycleDashboardComponent, {
|
||||||
|
JobLifecycleDashboardGQL
|
||||||
|
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
||||||
|
|
||||||
|
const componentList = {
|
||||||
|
ProductionDollars: {
|
||||||
|
label: i18next.t("dashboard.titles.productiondollars"),
|
||||||
|
component: DashboardTotalProductionDollars,
|
||||||
|
gqlFragment: null,
|
||||||
|
w: 1,
|
||||||
|
h: 1,
|
||||||
|
minW: 2,
|
||||||
|
minH: 1
|
||||||
|
},
|
||||||
|
ProductionHours: {
|
||||||
|
label: i18next.t("dashboard.titles.productionhours"),
|
||||||
|
component: DashboardTotalProductionHours,
|
||||||
|
gqlFragment: DashboardTotalProductionHoursGql,
|
||||||
|
w: 3,
|
||||||
|
h: 1,
|
||||||
|
minW: 3,
|
||||||
|
minH: 1
|
||||||
|
},
|
||||||
|
ProjectedMonthlySales: {
|
||||||
|
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
||||||
|
component: DashboardProjectedMonthlySales,
|
||||||
|
gqlFragment: DashboardProjectedMonthlySalesGql,
|
||||||
|
w: 2,
|
||||||
|
h: 1,
|
||||||
|
minW: 2,
|
||||||
|
minH: 1
|
||||||
|
},
|
||||||
|
MonthlyRevenueGraph: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
||||||
|
component: DashboardMonthlyRevenueGraph,
|
||||||
|
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
||||||
|
w: 4,
|
||||||
|
h: 2,
|
||||||
|
minW: 4,
|
||||||
|
minH: 2
|
||||||
|
},
|
||||||
|
MonthlyJobCosting: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
||||||
|
component: DashboardMonthlyJobCosting,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 6,
|
||||||
|
minH: 3,
|
||||||
|
w: 6,
|
||||||
|
h: 3
|
||||||
|
},
|
||||||
|
MonthlyPartsSales: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlypartssales"),
|
||||||
|
component: DashboardMonthlyPartsSales,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 2,
|
||||||
|
minH: 2,
|
||||||
|
w: 2,
|
||||||
|
h: 2
|
||||||
|
},
|
||||||
|
MonthlyLaborSales: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
||||||
|
component: DashboardMonthlyLaborSales,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 2,
|
||||||
|
minH: 2,
|
||||||
|
w: 2,
|
||||||
|
h: 2
|
||||||
|
},
|
||||||
|
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
|
||||||
|
MonthlyEmployeeEfficency: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
||||||
|
component: DashboardMonthlyEmployeeEfficiency,
|
||||||
|
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
||||||
|
minW: 2,
|
||||||
|
minH: 2,
|
||||||
|
w: 2,
|
||||||
|
h: 2
|
||||||
|
},
|
||||||
|
ScheduleInToday: {
|
||||||
|
label: i18next.t("dashboard.titles.scheduledintoday"),
|
||||||
|
component: DashboardScheduledInToday,
|
||||||
|
gqlFragment: DashboardScheduledInTodayGql,
|
||||||
|
minW: 6,
|
||||||
|
minH: 2,
|
||||||
|
w: 10,
|
||||||
|
h: 3
|
||||||
|
},
|
||||||
|
ScheduleOutToday: {
|
||||||
|
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
||||||
|
component: DashboardScheduledOutToday,
|
||||||
|
gqlFragment: DashboardScheduledOutTodayGql,
|
||||||
|
minW: 6,
|
||||||
|
minH: 2,
|
||||||
|
w: 10,
|
||||||
|
h: 3
|
||||||
|
},
|
||||||
|
JobLifecycle: {
|
||||||
|
label: i18next.t("dashboard.titles.joblifecycle"),
|
||||||
|
component: JobLifecycleDashboardComponent,
|
||||||
|
gqlFragment: JobLifecycleDashboardGQL,
|
||||||
|
minW: 6,
|
||||||
|
minH: 3,
|
||||||
|
w: 6,
|
||||||
|
h: 3
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default componentList;
|
||||||
85
client/src/components/dashboard-grid/createDashboardQuery.js
Normal file
85
client/src/components/dashboard-grid/createDashboardQuery.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
import dayjs from "../../utils/day.js";
|
||||||
|
import componentList from "./componentList.js";
|
||||||
|
|
||||||
|
const createDashboardQuery = (state) => {
|
||||||
|
const componentBasedAdditions =
|
||||||
|
state &&
|
||||||
|
Array.isArray(state.layout) &&
|
||||||
|
state.layout.map((item, index) => componentList[item.i].gqlFragment || "").join("");
|
||||||
|
return gql`
|
||||||
|
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
||||||
|
monthly_sales: jobs(where: {_and: [
|
||||||
|
{ voided: {_eq: false}},
|
||||||
|
{date_invoiced: {_gte: "${dayjs()
|
||||||
|
.startOf("month")
|
||||||
|
.startOf("day")
|
||||||
|
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs().endOf("month").endOf("day").toISOString()}"}}]}) {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
date_invoiced
|
||||||
|
job_totals
|
||||||
|
rate_la1
|
||||||
|
rate_la2
|
||||||
|
rate_la3
|
||||||
|
rate_la4
|
||||||
|
rate_laa
|
||||||
|
rate_lab
|
||||||
|
rate_lad
|
||||||
|
rate_lae
|
||||||
|
rate_laf
|
||||||
|
rate_lag
|
||||||
|
rate_lam
|
||||||
|
rate_lar
|
||||||
|
rate_las
|
||||||
|
rate_lau
|
||||||
|
rate_ma2s
|
||||||
|
rate_ma2t
|
||||||
|
rate_ma3s
|
||||||
|
rate_mabl
|
||||||
|
rate_macs
|
||||||
|
rate_mahw
|
||||||
|
rate_mapa
|
||||||
|
rate_mash
|
||||||
|
rate_matd
|
||||||
|
joblines(where: { removed: { _eq: false } }) {
|
||||||
|
id
|
||||||
|
mod_lbr_ty
|
||||||
|
mod_lb_hrs
|
||||||
|
act_price
|
||||||
|
part_qty
|
||||||
|
part_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
ins_co_nm
|
||||||
|
job_totals
|
||||||
|
joblines(where: { removed: { _eq: false } }) {
|
||||||
|
id
|
||||||
|
mod_lbr_ty
|
||||||
|
mod_lb_hrs
|
||||||
|
act_price
|
||||||
|
part_qty
|
||||||
|
part_type
|
||||||
|
}
|
||||||
|
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createDashboardQuery;
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import Icon, { SyncOutlined } from "@ant-design/icons";
|
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||||
import { gql, useMutation, useQuery } from "@apollo/client";
|
import { isEmpty, cloneDeep } from "lodash";
|
||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Dropdown, Space } from "antd";
|
import { Button, Dropdown, Space } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import i18next from "i18next";
|
import { useMemo, useState } from "react";
|
||||||
import _ from "lodash";
|
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MdClose } from "react-icons/md";
|
import { MdClose } from "react-icons/md";
|
||||||
@@ -15,38 +13,13 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import DashboardMonthlyEmployeeEfficiency, {
|
|
||||||
DashboardMonthlyEmployeeEfficiencyGql
|
|
||||||
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
|
|
||||||
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
|
|
||||||
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
|
|
||||||
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
|
|
||||||
import DashboardMonthlyRevenueGraph, {
|
|
||||||
DashboardMonthlyRevenueGraphGql
|
|
||||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
|
||||||
import DashboardProjectedMonthlySales, {
|
|
||||||
DashboardProjectedMonthlySalesGql
|
|
||||||
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
|
||||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
|
||||||
import DashboardTotalProductionHours, {
|
|
||||||
DashboardTotalProductionHoursGql
|
|
||||||
} from "../dashboard-components/total-production-hours/total-production-hours.component";
|
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
//Combination of the following:
|
|
||||||
// /node_modules/react-grid-layout/css/styles.css
|
|
||||||
// /node_modules/react-resizable/css/styles.css
|
|
||||||
import DashboardScheduledInToday, {
|
|
||||||
DashboardScheduledInTodayGql
|
|
||||||
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
|
|
||||||
import DashboardScheduledOutToday, {
|
|
||||||
DashboardScheduledOutTodayGql
|
|
||||||
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
|
|
||||||
import JobLifecycleDashboardComponent, {
|
|
||||||
JobLifecycleDashboardGQL
|
|
||||||
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component";
|
|
||||||
import "./dashboard-grid.styles.scss";
|
|
||||||
import { GenerateDashboardData } from "./dashboard-grid.utils";
|
import { GenerateDashboardData } from "./dashboard-grid.utils";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import componentList from "./componentList.js";
|
||||||
|
import createDashboardQuery from "./createDashboardQuery.js";
|
||||||
|
|
||||||
|
import "./dashboard-grid.styles.scss";
|
||||||
|
|
||||||
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
@@ -54,6 +27,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
@@ -85,19 +59,21 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
layout: { ...state, layout, layouts }
|
layout: { ...state, layout, layouts }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!!result.errors) {
|
|
||||||
notification["error"]({
|
if (!isEmpty(result?.errors)) {
|
||||||
|
notification.error({
|
||||||
message: t("dashboard.errors.updatinglayout", {
|
message: t("dashboard.errors.updatinglayout", {
|
||||||
message: JSON.stringify(result.errors)
|
message: JSON.stringify(result.errors)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveComponent = (key) => {
|
const handleRemoveComponent = (key) => {
|
||||||
logImEXEvent("dashboard_remove_component", { name: key });
|
logImEXEvent("dashboard_remove_component", { name: key });
|
||||||
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
||||||
|
|
||||||
const items = _.cloneDeep(state.items);
|
const items = cloneDeep(state.items);
|
||||||
|
|
||||||
items.splice(idxToRemove, 1);
|
items.splice(idxToRemove, 1);
|
||||||
setState({ ...state, items });
|
setState({ ...state, items });
|
||||||
@@ -120,7 +96,8 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const dashboarddata = React.useMemo(() => GenerateDashboardData(data), [data]);
|
const dashboardData = useMemo(() => GenerateDashboardData(data), [data]);
|
||||||
|
|
||||||
const existingLayoutKeys = state.items.map((i) => i.i);
|
const existingLayoutKeys = state.items.map((i) => i.i);
|
||||||
|
|
||||||
const menuItems = Object.keys(componentList).map((key) => ({
|
const menuItems = Object.keys(componentList).map((key) => ({
|
||||||
@@ -156,7 +133,6 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
width="100%"
|
width="100%"
|
||||||
layouts={state.layouts}
|
layouts={state.layouts}
|
||||||
onLayoutChange={handleLayoutChange}
|
onLayoutChange={handleLayoutChange}
|
||||||
// onBreakpointChange={onBreakpointChange}
|
|
||||||
>
|
>
|
||||||
{state.items.map((item, index) => {
|
{state.items.map((item, index) => {
|
||||||
const TheComponent = componentList[item.i].component;
|
const TheComponent = componentList[item.i].component;
|
||||||
@@ -182,7 +158,7 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
}}
|
}}
|
||||||
onClick={() => handleRemoveComponent(item.i)}
|
onClick={() => handleRemoveComponent(item.i)}
|
||||||
/>
|
/>
|
||||||
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboarddata} />
|
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboardData} />
|
||||||
</LoadingSkeleton>
|
</LoadingSkeleton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -193,189 +169,3 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(DashboardGridComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(DashboardGridComponent);
|
||||||
|
|
||||||
const componentList = {
|
|
||||||
ProductionDollars: {
|
|
||||||
label: i18next.t("dashboard.titles.productiondollars"),
|
|
||||||
component: DashboardTotalProductionDollars,
|
|
||||||
gqlFragment: null,
|
|
||||||
w: 1,
|
|
||||||
h: 1,
|
|
||||||
minW: 2,
|
|
||||||
minH: 1
|
|
||||||
},
|
|
||||||
ProductionHours: {
|
|
||||||
label: i18next.t("dashboard.titles.productionhours"),
|
|
||||||
component: DashboardTotalProductionHours,
|
|
||||||
gqlFragment: DashboardTotalProductionHoursGql,
|
|
||||||
w: 3,
|
|
||||||
h: 1,
|
|
||||||
minW: 3,
|
|
||||||
minH: 1
|
|
||||||
},
|
|
||||||
ProjectedMonthlySales: {
|
|
||||||
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
|
||||||
component: DashboardProjectedMonthlySales,
|
|
||||||
gqlFragment: DashboardProjectedMonthlySalesGql,
|
|
||||||
w: 2,
|
|
||||||
h: 1,
|
|
||||||
minW: 2,
|
|
||||||
minH: 1
|
|
||||||
},
|
|
||||||
MonthlyRevenueGraph: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
|
||||||
component: DashboardMonthlyRevenueGraph,
|
|
||||||
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
|
||||||
w: 4,
|
|
||||||
h: 2,
|
|
||||||
minW: 4,
|
|
||||||
minH: 2
|
|
||||||
},
|
|
||||||
MonthlyJobCosting: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
|
||||||
component: DashboardMonthlyJobCosting,
|
|
||||||
gqlFragment: null,
|
|
||||||
minW: 6,
|
|
||||||
minH: 3,
|
|
||||||
w: 6,
|
|
||||||
h: 3
|
|
||||||
},
|
|
||||||
MonthlyPartsSales: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlypartssales"),
|
|
||||||
component: DashboardMonthlyPartsSales,
|
|
||||||
gqlFragment: null,
|
|
||||||
minW: 2,
|
|
||||||
minH: 2,
|
|
||||||
w: 2,
|
|
||||||
h: 2
|
|
||||||
},
|
|
||||||
MonthlyLaborSales: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
|
||||||
component: DashboardMonthlyLaborSales,
|
|
||||||
gqlFragment: null,
|
|
||||||
minW: 2,
|
|
||||||
minH: 2,
|
|
||||||
w: 2,
|
|
||||||
h: 2
|
|
||||||
},
|
|
||||||
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
|
|
||||||
MonthlyEmployeeEfficency: {
|
|
||||||
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
|
||||||
component: DashboardMonthlyEmployeeEfficiency,
|
|
||||||
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
|
||||||
minW: 2,
|
|
||||||
minH: 2,
|
|
||||||
w: 2,
|
|
||||||
h: 2
|
|
||||||
},
|
|
||||||
ScheduleInToday: {
|
|
||||||
label: i18next.t("dashboard.titles.scheduledintoday"),
|
|
||||||
component: DashboardScheduledInToday,
|
|
||||||
gqlFragment: DashboardScheduledInTodayGql,
|
|
||||||
minW: 6,
|
|
||||||
minH: 2,
|
|
||||||
w: 10,
|
|
||||||
h: 3
|
|
||||||
},
|
|
||||||
ScheduleOutToday: {
|
|
||||||
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
|
||||||
component: DashboardScheduledOutToday,
|
|
||||||
gqlFragment: DashboardScheduledOutTodayGql,
|
|
||||||
minW: 6,
|
|
||||||
minH: 2,
|
|
||||||
w: 10,
|
|
||||||
h: 3
|
|
||||||
},
|
|
||||||
JobLifecycle: {
|
|
||||||
label: i18next.t("dashboard.titles.joblifecycle"),
|
|
||||||
component: JobLifecycleDashboardComponent,
|
|
||||||
gqlFragment: JobLifecycleDashboardGQL,
|
|
||||||
minW: 6,
|
|
||||||
minH: 3,
|
|
||||||
w: 6,
|
|
||||||
h: 3
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createDashboardQuery = (state) => {
|
|
||||||
const componentBasedAdditions =
|
|
||||||
state &&
|
|
||||||
Array.isArray(state.layout) &&
|
|
||||||
state.layout.map((item, index) => componentList[item.i].gqlFragment || "").join("");
|
|
||||||
return gql`
|
|
||||||
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
|
||||||
monthly_sales: jobs(where: {_and: [
|
|
||||||
{ voided: {_eq: false}},
|
|
||||||
{date_invoiced: {_gte: "${dayjs()
|
|
||||||
.startOf("month")
|
|
||||||
.startOf("day")
|
|
||||||
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs()
|
|
||||||
.endOf("month")
|
|
||||||
.endOf("day")
|
|
||||||
.toISOString()}"}}]}) {
|
|
||||||
id
|
|
||||||
ro_number
|
|
||||||
date_invoiced
|
|
||||||
job_totals
|
|
||||||
rate_la1
|
|
||||||
rate_la2
|
|
||||||
rate_la3
|
|
||||||
rate_la4
|
|
||||||
rate_laa
|
|
||||||
rate_lab
|
|
||||||
rate_lad
|
|
||||||
rate_lae
|
|
||||||
rate_laf
|
|
||||||
rate_lag
|
|
||||||
rate_lam
|
|
||||||
rate_lar
|
|
||||||
rate_las
|
|
||||||
rate_lau
|
|
||||||
rate_ma2s
|
|
||||||
rate_ma2t
|
|
||||||
rate_ma3s
|
|
||||||
rate_mabl
|
|
||||||
rate_macs
|
|
||||||
rate_mahw
|
|
||||||
rate_mapa
|
|
||||||
rate_mash
|
|
||||||
rate_matd
|
|
||||||
joblines(where: { removed: { _eq: false } }) {
|
|
||||||
id
|
|
||||||
mod_lbr_ty
|
|
||||||
mod_lb_hrs
|
|
||||||
act_price
|
|
||||||
part_qty
|
|
||||||
part_type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
|
||||||
id
|
|
||||||
ro_number
|
|
||||||
ins_co_nm
|
|
||||||
job_totals
|
|
||||||
joblines(where: { removed: { _eq: false } }) {
|
|
||||||
id
|
|
||||||
mod_lbr_ty
|
|
||||||
mod_lb_hrs
|
|
||||||
act_price
|
|
||||||
part_qty
|
|
||||||
part_type
|
|
||||||
}
|
|
||||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
|
||||||
aggregate {
|
|
||||||
sum {
|
|
||||||
mod_lb_hrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
|
||||||
aggregate {
|
|
||||||
sum {
|
|
||||||
mod_lb_hrs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
|
import { Progress, Result, Space, Upload } from "antd";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
|
import formatBytes from "../../utils/formatbytes";
|
||||||
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
|
import { handleUpload } from "./documents-upload-imgproxy.utility.js";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
export function DocumentsUploadImgproxyComponent({
|
||||||
|
children,
|
||||||
|
currentUser,
|
||||||
|
bodyshop,
|
||||||
|
jobId,
|
||||||
|
tagsArray,
|
||||||
|
billId,
|
||||||
|
callbackAfterUpload,
|
||||||
|
totalSize,
|
||||||
|
ignoreSizeLimit = false
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [fileList, setFileList] = useState([]);
|
||||||
|
const notification = useNotification();
|
||||||
|
|
||||||
|
const pct = useMemo(() => {
|
||||||
|
return parseInt((totalSize / ((bodyshop && bodyshop.jobsizelimit) || 1)) * 100);
|
||||||
|
}, [bodyshop, totalSize]);
|
||||||
|
|
||||||
|
if (pct > 100 && !ignoreSizeLimit)
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="error"
|
||||||
|
title={t("documents.labels.storageexceeded_title")}
|
||||||
|
subTitle={t("documents.labels.storageexceeded")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDone = (uid) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setFileList((fileList) => fileList.filter((x) => x.uid !== uid));
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Upload.Dragger
|
||||||
|
multiple={true}
|
||||||
|
fileList={fileList}
|
||||||
|
disabled={!hasMediaAccess}
|
||||||
|
onChange={(f) => {
|
||||||
|
if (f.event && f.event.percent === 100) handleDone(f.file.uid);
|
||||||
|
setFileList(f.fileList);
|
||||||
|
}}
|
||||||
|
beforeUpload={(file, fileList) => {
|
||||||
|
if (ignoreSizeLimit) return true;
|
||||||
|
const newFiles = fileList.reduce((acc, val) => acc + val.size, 0);
|
||||||
|
const shouldStopUpload = (totalSize + newFiles) / ((bodyshop && bodyshop.jobsizelimit) || 1) >= 1;
|
||||||
|
|
||||||
|
//Check to see if old files plus newly uploaded ones will be too much.
|
||||||
|
if (shouldStopUpload) {
|
||||||
|
notification.error({
|
||||||
|
key: "cannotuploaddocuments",
|
||||||
|
message: t("documents.labels.upload_limitexceeded_title"),
|
||||||
|
description: t("documents.labels.upload_limitexceeded")
|
||||||
|
});
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
customRequest={(ev) =>
|
||||||
|
handleUpload(
|
||||||
|
ev,
|
||||||
|
{
|
||||||
|
bodyshop: bodyshop,
|
||||||
|
uploaded_by: currentUser.email,
|
||||||
|
jobId: jobId,
|
||||||
|
billId: billId,
|
||||||
|
tagsArray: tagsArray,
|
||||||
|
callback: callbackAfterUpload
|
||||||
|
},
|
||||||
|
notification
|
||||||
|
)
|
||||||
|
}
|
||||||
|
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
|
||||||
|
// showUploadList={false}
|
||||||
|
>
|
||||||
|
{children || (
|
||||||
|
<>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<UploadOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">
|
||||||
|
<LockWrapperComponent featureName="media">{t("documents.labels.dragtoupload")}</LockWrapperComponent>
|
||||||
|
</p>
|
||||||
|
{!ignoreSizeLimit && (
|
||||||
|
<Space wrap className="ant-upload-text">
|
||||||
|
<Progress type="dashboard" percent={pct} size="small" />
|
||||||
|
<span>
|
||||||
|
{t("documents.labels.usage", {
|
||||||
|
percent: pct,
|
||||||
|
used: formatBytes(totalSize),
|
||||||
|
total: formatBytes(bodyshop && bodyshop.jobsizelimit)
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Upload.Dragger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(DocumentsUploadImgproxyComponent);
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import exifr from "exifr";
|
||||||
|
import i18n from "i18next";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||||
|
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
||||||
|
import client from "../../utils/GraphQLClient";
|
||||||
|
|
||||||
|
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||||
|
|
||||||
|
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||||
|
var cleanAxios = axios.create();
|
||||||
|
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
||||||
|
|
||||||
|
export const handleUpload = (ev, context, notification) => {
|
||||||
|
logImEXEvent("document_upload", { filetype: ev.file?.type });
|
||||||
|
|
||||||
|
const { onError, onSuccess, onProgress } = ev;
|
||||||
|
const { bodyshop, jobId } = context;
|
||||||
|
|
||||||
|
const fileName = ev.file?.name || ev.filename;
|
||||||
|
|
||||||
|
let extension = fileName.split(".").pop();
|
||||||
|
let key = `${bodyshop.id}/${jobId}/${replaceAccents(fileName).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}`;
|
||||||
|
|
||||||
|
uploadToS3(key, extension, ev.file.type, ev.file, onError, onSuccess, onProgress, context, notification).catch(
|
||||||
|
(error) => {
|
||||||
|
console.error("Error uploading file to S3", error);
|
||||||
|
notification.error({
|
||||||
|
message: i18n.t("documents.errors.insert", {
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//Handles only 1 file at a time.
|
||||||
|
export const uploadToS3 = async (
|
||||||
|
key,
|
||||||
|
extension,
|
||||||
|
fileType,
|
||||||
|
file,
|
||||||
|
onError,
|
||||||
|
onSuccess,
|
||||||
|
onProgress,
|
||||||
|
context,
|
||||||
|
notification
|
||||||
|
) => {
|
||||||
|
const { bodyshop, jobId, billId, uploaded_by, callback } = context;
|
||||||
|
|
||||||
|
//Get the signed url allowing us to PUT to S3.
|
||||||
|
const signedURLResponse = await axios.post("/media/imgproxy/sign", {
|
||||||
|
filenames: [key],
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
jobid: jobId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (signedURLResponse.status !== 200) {
|
||||||
|
if (onError) onError(signedURLResponse.statusText);
|
||||||
|
notification.error({
|
||||||
|
message: i18n.t("documents.errors.getpresignurl", {
|
||||||
|
message: signedURLResponse.statusText
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Key should be same as we provided to maintain backwards compatibility.
|
||||||
|
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key } = signedURLResponse.data.signedUrls[0];
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
onUploadProgress: (e) => {
|
||||||
|
if (onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const s3UploadResponse = await cleanAxios.put(preSignedUploadUrlToS3, file, options);
|
||||||
|
//Insert the document with the matching key.
|
||||||
|
let takenat;
|
||||||
|
if (fileType.includes("image")) {
|
||||||
|
try {
|
||||||
|
const exif = await exifr.parse(file);
|
||||||
|
takenat = exif && exif.DateTimeOriginal;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Unable to parse image file for EXIF Data", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentInsert = await client.mutate({
|
||||||
|
mutation: INSERT_NEW_DOCUMENT,
|
||||||
|
variables: {
|
||||||
|
docInput: [
|
||||||
|
{
|
||||||
|
...(jobId ? { jobid: jobId } : {}),
|
||||||
|
...(billId ? { billid: billId } : {}),
|
||||||
|
uploaded_by: uploaded_by,
|
||||||
|
key: s3Key,
|
||||||
|
type: fileType,
|
||||||
|
extension: s3UploadResponse.data.format || extension,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
size: s3UploadResponse.data.bytes || file.size, //Leftover from Cloudinary. We don't do any optimization on upload, so it will always be file.size.
|
||||||
|
takenat
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!documentInsert.errors) {
|
||||||
|
if (onSuccess)
|
||||||
|
onSuccess({
|
||||||
|
uid: documentInsert.data.insert_documents.returning[0].id,
|
||||||
|
name: documentInsert.data.insert_documents.returning[0].name,
|
||||||
|
status: "done",
|
||||||
|
key: documentInsert.data.insert_documents.returning[0].key
|
||||||
|
});
|
||||||
|
notification.success({
|
||||||
|
key: "docuploadsuccess",
|
||||||
|
message: i18n.t("documents.successes.insert")
|
||||||
|
});
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (onError) onError(JSON.stringify(documentInsert.errors));
|
||||||
|
notification.error({
|
||||||
|
message: i18n.t("documents.errors.insert", {
|
||||||
|
message: JSON.stringify(documentInsert.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error uploading file to S3", error.message, error.stack);
|
||||||
|
notification.error({
|
||||||
|
message: i18n.t("documents.errors.insert", {
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (onError) onError(JSON.stringify(error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function replaceAccents(str) {
|
||||||
|
// Verifies if the String has accents and replace them
|
||||||
|
if (str.search(/[\xC0-\xFF]/g) > -1) {
|
||||||
|
str = str
|
||||||
|
.replace(/[\xC0-\xC5]/g, "A")
|
||||||
|
.replace(/[\xC6]/g, "AE")
|
||||||
|
.replace(/[\xC7]/g, "C")
|
||||||
|
.replace(/[\xC8-\xCB]/g, "E")
|
||||||
|
.replace(/[\xCC-\xCF]/g, "I")
|
||||||
|
.replace(/[\xD0]/g, "D")
|
||||||
|
.replace(/[\xD1]/g, "N")
|
||||||
|
.replace(/[\xD2-\xD6\xD8]/g, "O")
|
||||||
|
.replace(/[\xD9-\xDC]/g, "U")
|
||||||
|
.replace(/[\xDD]/g, "Y")
|
||||||
|
.replace(/[\xDE]/g, "P")
|
||||||
|
.replace(/[\xE0-\xE5]/g, "a")
|
||||||
|
.replace(/[\xE6]/g, "ae")
|
||||||
|
.replace(/[\xE7]/g, "c")
|
||||||
|
.replace(/[\xE8-\xEB]/g, "e")
|
||||||
|
.replace(/[\xEC-\xEF]/g, "i")
|
||||||
|
.replace(/[\xF1]/g, "n")
|
||||||
|
.replace(/[\xF2-\xF6\xF8]/g, "o")
|
||||||
|
.replace(/[\xF9-\xFC]/g, "u")
|
||||||
|
.replace(/[\xFE]/g, "p")
|
||||||
|
.replace(/[\xFD\xFF]/g, "y");
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import AlertComponent from "../alert/alert.component";
|
|||||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||||
import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -23,6 +25,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(EmailDocumentsCompon
|
|||||||
|
|
||||||
export function EmailDocumentsComponent({ emailConfig, form, selectedMediaState, bodyshop }) {
|
export function EmailDocumentsComponent({ emailConfig, form, selectedMediaState, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
treatments: { Imgproxy }
|
||||||
|
} = useSplitTreatments({
|
||||||
|
attributes: {},
|
||||||
|
names: ["Imgproxy"],
|
||||||
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
const [selectedMedia, setSelectedMedia] = selectedMediaState;
|
const [selectedMedia, setSelectedMedia] = selectedMediaState;
|
||||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||||
@@ -46,17 +55,37 @@ export function EmailDocumentsComponent({ emailConfig, form, selectedMediaState,
|
|||||||
10485760 - new Blob([form.getFieldValue("html")]).size ? (
|
10485760 - new Blob([form.getFieldValue("html")]).size ? (
|
||||||
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
|
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!bodyshop.uselocalmediaserver && data && (
|
|
||||||
<JobDocumentsGalleryExternal
|
{Imgproxy.treatment === "on" ? (
|
||||||
data={data ? data.documents : []}
|
<>
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
{!bodyshop.uselocalmediaserver && data && (
|
||||||
/>
|
<JobsDocumentImgproxyGalleryExternal
|
||||||
)}
|
jobId={emailConfig.jobid}
|
||||||
{bodyshop.uselocalmediaserver && (
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
<JobsDocumentsLocalGalleryExternalComponent
|
/>
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
)}
|
||||||
jobId={emailConfig.jobid}
|
{bodyshop.uselocalmediaserver && (
|
||||||
/>
|
<JobsDocumentsLocalGalleryExternalComponent
|
||||||
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
|
jobId={emailConfig.jobid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!bodyshop.uselocalmediaserver && data && (
|
||||||
|
<JobDocumentsGalleryExternal
|
||||||
|
data={data ? data.documents : []}
|
||||||
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{bodyshop.uselocalmediaserver && (
|
||||||
|
<JobsDocumentsLocalGalleryExternalComponent
|
||||||
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
|
jobId={emailConfig.jobid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { PrinterFilled } from "@ant-design/icons";
|
import { PauseCircleOutlined, PlayCircleOutlined, PrinterFilled } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Card, Col, Divider, Drawer, Grid, Row, Space } from "antd";
|
import { Button, Card, Col, Divider, Drawer, Grid, Row, Space } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobSyncButton from "../job-sync-button/job-sync-button.component";
|
import JobSyncButton from "../job-sync-button/job-sync-button.component";
|
||||||
|
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||||
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
|
import JobDetailCardsDamageComponent from "./job-detail-cards.damage.component";
|
||||||
@@ -21,15 +25,21 @@ import JobDetailCardsInsuranceComponent from "./job-detail-cards.insurance.compo
|
|||||||
import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
|
import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
|
||||||
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
||||||
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
||||||
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" }))
|
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const span = {
|
const span = {
|
||||||
@@ -38,8 +48,9 @@ const span = {
|
|||||||
xxl: { span: 8 }
|
xxl: { span: 8 }
|
||||||
};
|
};
|
||||||
|
|
||||||
export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTrail }) {
|
||||||
const { scenarioNotificationsOn } = useSocket();
|
const { scenarioNotificationsOn } = useSocket();
|
||||||
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
.filter((screen) => !!screen[1])
|
.filter((screen) => !!screen[1])
|
||||||
.slice(-1)[0];
|
.slice(-1)[0];
|
||||||
@@ -91,7 +102,29 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<JobSyncButton job={data.jobs_by_pk} />
|
<JobSyncButton job={data.jobs_by_pk} />
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
logImEXEvent("production_toggle_alert");
|
||||||
|
updateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: data.jobs_by_pk.id,
|
||||||
|
job: {
|
||||||
|
suspended: !data.jobs_by_pk.suspended
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: data.jobs_by_pk.id,
|
||||||
|
operation: AuditTrailMapping.jobsuspend(
|
||||||
|
data.jobs_by_pk.suspended ? !data.jobs_by_pk.suspended : true
|
||||||
|
),
|
||||||
|
type: "jobsuspend"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={data.jobs_by_pk.suspended ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
||||||
|
>
|
||||||
|
{data.jobs_by_pk.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPrintCenterContext({
|
setPrintCenterContext({
|
||||||
@@ -103,8 +136,8 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
icon={<PrinterFilled />}
|
||||||
>
|
>
|
||||||
<PrinterFilled />
|
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>
|
</Button>
|
||||||
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
|
<Link to={`/manage/jobs/${data.jobs_by_pk.id}?tab=repairdata`}>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import CABCpvrtCalculator from "../ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component";
|
import CABCpvrtCalculator from "../ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
||||||
@@ -14,9 +15,8 @@ import JobsDetailRatesLabor from "./jobs-detail-rates.labor.component";
|
|||||||
import JobsDetailRatesMaterials from "./jobs-detail-rates.materials.component";
|
import JobsDetailRatesMaterials from "./jobs-detail-rates.materials.component";
|
||||||
import JobsDetailRatesOther from "./jobs-detail-rates.other.component";
|
import JobsDetailRatesOther from "./jobs-detail-rates.other.component";
|
||||||
import JobsDetailRatesParts from "./jobs-detail-rates.parts.component";
|
import JobsDetailRatesParts from "./jobs-detail-rates.parts.component";
|
||||||
import JobsDetailRatesTaxes from "./jobs-detail-rates.taxes.component";
|
|
||||||
import JobsDetailRatesProfileOVerride from "./jobs-detail-rates.profile-override.component";
|
import JobsDetailRatesProfileOVerride from "./jobs-detail-rates.profile-override.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import JobsDetailRatesTaxes from "./jobs-detail-rates.taxes.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -66,14 +66,48 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
|
|||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
<Form.Item label={t("jobs.fields.auto_add_ats")} name="auto_add_ats" valuePropName="checked">
|
<Form.Item label={t("jobs.fields.auto_add_ats")} name="auto_add_ats" valuePropName="checked">
|
||||||
<Switch disabled={jobRO} />
|
<Switch
|
||||||
|
disabled={jobRO}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
form.setFieldsValue({ flat_rate_ats: false });
|
||||||
|
form.setFieldsValue({ rate_ats: form.getFieldValue('rate_ats') || bodyshop.shoprates.rate_ats });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}>
|
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}>
|
||||||
{() => {
|
{() => {
|
||||||
if (form.getFieldValue("auto_add_ats"))
|
if (form.getFieldValue("auto_add_ats"))
|
||||||
return (
|
return (
|
||||||
<Form.Item label={t("jobs.fields.rate_ats")} name="rate_ats" initialValue={bodyshop.shoprates.rate_atp}>
|
<Form.Item label={t("jobs.fields.rate_ats")} name="rate_ats">
|
||||||
|
<CurrencyInput disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.flat_rate_ats")} name="flat_rate_ats" valuePropName="checked">
|
||||||
|
<Switch
|
||||||
|
disabled={jobRO}
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
form.setFieldsValue({ auto_add_ats: false });
|
||||||
|
form.setFieldsValue({ rate_ats_flat: form.getFieldValue('rate_ats_flat') || bodyshop.shoprates.rate_ats_flat });
|
||||||
|
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.flat_rate_ats !== cur.flat_rate_ats}>
|
||||||
|
{() => {
|
||||||
|
if (form.getFieldValue("flat_rate_ats"))
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.rate_ats_flat")}
|
||||||
|
name="rate_ats_flat"
|
||||||
|
>
|
||||||
<CurrencyInput disabled={jobRO} />
|
<CurrencyInput disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsDownloadButton);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsDownloadButton);
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
export function JobsDocumentsDownloadButton({ bodyshop, galleryImages, identifier }) {
|
export function JobsDocumentsDownloadButton({ bodyshop, galleryImages, identifier }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [download, setDownload] = useState(null);
|
const [download, setDownload] = useState(null);
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsGalleryReassign);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsGalleryReassign);
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages, callback }) {
|
export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages, callback }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
|
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Col, Row, Space } from "antd";
|
import { Button, Card, Col, Row, Space } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Gallery } from "react-grid-gallery";
|
import { Gallery } from "react-grid-gallery";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Lightbox from "react-image-lightbox";
|
||||||
|
import "react-image-lightbox/style.css";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
|
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
|
||||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||||
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
||||||
import JobsDocumentsDownloadButton from "./jobs-document-gallery.download.component";
|
import JobsDocumentsDownloadButton from "./jobs-document-gallery.download.component";
|
||||||
import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.component";
|
import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.component";
|
||||||
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
|
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
|
||||||
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-gallery.selectall.component";
|
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-gallery.selectall.component";
|
||||||
|
|
||||||
import Lightbox from "react-image-lightbox";
|
|
||||||
import "react-image-lightbox/style.css";
|
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
function JobsDocumentsComponent({
|
function JobsDocumentsComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
data,
|
data,
|
||||||
@@ -114,6 +119,7 @@ function JobsDocumentsComponent({
|
|||||||
);
|
);
|
||||||
setgalleryImages(documents);
|
setgalleryImages(documents);
|
||||||
}, [data, setgalleryImages, t]);
|
}, [data, setgalleryImages, t]);
|
||||||
|
|
||||||
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
||||||
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
|
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
|
||||||
return (
|
return (
|
||||||
@@ -137,7 +143,6 @@ function JobsDocumentsComponent({
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Card>
|
<Card>
|
||||||
<DocumentsUploadComponent
|
<DocumentsUploadComponent
|
||||||
|
|||||||
@@ -2,29 +2,77 @@ import { useQuery } from "@apollo/client";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import JobDocumentsImgProxy from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import JobDocuments from "./jobs-documents-gallery.component";
|
import JobDocuments from "./jobs-documents-gallery.component";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsContainer);
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
|
export function JobsDocumentsContainer({
|
||||||
|
jobId,
|
||||||
|
billId,
|
||||||
|
documentsList,
|
||||||
|
billsCallback,
|
||||||
|
refetchOverride,
|
||||||
|
ignoreSizeLimit,
|
||||||
|
bodyshop
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
treatments: { Imgproxy }
|
||||||
|
} = useSplitTreatments({
|
||||||
|
attributes: {},
|
||||||
|
names: ["Imgproxy"],
|
||||||
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
export default function JobsDocumentsContainer({ jobId, billId, documentsList, billsCallback }) {
|
|
||||||
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||||
variables: { jobId: jobId },
|
variables: { jobId: jobId },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: !!billId
|
skip: Imgproxy.treatment === "on" || !!billId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
if (error) return <AlertComponent type="error" message={error.message} />;
|
if (error) return <AlertComponent type="error" message={error.message} />;
|
||||||
|
|
||||||
return (
|
if (Imgproxy.treatment === "on") {
|
||||||
<JobDocuments
|
return (
|
||||||
data={(data && data.documents) || documentsList || []}
|
<JobDocumentsImgProxy
|
||||||
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
data={(data && data.documents) || documentsList || []}
|
||||||
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
||||||
billId={billId}
|
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
||||||
jobId={jobId}
|
billId={billId}
|
||||||
refetch={refetch}
|
jobId={jobId}
|
||||||
billsCallback={billsCallback}
|
refetch={refetchOverride || refetch}
|
||||||
/>
|
billsCallback={billsCallback}
|
||||||
);
|
ignoreSizeLimit={ignoreSizeLimit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<JobDocuments
|
||||||
|
data={(data && data.documents) || documentsList || []}
|
||||||
|
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
||||||
|
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
||||||
|
billId={billId}
|
||||||
|
jobId={jobId}
|
||||||
|
refetch={refetchOverride || refetch}
|
||||||
|
billsCallback={billsCallback}
|
||||||
|
ignoreSizeLimit={ignoreSizeLimit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,13 @@ import React, { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
export default function JobsDocumentsDeleteButton({ galleryImages, deletionCallback }) {
|
export default function JobsDocumentsDeleteButton({ galleryImages, deletionCallback }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { Gallery } from "react-grid-gallery";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
||||||
|
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
function JobsDocumentGalleryExternal({
|
function JobsDocumentGalleryExternal({
|
||||||
data,
|
data,
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ import { Button, Space } from "antd";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Imgproxy code. This code will be removed once the imgproxy migration is completed.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
|
|
||||||
export default function JobsDocumentsGallerySelectAllComponent({ galleryImages, setGalleryImages }) {
|
export default function JobsDocumentsGallerySelectAllComponent({ galleryImages, setGalleryImages }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { Button, Space } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import cleanAxios from "../../utils/CleanAxios";
|
||||||
|
import formatBytes from "../../utils/formatbytes";
|
||||||
|
//import yauzl from "yauzl";
|
||||||
|
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyDownloadButton);
|
||||||
|
|
||||||
|
export function JobsDocumentsImgproxyDownloadButton({ bodyshop, galleryImages, identifier }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [download, setDownload] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const imagesToDownload = [
|
||||||
|
...galleryImages.images.filter((image) => image.isSelected),
|
||||||
|
...galleryImages.other.filter((image) => image.isSelected)
|
||||||
|
];
|
||||||
|
|
||||||
|
function downloadProgress(progressEvent) {
|
||||||
|
setDownload((currentDownloadState) => {
|
||||||
|
return {
|
||||||
|
downloaded: progressEvent.loaded || 0,
|
||||||
|
speed: (progressEvent.loaded || 0) - ((currentDownloadState && currentDownloadState.downloaded) || 0)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function standardMediaDownload(bufferData) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${identifier || "documents"}.zip`;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
const handleDownload = async () => {
|
||||||
|
logImEXEvent("jobs_documents_download");
|
||||||
|
setLoading(true);
|
||||||
|
const zipUrl = await axios({
|
||||||
|
url: "/media/imgproxy/download",
|
||||||
|
method: "POST",
|
||||||
|
data: { documentids: imagesToDownload.map((_) => _.id) }
|
||||||
|
});
|
||||||
|
|
||||||
|
const theDownloadedZip = await cleanAxios({
|
||||||
|
url: zipUrl.data.url,
|
||||||
|
method: "GET",
|
||||||
|
responseType: "arraybuffer",
|
||||||
|
onDownloadProgress: downloadProgress
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
setDownload(null);
|
||||||
|
|
||||||
|
standardMediaDownload(theDownloadedZip.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button loading={download || loading} disabled={imagesToDownload.length < 1} onClick={handleDownload}>
|
||||||
|
<Space>
|
||||||
|
<span>{t("documents.actions.download")}</span>
|
||||||
|
{download && <span>{`(${formatBytes(download.downloaded)} @ ${formatBytes(download.speed)} / second)`}</span>}
|
||||||
|
</Space>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { useApolloClient } from "@apollo/client";
|
||||||
|
import { Button, Form, Popover, Space } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries.js";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import JobSearchSelect from "../job-search-select/job-search-select.component.jsx";
|
||||||
|
import { isFunction } from "lodash";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyGalleryReassign);
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
|
export function JobsDocumentsImgproxyGalleryReassign({ bodyshop, galleryImages, callback }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const notification = useNotification();
|
||||||
|
|
||||||
|
const selectedImages = useMemo(() => {
|
||||||
|
return [
|
||||||
|
...galleryImages.images.filter((image) => image.isSelected),
|
||||||
|
...galleryImages.other.filter((image) => image.isSelected)
|
||||||
|
];
|
||||||
|
}, [galleryImages]);
|
||||||
|
const client = useApolloClient();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFinish = async ({ jobid }) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
//Check to see if the space remaining on the new job is sufficient. If it isn't cancel this.
|
||||||
|
const newJobData = await client.query({
|
||||||
|
query: GET_DOC_SIZE_BY_JOB,
|
||||||
|
variables: { jobId: jobid }
|
||||||
|
});
|
||||||
|
|
||||||
|
const transferedDocSizeTotal = selectedImages.reduce((acc, val) => acc + val.size, 0);
|
||||||
|
|
||||||
|
const shouldPreventTransfer =
|
||||||
|
bodyshop.jobsizelimit - newJobData.data.documents_aggregate.aggregate.sum.size < transferedDocSizeTotal;
|
||||||
|
|
||||||
|
if (shouldPreventTransfer) {
|
||||||
|
notification.error({
|
||||||
|
key: "cannotuploaddocuments",
|
||||||
|
message: t("documents.labels.reassign_limitexceeded_title"),
|
||||||
|
description: t("documents.labels.reassign_limitexceeded")
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await axios.post("/media/imgproxy/rename", {
|
||||||
|
tojobid: jobid,
|
||||||
|
documents: selectedImages.map((i) => {
|
||||||
|
//Need to check if the current key folder is null, or another job.
|
||||||
|
const currentKeys = i.key.split("/");
|
||||||
|
currentKeys[1] = jobid;
|
||||||
|
currentKeys.join("/");
|
||||||
|
return {
|
||||||
|
id: i.id,
|
||||||
|
from: i.key,
|
||||||
|
to: currentKeys.join("/"),
|
||||||
|
extension: i.extension,
|
||||||
|
type: i.type
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
//Add in confirmation & errors.
|
||||||
|
if (isFunction(callback)) callback();
|
||||||
|
|
||||||
|
if (res.errors) {
|
||||||
|
notification.error({
|
||||||
|
message: t("documents.errors.updating", {
|
||||||
|
message: JSON.stringify(res.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!res.mutationResult?.errors) {
|
||||||
|
notification.success({
|
||||||
|
message: t("documents.successes.updated")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const popContent = (
|
||||||
|
<div>
|
||||||
|
<Form onFinish={handleFinish} layout="vertical" form={form}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("documents.labels.newjobid")}
|
||||||
|
style={{ width: "20rem" }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={"jobid"}
|
||||||
|
>
|
||||||
|
<JobSearchSelect notExported={false} notInvoiced={false} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" onClick={() => form.submit()}>
|
||||||
|
{t("general.actions.submit")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setOpen(false)}>{t("general.actions.cancel")}</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover content={popContent} open={open}>
|
||||||
|
<Button disabled={selectedImages.length < 1} onClick={() => setOpen(true)} loading={loading}>
|
||||||
|
{t("documents.actions.reassign")}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Card, Col, Row, Space } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import i18n from "i18next";
|
||||||
|
import { isFunction } from "lodash";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { Gallery } from "react-grid-gallery";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Lightbox from "react-image-lightbox";
|
||||||
|
import "react-image-lightbox/style.css";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import DocumentsUploadImgproxyComponent from "../documents-upload-imgproxy/documents-upload-imgproxy.component";
|
||||||
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
|
import JobsDocumentsDownloadButton from "./jobs-document-imgproxy-gallery.download.component";
|
||||||
|
import JobsDocumentsGalleryReassign from "./jobs-document-imgproxy-gallery.reassign.component";
|
||||||
|
import JobsDocumentsDeleteButton from "./jobs-documents-imgproxy-gallery.delete.component";
|
||||||
|
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-imgproxy-gallery.selectall.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
|
function JobsDocumentsImgproxyComponent({
|
||||||
|
bodyshop,
|
||||||
|
data,
|
||||||
|
jobId,
|
||||||
|
refetch,
|
||||||
|
billId,
|
||||||
|
billsCallback,
|
||||||
|
totalSize,
|
||||||
|
downloadIdentifier,
|
||||||
|
ignoreSizeLimit
|
||||||
|
}) {
|
||||||
|
const [galleryImages, setGalleryImages] = useState({ images: [], other: [] });
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
||||||
|
|
||||||
|
const fetchThumbnails = useCallback(() => {
|
||||||
|
fetchImgproxyThumbnails({ setStateCallback: setGalleryImages, jobId });
|
||||||
|
}, [jobId, setGalleryImages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
fetchThumbnails();
|
||||||
|
}
|
||||||
|
}, [data, fetchThumbnails]);
|
||||||
|
|
||||||
|
const hasMediaAccess = HasFeatureAccess({ bodyshop, featureName: "media" });
|
||||||
|
const hasMobileAccess = HasFeatureAccess({ bodyshop, featureName: "mobile" });
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
//Handle any doc refresh.
|
||||||
|
|
||||||
|
isFunction(refetch) && refetch();
|
||||||
|
|
||||||
|
//Do the imgproxy refresh too
|
||||||
|
fetchThumbnails();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
|
<JobsDocumentsGallerySelectAllComponent galleryImages={galleryImages} setGalleryImages={setGalleryImages} />
|
||||||
|
<JobsDocumentsDownloadButton galleryImages={galleryImages} identifier={downloadIdentifier} />
|
||||||
|
<JobsDocumentsDeleteButton
|
||||||
|
galleryImages={galleryImages}
|
||||||
|
deletionCallback={billsCallback || fetchThumbnails || refetch}
|
||||||
|
/>
|
||||||
|
{!billId && (
|
||||||
|
<JobsDocumentsGalleryReassign galleryImages={galleryImages} callback={fetchThumbnails || refetch} />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
{!hasMediaAccess && (
|
||||||
|
<Col span={24}>
|
||||||
|
<Card>
|
||||||
|
<UpsellComponent disableMask upsell={upsellEnum().media.general} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
<Col span={24}>
|
||||||
|
<Card>
|
||||||
|
<DocumentsUploadImgproxyComponent
|
||||||
|
jobId={jobId}
|
||||||
|
totalSize={totalSize}
|
||||||
|
billId={billId}
|
||||||
|
callbackAfterUpload={billsCallback || fetchThumbnails || refetch}
|
||||||
|
ignoreSizeLimit={ignoreSizeLimit}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
{hasMediaAccess && !hasMobileAccess && (
|
||||||
|
<Col span={24}>
|
||||||
|
<Card>
|
||||||
|
<UpsellComponent upsell={upsellEnum().media.mobile} />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
<Col span={24}>
|
||||||
|
<Card title={t("jobs.labels.documents-images")}>
|
||||||
|
<Gallery
|
||||||
|
images={galleryImages.images}
|
||||||
|
onClick={(index, item) => {
|
||||||
|
setModalState({ open: true, index: index });
|
||||||
|
// window.open(
|
||||||
|
// item.fullsize,
|
||||||
|
// "_blank",
|
||||||
|
// "toolbar=0,location=0,menubar=0"
|
||||||
|
// );
|
||||||
|
}}
|
||||||
|
onSelect={(index, image) => {
|
||||||
|
setGalleryImages({
|
||||||
|
...galleryImages,
|
||||||
|
images: galleryImages.images.map((g, idx) =>
|
||||||
|
index === idx ? { ...g, isSelected: !g.isSelected } : g
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<Card title={t("jobs.labels.documents-other")}>
|
||||||
|
<Gallery
|
||||||
|
images={galleryImages.other}
|
||||||
|
thumbnailStyle={() => {
|
||||||
|
return {
|
||||||
|
backgroundImage: <FileExcelFilled />,
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
cursor: "pointer"
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
onClick={(index) => {
|
||||||
|
window.open(galleryImages.other[index].source, "_blank", "toolbar=0,location=0,menubar=0");
|
||||||
|
}}
|
||||||
|
onSelect={(index) => {
|
||||||
|
setGalleryImages({
|
||||||
|
...galleryImages,
|
||||||
|
other: galleryImages.other.map((g, idx) => (index === idx ? { ...g, isSelected: !g.isSelected } : g))
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
{modalState.open && (
|
||||||
|
<Lightbox
|
||||||
|
toolbarButtons={[
|
||||||
|
<EditFilled
|
||||||
|
key="edit"
|
||||||
|
onClick={() => {
|
||||||
|
const newWindow = window.open(
|
||||||
|
`${window.location.protocol}//${window.location.host}/edit?documentId=${
|
||||||
|
galleryImages.images[modalState.index].id
|
||||||
|
}`,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer"
|
||||||
|
);
|
||||||
|
if (newWindow) newWindow.opener = null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
]}
|
||||||
|
mainSrc={galleryImages.images[modalState.index].fullsize}
|
||||||
|
nextSrc={galleryImages.images[(modalState.index + 1) % galleryImages.images.length].fullsize}
|
||||||
|
prevSrc={
|
||||||
|
galleryImages.images[(modalState.index + galleryImages.images.length - 1) % galleryImages.images.length]
|
||||||
|
.fullsize
|
||||||
|
}
|
||||||
|
onCloseRequest={() => setModalState({ open: false, index: 0 })}
|
||||||
|
onMovePrevRequest={() =>
|
||||||
|
setModalState({
|
||||||
|
...modalState,
|
||||||
|
index: (modalState.index + galleryImages.images.length - 1) % galleryImages.images.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMoveNextRequest={() =>
|
||||||
|
setModalState({
|
||||||
|
...modalState,
|
||||||
|
index: (modalState.index + 1) % galleryImages.images.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDocumentsImgproxyComponent);
|
||||||
|
|
||||||
|
export const fetchImgproxyThumbnails = async ({ setStateCallback, jobId, imagesOnly }) => {
|
||||||
|
const result = await axios.post("/media/imgproxy/thumbnails", { jobid: jobId });
|
||||||
|
const documents = result.data.reduce(
|
||||||
|
(acc, value) => {
|
||||||
|
if (value.type.startsWith("image")) {
|
||||||
|
acc.images.push({
|
||||||
|
src: value.thumbnailUrl,
|
||||||
|
fullsize: value.originalUrl,
|
||||||
|
height: 225,
|
||||||
|
width: 225,
|
||||||
|
isSelected: false,
|
||||||
|
key: value.key,
|
||||||
|
extension: value.extension,
|
||||||
|
id: value.id,
|
||||||
|
type: value.type,
|
||||||
|
size: value.size,
|
||||||
|
tags: [{ value: value.type, title: value.type }]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const fileName = value.key.split("/").pop();
|
||||||
|
acc.other.push({
|
||||||
|
source: value.originalUrlViaProxyPath,
|
||||||
|
src: value.thumbnailUrl,
|
||||||
|
fullsize: value.presignedGetUrl,
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
value: fileName,
|
||||||
|
title: fileName
|
||||||
|
},
|
||||||
|
|
||||||
|
{ value: value.type, title: value.type },
|
||||||
|
...(value.bill
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
value: value.bill.vendor.name,
|
||||||
|
title: i18n.t("vendors.fields.name")
|
||||||
|
},
|
||||||
|
{ value: value.bill.date, title: i18n.t("bills.fields.date") },
|
||||||
|
{
|
||||||
|
value: value.bill.invoice_number,
|
||||||
|
title: i18n.t("bills.fields.invoice_number")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
],
|
||||||
|
height: 225,
|
||||||
|
width: 225,
|
||||||
|
isSelected: false,
|
||||||
|
extension: value.extension,
|
||||||
|
key: value.key,
|
||||||
|
id: value.id,
|
||||||
|
type: value.type,
|
||||||
|
size: value.size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ images: [], other: [] }
|
||||||
|
);
|
||||||
|
|
||||||
|
setStateCallback(imagesOnly ? documents.images : documents);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
import JobDocuments from "./jobs-documents-imgproxy-gallery.component";
|
||||||
|
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function JobsDocumentsImgproxyContainer({ jobId, billId, documentsList, billsCallback }) {
|
||||||
|
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||||
|
variables: { jobId: jobId },
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only",
|
||||||
|
skip: !!billId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner />;
|
||||||
|
if (error) return <AlertComponent type="error" message={error.message} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JobDocuments
|
||||||
|
data={(data && data.documents) || documentsList || []}
|
||||||
|
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
||||||
|
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
||||||
|
billId={billId}
|
||||||
|
jobId={jobId}
|
||||||
|
refetch={refetch}
|
||||||
|
billsCallback={billsCallback}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { QuestionCircleOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Popconfirm } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { isFunction } from "lodash";
|
||||||
|
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function JobsDocumentsImgproxyDeleteButton({ galleryImages, deletionCallback }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const notification = useNotification();
|
||||||
|
|
||||||
|
const imagesToDelete = [
|
||||||
|
...galleryImages.images.filter((image) => image.isSelected),
|
||||||
|
...galleryImages.other.filter((image) => image.isSelected)
|
||||||
|
];
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
logImEXEvent("job_documents_delete", { count: imagesToDelete.length });
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await axios.post("/media/imgproxy/delete", {
|
||||||
|
ids: imagesToDelete.map((d) => d.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data.error) {
|
||||||
|
notification.error({
|
||||||
|
message: t("documents.errors.deleting", {
|
||||||
|
error: JSON.stringify(res.data.error.response.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.success({
|
||||||
|
key: "docdeletedsuccesfully",
|
||||||
|
message: t("documents.successes.delete")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isFunction(deletionCallback)) deletionCallback();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.error({
|
||||||
|
message: t("documents.errors.deleting", {
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popconfirm
|
||||||
|
disabled={imagesToDelete.length < 1}
|
||||||
|
icon={<QuestionCircleOutlined style={{ color: "red" }} />}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
title={t("documents.labels.confirmdelete")}
|
||||||
|
okText={t("general.actions.delete")}
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
cancelText={t("general.actions.cancel")}
|
||||||
|
>
|
||||||
|
<Button disabled={imagesToDelete.length < 1} loading={loading}>
|
||||||
|
{t("documents.actions.delete")}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Gallery } from "react-grid-gallery";
|
||||||
|
import { fetchImgproxyThumbnails } from "./jobs-documents-imgproxy-gallery.component";
|
||||||
|
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
|
|
||||||
|
function JobsDocumentImgproxyGalleryExternal({ jobId, externalMediaState }) {
|
||||||
|
const [galleryImages, setgalleryImages] = externalMediaState;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (jobId) fetchImgproxyThumbnails({ setStateCallback: setgalleryImages, jobId, imagesOnly: true });
|
||||||
|
}, [jobId, setgalleryImages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="clearfix">
|
||||||
|
<Gallery
|
||||||
|
images={galleryImages}
|
||||||
|
backdropClosesModal={true}
|
||||||
|
onSelect={(index, image) => {
|
||||||
|
setgalleryImages(galleryImages.map((g, idx) => (index === idx ? { ...g, isSelected: !g.isSelected } : g)));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JobsDocumentImgproxyGalleryExternal;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Button, Space } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
/*
|
||||||
|
################################################################################################
|
||||||
|
Developer Note:
|
||||||
|
Known Technical Debt Item
|
||||||
|
Modifications to this code requires complementary changes to the Cloudinary code. Cloudinary code will be removed upon completed migration.
|
||||||
|
################################################################################################
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function JobsDocumentsImgproxyGallerySelectAllComponent({ galleryImages, setGalleryImages }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
setGalleryImages({
|
||||||
|
...galleryImages,
|
||||||
|
other: galleryImages.other.map((i) => {
|
||||||
|
return { ...i, isSelected: true };
|
||||||
|
}),
|
||||||
|
images: galleryImages.images.map((i) => {
|
||||||
|
return { ...i, isSelected: true };
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleSelectAllImages = () => {
|
||||||
|
setGalleryImages({
|
||||||
|
...galleryImages,
|
||||||
|
|
||||||
|
images: galleryImages.images.map((i) => {
|
||||||
|
return { ...i, isSelected: true };
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleSelectAllDocuments = () => {
|
||||||
|
setGalleryImages({
|
||||||
|
...galleryImages,
|
||||||
|
other: galleryImages.other.map((i) => {
|
||||||
|
return { ...i, isSelected: true };
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleDeselectAll = () => {
|
||||||
|
setGalleryImages({
|
||||||
|
...galleryImages,
|
||||||
|
other: galleryImages.other.map((i) => {
|
||||||
|
return { ...i, isSelected: false };
|
||||||
|
}),
|
||||||
|
images: galleryImages.images.map((i) => {
|
||||||
|
return { ...i, isSelected: false };
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space wrap>
|
||||||
|
<Button onClick={handleSelectAll}>{t("general.actions.selectall")}</Button>
|
||||||
|
<Button onClick={handleSelectAllImages}>{t("documents.actions.selectallimages")}</Button>
|
||||||
|
<Button onClick={handleSelectAllDocuments}>{t("documents.actions.selectallotherdocuments")}</Button>
|
||||||
|
<Button onClick={handleDeselectAll}>{t("general.actions.deselectall")}</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/* you can make up upload button and sample style by using stylesheets */
|
||||||
|
.ant-upload-select-picture-card i {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-upload-select-picture-card .ant-upload-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
||||||
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
||||||
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -32,7 +33,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleClick = ({ item }) => {
|
const handleClick = ({ item, key, keyPath }) => {
|
||||||
form.setFieldsValue({ comments: item.props.value });
|
form.setFieldsValue({ comments: item.props.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,18 +98,17 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
|||||||
<Checkbox />
|
<Checkbox />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
{!isReturn && (
|
|
||||||
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
<Form.Item name="order_type" initialValue="parts_order" label={t("parts_orders.labels.order_type")}>
|
||||||
<Radio.Group disabled={sendType === "oec"}>
|
<Radio.Group disabled={sendType === "oec"}>
|
||||||
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
<Radio value={"parts_order"}>{t("parts_orders.labels.parts_order")}</Radio>
|
||||||
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
<Radio value={"sublet"}>{t("parts_orders.labels.sublet_order")}</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Divider orientation="left">{t("parts_orders.labels.inthisorder")}</Divider>
|
<Divider orientation="left">{t("parts_orders.labels.inthisorder")}</Divider>
|
||||||
<Form.List name={["parts_order_lines", "data"]}>
|
<Form.List name={["parts_order_lines", "data"]}>
|
||||||
{(fields, { remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { PrinterFilled } from "@ant-design/icons";
|
import { PauseCircleOutlined, PlayCircleOutlined, PrinterFilled } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
|
||||||
import { Button, Descriptions, Drawer, Space } from "antd";
|
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { Button, Descriptions, Drawer, Space } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils.js";
|
||||||
|
import { QUERY_JOB_CARD_DETAILS, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
@@ -24,22 +27,29 @@ import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.p
|
|||||||
import CardTemplate from "../job-detail-cards/job-detail-cards.template.component";
|
import CardTemplate from "../job-detail-cards/job-detail-cards.template.component";
|
||||||
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
||||||
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
|
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
|
||||||
|
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
|
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
|
||||||
import JobWatcherToggleContainer from "../job-watcher-toggle/job-watcher-toggle.container.jsx";
|
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
technician: selectTechnician
|
technician: selectTechnician
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" }))
|
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListDetail);
|
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListDetail);
|
||||||
|
|
||||||
export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, technician }) {
|
export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, technician, insertAuditTrail }) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const { selected } = search;
|
const { selected } = search;
|
||||||
@@ -58,6 +68,7 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
@@ -72,6 +83,29 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{!technician ? <ProductionRemoveButton jobId={theJob.id} /> : null}
|
{!technician ? <ProductionRemoveButton jobId={theJob.id} /> : null}
|
||||||
|
{!technician && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
logImEXEvent("production_toggle_alert");
|
||||||
|
updateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: theJob.id,
|
||||||
|
job: {
|
||||||
|
suspended: !theJob.suspended
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: theJob.id,
|
||||||
|
operation: AuditTrailMapping.jobsuspend(theJob.suspended ? !theJob.suspended : true),
|
||||||
|
type: "jobsuspend"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
icon={theJob.suspended ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
||||||
|
>
|
||||||
|
{theJob.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPrintCenterContext({
|
setPrintCenterContext({
|
||||||
@@ -83,8 +117,8 @@ export function ProductionListDetail({ bodyshop, jobs, setPrintCenterContext, te
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
icon={<PrinterFilled />}
|
||||||
>
|
>
|
||||||
<PrinterFilled />
|
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>
|
</Button>
|
||||||
{!technician ? <ScoreboardAddButton job={data ? data.jobs_by_pk : {}} /> : null}
|
{!technician ? <ScoreboardAddButton job={data ? data.jobs_by_pk : {}} /> : null}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { QUERY_ALL_ASSOCIATIONS, UPDATE_ACTIVE_ASSOCIATION } from "../../graphql
|
|||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import ProfileShopsComponent from "./profile-shops.component";
|
import ProfileShopsComponent from "./profile-shops.component";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { getToken } from "firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { Button, Form, Input } from "antd";
|
import { Button, Form, Input, Space } from "antd";
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||||
@@ -10,326 +9,338 @@ export default function ShopInfoLaborRates({ form }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Form.List name={["md_labor_rates"]}>
|
<LayoutFormRow header={t("bodyshop.labels.shoprates")}>
|
||||||
{(fields, { add, remove, move }) => {
|
<Form.Item label={t("jobs.fields.rate_ats")} name={["shoprates", "rate_ats"]}>
|
||||||
return (
|
<CurrencyInput min={0} />
|
||||||
<div>
|
</Form.Item>
|
||||||
{fields.map((field, index) => (
|
<Form.Item label={t("jobs.fields.rate_ats_flat")} name={["shoprates", "rate_ats_flat"]}>
|
||||||
<Form.Item key={field.key}>
|
<CurrencyInput min={0} />
|
||||||
<LayoutFormRow>
|
</Form.Item>
|
||||||
<Form.Item
|
</LayoutFormRow>
|
||||||
label={t("jobs.fields.labor_rate_desc")}
|
<LayoutFormRow header={t("bodyshop.labels.laborrates")}>
|
||||||
key={`${index}rate_label`}
|
<Form.List name={["md_labor_rates"]}>
|
||||||
name={[field.name, "rate_label"]}
|
{(fields, { add, remove, move }) => {
|
||||||
rules={[
|
return (
|
||||||
{
|
<div>
|
||||||
required: true
|
{fields.map((field, index) => (
|
||||||
//message: t("general.validation.required"),
|
<Form.Item key={field.key}>
|
||||||
}
|
<LayoutFormRow noDivider={index === 0}>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.labor_rate_desc")}
|
||||||
<Input />
|
key={`${index}rate_label`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_label"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_laa")}
|
{
|
||||||
key={`${index}rate_laa`}
|
required: true
|
||||||
name={[field.name, "rate_laa"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<Input />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_laa")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_laa`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_laa"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lab")}
|
{
|
||||||
key={`${index}rate_lab`}
|
required: true
|
||||||
name={[field.name, "rate_lab"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lab")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lab`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lab"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lad")}
|
{
|
||||||
key={`${index}rate_lad`}
|
required: true
|
||||||
name={[field.name, "rate_lad"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lad")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lad`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lad"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lae")}
|
{
|
||||||
key={`${index}rate_lae`}
|
required: true
|
||||||
name={[field.name, "rate_lae"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lae")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lae`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lae"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_laf")}
|
{
|
||||||
key={`${index}rate_laf`}
|
required: true
|
||||||
name={[field.name, "rate_laf"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_laf")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_laf`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_laf"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lag")}
|
{
|
||||||
key={`${index}rate_lag`}
|
required: true
|
||||||
name={[field.name, "rate_lag"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lag")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lag`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lag"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lam")}
|
{
|
||||||
key={`${index}rate_lam`}
|
required: true
|
||||||
name={[field.name, "rate_lam"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lam")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lam`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lam"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_lar")}
|
{
|
||||||
key={`${index}rate_lar`}
|
required: true
|
||||||
name={[field.name, "rate_lar"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_lar")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_lar`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_lar"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_las")}
|
{
|
||||||
key={`${index}rate_las`}
|
required: true
|
||||||
name={[field.name, "rate_las"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_las")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_las`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_las"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_la1")}
|
{
|
||||||
key={`${index}rate_la1`}
|
required: true
|
||||||
name={[field.name, "rate_la1"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_la1")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_la1`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_la1"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_la2")}
|
{
|
||||||
key={`${index}rate_la2`}
|
required: true
|
||||||
name={[field.name, "rate_la2"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_la2")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_la2`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_la2"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_la3")}
|
{
|
||||||
key={`${index}rate_la3`}
|
required: true
|
||||||
name={[field.name, "rate_la3"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_la3")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_la3`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_la3"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_la4")}
|
{
|
||||||
key={`${index}rate_la4`}
|
required: true
|
||||||
name={[field.name, "rate_la4"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_la4")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_la4`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_la4"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_mash")}
|
{
|
||||||
key={`${index}rate_mash`}
|
required: true
|
||||||
name={[field.name, "rate_mash"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_mash")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_mash`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_mash"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_mapa")}
|
{
|
||||||
key={`${index}rate_mapa`}
|
required: true
|
||||||
name={[field.name, "rate_mapa"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_mapa")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_mapa`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_mapa"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_ma2s")}
|
{
|
||||||
key={`${index}rate_ma2s`}
|
required: true
|
||||||
name={[field.name, "rate_ma2s"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_ma2s")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_ma2s`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_ma2s"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_ma3s")}
|
{
|
||||||
key={`${index}rate_ma3s`}
|
required: true
|
||||||
name={[field.name, "rate_ma3s"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_ma3s")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_ma3s`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_ma3s"]}
|
||||||
{
|
rules={[
|
||||||
// <Form.Item
|
{
|
||||||
// label={t("jobs.fields.rate_mabl")}
|
required: true
|
||||||
// key={`${index}rate_mabl`}
|
//message: t("general.validation.required"),
|
||||||
// name={[field.name, "rate_mabl"]}
|
}
|
||||||
// rules={[
|
]}
|
||||||
// {
|
>
|
||||||
// required: true,
|
<CurrencyInput min={0} />
|
||||||
// //message: t("general.validation.required"),
|
</Form.Item>
|
||||||
// },
|
{
|
||||||
// ]}
|
// <Form.Item
|
||||||
// >
|
// label={t("jobs.fields.rate_mabl")}
|
||||||
// <CurrencyInput min={0} />
|
// key={`${index}rate_mabl`}
|
||||||
// </Form.Item>
|
// name={[field.name, "rate_mabl"]}
|
||||||
// <Form.Item
|
// rules={[
|
||||||
// label={t("jobs.fields.rate_macs")}
|
// {
|
||||||
// key={`${index}rate_macs`}
|
// required: true,
|
||||||
// name={[field.name, "rate_macs"]}
|
// //message: t("general.validation.required"),
|
||||||
// rules={[
|
// },
|
||||||
// {
|
// ]}
|
||||||
// required: true,
|
// >
|
||||||
// //message: t("general.validation.required"),
|
// <CurrencyInput min={0} />
|
||||||
// },
|
// </Form.Item>
|
||||||
// ]}
|
// <Form.Item
|
||||||
// >
|
// label={t("jobs.fields.rate_macs")}
|
||||||
// <CurrencyInput min={0} />
|
// key={`${index}rate_macs`}
|
||||||
// </Form.Item>
|
// name={[field.name, "rate_macs"]}
|
||||||
}
|
// rules={[
|
||||||
<Form.Item
|
// {
|
||||||
label={t("jobs.fields.rate_matd")}
|
// required: true,
|
||||||
key={`${index}rate_matd`}
|
// //message: t("general.validation.required"),
|
||||||
name={[field.name, "rate_matd"]}
|
// },
|
||||||
rules={[
|
// ]}
|
||||||
{
|
// >
|
||||||
required: true
|
// <CurrencyInput min={0} />
|
||||||
//message: t("general.validation.required"),
|
// </Form.Item>
|
||||||
}
|
}
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_matd")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_matd`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_matd"]}
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("jobs.fields.rate_mahw")}
|
{
|
||||||
key={`${index}rate_mahw`}
|
required: true
|
||||||
name={[field.name, "rate_mahw"]}
|
//message: t("general.validation.required"),
|
||||||
rules={[
|
}
|
||||||
{
|
]}
|
||||||
required: true
|
>
|
||||||
//message: t("general.validation.required"),
|
<CurrencyInput min={0} />
|
||||||
}
|
</Form.Item>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
label={t("jobs.fields.rate_mahw")}
|
||||||
<CurrencyInput min={0} />
|
key={`${index}rate_mahw`}
|
||||||
</Form.Item>
|
name={[field.name, "rate_mahw"]}
|
||||||
<DeleteFilled
|
rules={[
|
||||||
onClick={() => {
|
{
|
||||||
remove(field.name);
|
required: true
|
||||||
}}
|
//message: t("general.validation.required"),
|
||||||
/>
|
}
|
||||||
<FormListMoveArrows move={move} index={index} total={fields.rate_length} />
|
]}
|
||||||
</LayoutFormRow>
|
>
|
||||||
|
<CurrencyInput min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Space>
|
||||||
|
<DeleteFilled
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormListMoveArrows move={move} index={index} total={fields.rate_length} />
|
||||||
|
</Space>
|
||||||
|
</LayoutFormRow>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{t("bodyshop.actions.newlaborrate")}
|
||||||
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
</div>
|
||||||
<Form.Item>
|
);
|
||||||
<Button
|
}}
|
||||||
type="dashed"
|
</Form.List>
|
||||||
onClick={() => {
|
</LayoutFormRow>
|
||||||
add();
|
</>
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("bodyshop.actions.newlaborrate")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { getAnalytics, logEvent } from "firebase/analytics";
|
import { getAnalytics, logEvent } from "@firebase/analytics";
|
||||||
import { initializeApp } from "firebase/app";
|
import { initializeApp } from "@firebase/app";
|
||||||
import { getAuth, updatePassword, updateProfile } from "firebase/auth";
|
import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
|
||||||
import { getFirestore } from "firebase/firestore";
|
import { getFirestore } from "@firebase/firestore";
|
||||||
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
|
||||||
import { store } from "../redux/store";
|
import { store } from "../redux/store";
|
||||||
|
|
||||||
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||||
|
|||||||
@@ -509,6 +509,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
est_ct_ln
|
est_ct_ln
|
||||||
est_ea
|
est_ea
|
||||||
est_ph1
|
est_ph1
|
||||||
|
flat_rate_ats
|
||||||
federal_tax_rate
|
federal_tax_rate
|
||||||
id
|
id
|
||||||
inproduction
|
inproduction
|
||||||
@@ -649,6 +650,7 @@ export const GET_JOB_BY_PK = gql`
|
|||||||
policy_no
|
policy_no
|
||||||
production_vars
|
production_vars
|
||||||
rate_ats
|
rate_ats
|
||||||
|
rate_ats_flat
|
||||||
rate_la1
|
rate_la1
|
||||||
rate_la2
|
rate_la2
|
||||||
rate_la3
|
rate_la3
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default connect(mapStateToProps, null)(LandingPage);
|
|||||||
|
|
||||||
export function LandingPage({ currentUser }) {
|
export function LandingPage({ currentUser }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
console.log("Main");
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigate("/manage/jobs");
|
navigate("/manage/jobs");
|
||||||
}, [currentUser, navigate]);
|
}, [currentUser, navigate]);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import AlertComponent from "../../components/alert/alert.component";
|
import AlertComponent from "../../components/alert/alert.component";
|
||||||
import JobsDocumentsComponent from "../../components/jobs-documents-gallery/jobs-documents-gallery.component";
|
import JobsDocumentsContainer from "../../components/jobs-documents-gallery/jobs-documents-gallery.container";
|
||||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||||
import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries";
|
import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries";
|
||||||
|
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -19,10 +20,18 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export default connect(mapStateToProps, mapDispatchToProps)(TemporaryDocsComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(TemporaryDocsComponent);
|
||||||
|
|
||||||
export function TemporaryDocsComponent({ bodyshop }) {
|
export function TemporaryDocsComponent({ bodyshop }) {
|
||||||
|
const {
|
||||||
|
treatments: { Imgproxy }
|
||||||
|
} = useSplitTreatments({
|
||||||
|
attributes: {},
|
||||||
|
names: ["Imgproxy"],
|
||||||
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
|
});
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_TEMPORARY_DOCS, {
|
const { loading, error, data, refetch } = useQuery(QUERY_TEMPORARY_DOCS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: bodyshop.uselocalmediaserver
|
skip: Imgproxy.treatment === "on"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
@@ -32,12 +41,14 @@ export function TemporaryDocsComponent({ bodyshop }) {
|
|||||||
return <JobsDocumentsLocalGallery job={{ id: "temporary" }} />;
|
return <JobsDocumentsLocalGallery job={{ id: "temporary" }} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<JobsDocumentsComponent
|
<>
|
||||||
data={data ? data.documents : []}
|
<JobsDocumentsContainer
|
||||||
jobId={null}
|
documentsList={data ? data.documents : []}
|
||||||
billId={null}
|
jobId={null}
|
||||||
refetch={refetch}
|
billId={null}
|
||||||
ignoreSizeLimit
|
refetchOverride={refetch}
|
||||||
/>
|
ignoreSizeLimit
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,3 +118,8 @@ export const setCurrentEula = (eula) => ({
|
|||||||
export const acceptEula = () => ({
|
export const acceptEula = () => ({
|
||||||
type: UserActionTypes.EULA_ACCEPTED
|
type: UserActionTypes.EULA_ACCEPTED
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setImexShopId = (imexshopid) => ({
|
||||||
|
type: UserActionTypes.SET_IMEX_SHOP_ID,
|
||||||
|
payload: imexshopid
|
||||||
|
});
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
|||||||
};
|
};
|
||||||
case UserActionTypes.SET_AUTH_LEVEL:
|
case UserActionTypes.SET_AUTH_LEVEL:
|
||||||
return { ...state, authLevel: action.payload };
|
return { ...state, authLevel: action.payload };
|
||||||
|
case UserActionTypes.SET_IMEX_SHOP_ID:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
imexshopid: action.payload
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,19 @@ import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
|||||||
import * as Sentry from "@sentry/browser";
|
import * as Sentry from "@sentry/browser";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { setUserId, setUserProperties } from "firebase/analytics";
|
import { setUserId, setUserProperties } from "@firebase/analytics";
|
||||||
import {
|
import {
|
||||||
checkActionCode,
|
checkActionCode,
|
||||||
confirmPasswordReset,
|
confirmPasswordReset,
|
||||||
sendPasswordResetEmail,
|
sendPasswordResetEmail,
|
||||||
signInWithEmailAndPassword,
|
signInWithEmailAndPassword,
|
||||||
signOut
|
signOut
|
||||||
} from "firebase/auth";
|
} from "@firebase/auth";
|
||||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "firebase/firestore";
|
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||||
import { getToken } from "firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||||
import { factory } from "../../App/App.container";
|
|
||||||
import {
|
import {
|
||||||
analytics,
|
analytics,
|
||||||
auth,
|
auth,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
sendPasswordResetFailure,
|
sendPasswordResetFailure,
|
||||||
sendPasswordResetSuccess,
|
sendPasswordResetSuccess,
|
||||||
setAuthlevel,
|
setAuthlevel,
|
||||||
|
setImexShopId,
|
||||||
setInstanceConflict,
|
setInstanceConflict,
|
||||||
setInstanceId,
|
setInstanceId,
|
||||||
setLocalFingerprint,
|
setLocalFingerprint,
|
||||||
@@ -318,7 +318,8 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory.client(payload.imexshopid);
|
// Dispatch the imexshopid to Redux store
|
||||||
|
yield put(setImexShopId(payload.imexshopid));
|
||||||
|
|
||||||
const authRecord = payload.associations.filter((a) => a.useremail.toLowerCase() === userEmail.toLowerCase());
|
const authRecord = payload.associations.filter((a) => a.useremail.toLowerCase() === userEmail.toLowerCase());
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const UserActionTypes = {
|
|||||||
CHECK_ACTION_CODE_SUCCESS: "CHECK_ACTION_CODE_SUCCESS",
|
CHECK_ACTION_CODE_SUCCESS: "CHECK_ACTION_CODE_SUCCESS",
|
||||||
CHECK_ACTION_CODE_FAILURE: "CHECK_ACTION_CODE_FAILURE",
|
CHECK_ACTION_CODE_FAILURE: "CHECK_ACTION_CODE_FAILURE",
|
||||||
SET_CURRENT_EULA: "SET_CURRENT_EULA",
|
SET_CURRENT_EULA: "SET_CURRENT_EULA",
|
||||||
EULA_ACCEPTED: "EULA_ACCEPTED"
|
EULA_ACCEPTED: "EULA_ACCEPTED",
|
||||||
|
SET_IMEX_SHOP_ID: "SET_IMEX_SHOP_ID"
|
||||||
};
|
};
|
||||||
export default UserActionTypes;
|
export default UserActionTypes;
|
||||||
|
|||||||
@@ -722,6 +722,7 @@
|
|||||||
"scoreboardsetup": "Scoreboard Setup",
|
"scoreboardsetup": "Scoreboard Setup",
|
||||||
"shop_enabled_features": "Shop Enabled Features",
|
"shop_enabled_features": "Shop Enabled Features",
|
||||||
"shopinfo": "Shop Information",
|
"shopinfo": "Shop Information",
|
||||||
|
"shoprates": "Shop Rates",
|
||||||
"speedprint": "Speed Print Configuration",
|
"speedprint": "Speed Print Configuration",
|
||||||
"ssbuckets": "Job Size Definitions",
|
"ssbuckets": "Job Size Definitions",
|
||||||
"systemsettings": "System Settings",
|
"systemsettings": "System Settings",
|
||||||
@@ -1658,7 +1659,7 @@
|
|||||||
"08": "Left Rear Side",
|
"08": "Left Rear Side",
|
||||||
"09": "Left Side"
|
"09": "Left Side"
|
||||||
},
|
},
|
||||||
"auto_add_ats": "Automatically Add/Update ATS",
|
"auto_add_ats": "Automatically Add/Update ATS?",
|
||||||
"ca_bc_pvrt": "PVRT",
|
"ca_bc_pvrt": "PVRT",
|
||||||
"ca_customer_gst": "Customer Portion of GST",
|
"ca_customer_gst": "Customer Portion of GST",
|
||||||
"ca_gst_registrant": "GST Registrant",
|
"ca_gst_registrant": "GST Registrant",
|
||||||
@@ -1757,6 +1758,7 @@
|
|||||||
"est_ct_ln": "Estimator Last Name",
|
"est_ct_ln": "Estimator Last Name",
|
||||||
"est_ea": "Estimator Email",
|
"est_ea": "Estimator Email",
|
||||||
"est_ph1": "Estimator Phone #",
|
"est_ph1": "Estimator Phone #",
|
||||||
|
"flat_rate_ats": "Flat Rate ATS?",
|
||||||
"federal_tax_payable": "Federal Tax Payable",
|
"federal_tax_payable": "Federal Tax Payable",
|
||||||
"federal_tax_rate": "Federal Tax Rate",
|
"federal_tax_rate": "Federal Tax Rate",
|
||||||
"ins_addr1": "Insurance Co. Address",
|
"ins_addr1": "Insurance Co. Address",
|
||||||
@@ -1868,6 +1870,7 @@
|
|||||||
},
|
},
|
||||||
"queued_for_parts": "Queued for Parts",
|
"queued_for_parts": "Queued for Parts",
|
||||||
"rate_ats": "ATS Rate",
|
"rate_ats": "ATS Rate",
|
||||||
|
"rate_ats_flat": "ATS Flat Rate",
|
||||||
"rate_la1": "LA1",
|
"rate_la1": "LA1",
|
||||||
"rate_la2": "LA2",
|
"rate_la2": "LA2",
|
||||||
"rate_la3": "LA3",
|
"rate_la3": "LA3",
|
||||||
|
|||||||
@@ -722,6 +722,7 @@
|
|||||||
"scoreboardsetup": "",
|
"scoreboardsetup": "",
|
||||||
"shop_enabled_features": "",
|
"shop_enabled_features": "",
|
||||||
"shopinfo": "",
|
"shopinfo": "",
|
||||||
|
"shoprates": "",
|
||||||
"speedprint": "",
|
"speedprint": "",
|
||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
@@ -1757,6 +1758,7 @@
|
|||||||
"est_ct_ln": "Apellido del tasador",
|
"est_ct_ln": "Apellido del tasador",
|
||||||
"est_ea": "Correo electrónico del tasador",
|
"est_ea": "Correo electrónico del tasador",
|
||||||
"est_ph1": "Número de teléfono del tasador",
|
"est_ph1": "Número de teléfono del tasador",
|
||||||
|
"flat_rate_ats": "",
|
||||||
"federal_tax_payable": "Impuesto federal por pagar",
|
"federal_tax_payable": "Impuesto federal por pagar",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
"ins_addr1": "Dirección de Insurance Co.",
|
"ins_addr1": "Dirección de Insurance Co.",
|
||||||
@@ -1868,6 +1870,7 @@
|
|||||||
},
|
},
|
||||||
"queued_for_parts": "",
|
"queued_for_parts": "",
|
||||||
"rate_ats": "",
|
"rate_ats": "",
|
||||||
|
"rate_ats_flat": "",
|
||||||
"rate_la1": "Tarifa LA1",
|
"rate_la1": "Tarifa LA1",
|
||||||
"rate_la2": "Tarifa LA2",
|
"rate_la2": "Tarifa LA2",
|
||||||
"rate_la3": "Tarifa LA3",
|
"rate_la3": "Tarifa LA3",
|
||||||
|
|||||||
@@ -722,6 +722,7 @@
|
|||||||
"scoreboardsetup": "",
|
"scoreboardsetup": "",
|
||||||
"shop_enabled_features": "",
|
"shop_enabled_features": "",
|
||||||
"shopinfo": "",
|
"shopinfo": "",
|
||||||
|
"shoprates": "",
|
||||||
"speedprint": "",
|
"speedprint": "",
|
||||||
"ssbuckets": "",
|
"ssbuckets": "",
|
||||||
"systemsettings": "",
|
"systemsettings": "",
|
||||||
@@ -1757,6 +1758,7 @@
|
|||||||
"est_ct_ln": "Nom de l'évaluateur",
|
"est_ct_ln": "Nom de l'évaluateur",
|
||||||
"est_ea": "Courriel de l'évaluateur",
|
"est_ea": "Courriel de l'évaluateur",
|
||||||
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
||||||
|
"flat_rate_ats": "",
|
||||||
"federal_tax_payable": "Impôt fédéral à payer",
|
"federal_tax_payable": "Impôt fédéral à payer",
|
||||||
"federal_tax_rate": "",
|
"federal_tax_rate": "",
|
||||||
"ins_addr1": "Adresse Insurance Co.",
|
"ins_addr1": "Adresse Insurance Co.",
|
||||||
@@ -1868,6 +1870,7 @@
|
|||||||
},
|
},
|
||||||
"queued_for_parts": "",
|
"queued_for_parts": "",
|
||||||
"rate_ats": "",
|
"rate_ats": "",
|
||||||
|
"rate_ats_flat": "",
|
||||||
"rate_la1": "Taux LA1",
|
"rate_la1": "Taux LA1",
|
||||||
"rate_la2": "Taux LA2",
|
"rate_la2": "Taux LA2",
|
||||||
"rate_la3": "Taux LA3",
|
"rate_la3": "Taux LA3",
|
||||||
|
|||||||
@@ -191,24 +191,11 @@ export default defineConfig({
|
|||||||
"@sentry/react": ["@sentry/react"],
|
"@sentry/react": ["@sentry/react"],
|
||||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
||||||
logrocket: ["logrocket"],
|
logrocket: ["logrocket"],
|
||||||
"firebase/app": ["firebase/app"],
|
"@firebase/analytics": ["@firebase/analytics"],
|
||||||
"firebase/firestore": ["firebase/firestore"],
|
|
||||||
"firebase/firestore/lite": ["firebase/firestore/lite"],
|
|
||||||
"firebase/auth": ["firebase/auth"],
|
|
||||||
"firebase/functions": ["firebase/functions"],
|
|
||||||
"firebase/storage": ["firebase/storage"],
|
|
||||||
"firebase/database": ["firebase/database"],
|
|
||||||
"firebase/remote-config": ["firebase/remote-config"],
|
|
||||||
"firebase/performance": ["firebase/performance"],
|
|
||||||
"@firebase/app": ["@firebase/app"],
|
"@firebase/app": ["@firebase/app"],
|
||||||
"@firebase/firestore": ["@firebase/firestore"],
|
"@firebase/firestore": ["@firebase/firestore"],
|
||||||
"@firebase/firestore/lite": ["@firebase/firestore/lite"],
|
|
||||||
"@firebase/auth": ["@firebase/auth"],
|
"@firebase/auth": ["@firebase/auth"],
|
||||||
"@firebase/functions": ["@firebase/functions"],
|
"@firebase/messaging": ["@firebase/messaging"],
|
||||||
"@firebase/storage": ["@firebase/storage"],
|
|
||||||
"@firebase/database": ["@firebase/database"],
|
|
||||||
"@firebase/remote-config": ["@firebase/remote-config"],
|
|
||||||
"@firebase/performance": ["@firebase/performance"],
|
|
||||||
markerjs2: ["markerjs2"],
|
markerjs2: ["markerjs2"],
|
||||||
"@apollo/client": ["@apollo/client"],
|
"@apollo/client": ["@apollo/client"],
|
||||||
"libphonenumber-js": ["libphonenumber-js"]
|
"libphonenumber-js": ["libphonenumber-js"]
|
||||||
|
|||||||
@@ -3695,6 +3695,7 @@
|
|||||||
- est_st
|
- est_st
|
||||||
- est_zip
|
- est_zip
|
||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
@@ -3789,6 +3790,7 @@
|
|||||||
- qb_multiple_payers
|
- qb_multiple_payers
|
||||||
- queued_for_parts
|
- queued_for_parts
|
||||||
- rate_ats
|
- rate_ats
|
||||||
|
- rate_ats_flat
|
||||||
- rate_la1
|
- rate_la1
|
||||||
- rate_la2
|
- rate_la2
|
||||||
- rate_la3
|
- rate_la3
|
||||||
@@ -3965,6 +3967,7 @@
|
|||||||
- est_st
|
- est_st
|
||||||
- est_zip
|
- est_zip
|
||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
@@ -4060,6 +4063,7 @@
|
|||||||
- qb_multiple_payers
|
- qb_multiple_payers
|
||||||
- queued_for_parts
|
- queued_for_parts
|
||||||
- rate_ats
|
- rate_ats
|
||||||
|
- rate_ats_flat
|
||||||
- rate_la1
|
- rate_la1
|
||||||
- rate_la2
|
- rate_la2
|
||||||
- rate_la3
|
- rate_la3
|
||||||
@@ -4247,6 +4251,7 @@
|
|||||||
- est_st
|
- est_st
|
||||||
- est_zip
|
- est_zip
|
||||||
- federal_tax_rate
|
- federal_tax_rate
|
||||||
|
- flat_rate_ats
|
||||||
- g_bett_amt
|
- g_bett_amt
|
||||||
- id
|
- id
|
||||||
- inproduction
|
- inproduction
|
||||||
@@ -4342,6 +4347,7 @@
|
|||||||
- qb_multiple_payers
|
- qb_multiple_payers
|
||||||
- queued_for_parts
|
- queued_for_parts
|
||||||
- rate_ats
|
- rate_ats
|
||||||
|
- rate_ats_flat
|
||||||
- rate_la1
|
- rate_la1
|
||||||
- rate_la2
|
- rate_la2
|
||||||
- rate_la3
|
- rate_la3
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "flat_rate_ats" boolean
|
||||||
|
-- null default 'false';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "flat_rate_ats" boolean
|
||||||
|
null default 'false';
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "rate_ats_flat" numeric
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "rate_ats_flat" numeric
|
||||||
|
null;
|
||||||
1597
package-lock.json
generated
1597
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -9,56 +9,53 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
|
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
|
||||||
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
|
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
|
||||||
"admin": "cd admin && npm start",
|
|
||||||
"client": "cd client && npm start",
|
|
||||||
"server": "nodemon server.js",
|
|
||||||
"build": "cd client && npm run build",
|
|
||||||
"dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\"",
|
|
||||||
"deva": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\" \"npm run admin\"",
|
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.758.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.772.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.758.0",
|
"@aws-sdk/client-elasticache": "^3.772.0",
|
||||||
"@aws-sdk/client-s3": "^3.758.0",
|
"@aws-sdk/client-s3": "^3.772.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.758.0",
|
"@aws-sdk/client-secrets-manager": "^3.772.0",
|
||||||
"@aws-sdk/client-ses": "^3.758.0",
|
"@aws-sdk/client-ses": "^3.772.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.758.0",
|
"@aws-sdk/credential-provider-node": "^3.772.0",
|
||||||
|
"@aws-sdk/lib-storage": "^3.774.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.774.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.4",
|
||||||
"bee-queue": "^1.7.1",
|
"bee-queue": "^1.7.1",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"bullmq": "^5.41.7",
|
"bullmq": "^5.44.4",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.5.1",
|
"cloudinary": "^2.6.0",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dd-trace": "^5.40.0",
|
"dd-trace": "^5.43.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"firebase-admin": "^13.1.0",
|
"firebase-admin": "^13.2.0",
|
||||||
"graphql": "^16.10.0",
|
"graphql": "^16.10.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"inline-css": "^4.0.3",
|
"inline-css": "^4.0.3",
|
||||||
"intuit-oauth": "^4.2.0",
|
"intuit-oauth": "^4.2.0",
|
||||||
"ioredis": "^5.5.0",
|
"ioredis": "^5.6.0",
|
||||||
"json-2-csv": "^5.5.8",
|
"json-2-csv": "^5.5.9",
|
||||||
"juice": "^11.0.1",
|
"juice": "^11.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.47",
|
"moment-timezone": "^0.5.48",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-mailjet": "^6.0.6",
|
"node-mailjet": "^6.0.8",
|
||||||
"node-persist": "^4.0.4",
|
"node-persist": "^4.0.4",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"phone": "^3.1.58",
|
"phone": "^3.1.58",
|
||||||
@@ -66,22 +63,21 @@
|
|||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"skia-canvas": "^2.0.2",
|
"skia-canvas": "^2.0.2",
|
||||||
"soap": "^1.1.9",
|
"soap": "^1.1.10",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^4.23.0",
|
"twilio": "^5.5.1",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.23.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
"concurrently": "^8.2.2",
|
"eslint": "^9.23.0",
|
||||||
"eslint": "^9.21.0",
|
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ const loadQueues = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
|
|||||||
*/
|
*/
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.PORT || 5000;
|
const port = process.env.PORT || 4000;
|
||||||
|
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const AxiosLib = require("axios").default;
|
|||||||
const axios = AxiosLib.create();
|
const axios = AxiosLib.create();
|
||||||
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
|
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
|
||||||
const { CheckForErrors } = require("./pbs-job-export");
|
const { CheckForErrors } = require("./pbs-job-export");
|
||||||
const uuid = require("uuid").v4;
|
|
||||||
axios.interceptors.request.use((x) => {
|
axios.interceptors.request.use((x) => {
|
||||||
const socket = x.socket;
|
const socket = x.socket;
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ axios.interceptors.request.use((x) => {
|
|||||||
...x.headers[x.method],
|
...x.headers[x.method],
|
||||||
...x.headers
|
...x.headers
|
||||||
};
|
};
|
||||||
|
|
||||||
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
|
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
|
||||||
x.url
|
x.url
|
||||||
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
|
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const entegralEndpoint =
|
|||||||
: "https://uat-ws.armsbusinesssolutions.net/RepairOrderFolderService/RepairOrderFolderService.asmx?WSDL";
|
: "https://uat-ws.armsbusinesssolutions.net/RepairOrderFolderService/RepairOrderFolderService.asmx?WSDL";
|
||||||
|
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
const uuid = require("uuid").v4;
|
const { v4 } = require("uuid");
|
||||||
|
|
||||||
const momentFormat = "yyyy-MM-DDTHH:mm:ss.SSS";
|
const momentFormat = "yyyy-MM-DDTHH:mm:ss.SSS";
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ exports.default = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transId = uuid(); // Can this actually be the job id?
|
const transId = v4(); // Can this actually be the job id?
|
||||||
let obj = {
|
let obj = {
|
||||||
RqUID: transId,
|
RqUID: transId,
|
||||||
DocumentInfo: {
|
DocumentInfo: {
|
||||||
|
|||||||
@@ -1485,6 +1485,8 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) {
|
|||||||
materials
|
materials
|
||||||
auto_add_ats
|
auto_add_ats
|
||||||
rate_ats
|
rate_ats
|
||||||
|
flat_rate_ats
|
||||||
|
rate_ats_flat
|
||||||
joblines(where: { removed: { _eq: false } }){
|
joblines(where: { removed: { _eq: false } }){
|
||||||
id
|
id
|
||||||
line_no
|
line_no
|
||||||
@@ -2769,3 +2771,61 @@ exports.GET_BODYSHOP_BY_ID = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports.GET_DOCUMENTS_BY_JOB = `
|
||||||
|
query GET_DOCUMENTS_BY_JOB($jobId: uuid!) {
|
||||||
|
jobs_by_pk(id: $jobId) {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
}
|
||||||
|
documents_aggregate(where: { jobid: { _eq: $jobId } }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
documents(order_by: { takenat: desc }, where: { jobid: { _eq: $jobId } }) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
type
|
||||||
|
size
|
||||||
|
takenat
|
||||||
|
extension
|
||||||
|
bill {
|
||||||
|
id
|
||||||
|
invoice_number
|
||||||
|
date
|
||||||
|
vendor {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
exports.QUERY_TEMPORARY_DOCS = ` query QUERY_TEMPORARY_DOCS {
|
||||||
|
documents(where: { jobid: { _is_null: true } }, order_by: { takenat: desc }) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
type
|
||||||
|
extension
|
||||||
|
size
|
||||||
|
takenat
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
exports.GET_DOCUMENTS_BY_IDS = `
|
||||||
|
query GET_DOCUMENTS_BY_IDS($documentIds: [uuid!]!) {
|
||||||
|
documents(where: {id: {_in: $documentIds}}, order_by: {takenat: desc}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
type
|
||||||
|
extension
|
||||||
|
size
|
||||||
|
takenat
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const adminClient = require("../graphql-client/graphql-client").client;
|
// const adminClient = require("../graphql-client/graphql-client").client;
|
||||||
const _ = require("lodash");
|
// const _ = require("lodash");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const InstanceMgr = require("../utils/instanceMgr").default;
|
const InstanceMgr = require("../utils/instanceMgr").default;
|
||||||
|
|
||||||
@@ -45,7 +45,9 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).send();
|
if (result) {
|
||||||
|
res.status(200).send();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-ssu-USA-error", "ERROR", req?.user?.email, id, {
|
logger.log("job-totals-ssu-USA-error", "ERROR", req?.user?.email, id, {
|
||||||
jobid: id,
|
jobid: id,
|
||||||
@@ -59,7 +61,7 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
//IMPORTANT*** These two functions MUST be mirrrored.
|
//IMPORTANT*** These two functions MUST be mirrrored.
|
||||||
async function TotalsServerSide(req, res) {
|
async function TotalsServerSide(req, res) {
|
||||||
const { job, client } = req.body;
|
const { job, client } = req.body;
|
||||||
await AutoAddAtsIfRequired({ job: job, client: client });
|
await AtsAdjustmentsIfRequired({ job: job, client: client, user: req?.user });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let ret = {
|
let ret = {
|
||||||
@@ -138,10 +140,11 @@ async function Totals(req, res) {
|
|||||||
const client = req.userGraphQLClient;
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
logger.log("job-totals-ssu-USA", "DEBUG", req.user.email, job.id, {
|
logger.log("job-totals-ssu-USA", "DEBUG", req.user.email, job.id, {
|
||||||
jobid: job.id
|
jobid: job.id,
|
||||||
|
id: id
|
||||||
});
|
});
|
||||||
|
|
||||||
await AutoAddAtsIfRequired({ job, client });
|
await AtsAdjustmentsIfRequired({ job, client, user: req.user });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let ret = {
|
let ret = {
|
||||||
@@ -153,7 +156,7 @@ async function Totals(req, res) {
|
|||||||
|
|
||||||
res.status(200).json(ret);
|
res.status(200).json(ret);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-USA-error", "ERROR", req.user.email, job.id, {
|
logger.log("job-totals-ssu-USA-error", "ERROR", req.user.email, job.id, {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
@@ -162,40 +165,45 @@ async function Totals(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function AutoAddAtsIfRequired({ job, client }) {
|
async function AtsAdjustmentsIfRequired({ job, client, user }) {
|
||||||
//Check if ATS should be automatically added.
|
if (job.auto_add_ats || job.flat_rate_ats) {
|
||||||
if (job.auto_add_ats) {
|
let atsAmount = 0;
|
||||||
//Get the total sum of hours that should be the ATS amount.
|
|
||||||
//Check to see if an ATS line exists.
|
|
||||||
let atsLineIndex = null;
|
let atsLineIndex = null;
|
||||||
const atsHours = job.joblines.reduce((acc, val, index) => {
|
|
||||||
if (val.line_desc && val.line_desc.toLowerCase() === "ats amount") {
|
|
||||||
atsLineIndex = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
//Check if ATS should be automatically added.
|
||||||
val.mod_lbr_ty !== "LA1" &&
|
if (job.auto_add_ats) {
|
||||||
val.mod_lbr_ty !== "LA2" &&
|
const excludedLaborTypes = new Set(["LAA", "LAG", "LAS", "LAU", "LA1", "LA2", "LA3", "LA4"]);
|
||||||
val.mod_lbr_ty !== "LA3" &&
|
|
||||||
val.mod_lbr_ty !== "LA4" &&
|
|
||||||
val.mod_lbr_ty !== "LAU" &&
|
|
||||||
val.mod_lbr_ty !== "LAG" &&
|
|
||||||
val.mod_lbr_ty !== "LAS" &&
|
|
||||||
val.mod_lbr_ty !== "LAA"
|
|
||||||
) {
|
|
||||||
acc = acc + val.mod_lb_hrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
//Get the total sum of hours that should be the ATS amount.
|
||||||
}, 0);
|
//Check to see if an ATS line exists.
|
||||||
|
const atsHours = job.joblines.reduce((acc, val, index) => {
|
||||||
|
if (val.line_desc?.toLowerCase() === "ats amount") {
|
||||||
|
atsLineIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
const atsAmount = atsHours * (job.rate_ats || 0);
|
if (!excludedLaborTypes.has(val.mod_lbr_ty)) {
|
||||||
//If it does, update it in place, and make sure it is updated for local calculations.
|
acc = acc + val.mod_lb_hrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
atsAmount = atsHours * (job.rate_ats || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if a Flat Rate ATS should be added.
|
||||||
|
if (job.flat_rate_ats) {
|
||||||
|
atsLineIndex = ((i) => (i === -1 ? null : i))(
|
||||||
|
job.joblines.findIndex((line) => line.line_desc?.toLowerCase() === "ats amount")
|
||||||
|
);
|
||||||
|
atsAmount = job.rate_ats_flat || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If it does not, create one for local calculations and insert it.
|
||||||
if (atsLineIndex === null) {
|
if (atsLineIndex === null) {
|
||||||
const newAtsLine = {
|
const newAtsLine = {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
alt_partm: null,
|
alt_partm: null,
|
||||||
line_no: 35,
|
|
||||||
unq_seq: 0,
|
unq_seq: 0,
|
||||||
line_ind: "E",
|
line_ind: "E",
|
||||||
line_desc: "ATS Amount",
|
line_desc: "ATS Amount",
|
||||||
@@ -220,19 +228,42 @@ async function AutoAddAtsIfRequired({ job, client }) {
|
|||||||
prt_dsmk_m: 0.0
|
prt_dsmk_m: 0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
try {
|
||||||
lineInput: [newAtsLine]
|
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
||||||
});
|
lineInput: [newAtsLine]
|
||||||
|
});
|
||||||
|
|
||||||
job.joblines.push(newAtsLine);
|
if (result) {
|
||||||
|
job.joblines.push(newAtsLine);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("job-totals-ssu-ats-error", "ERROR", user?.email, job.id, {
|
||||||
|
jobid: job.id,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//If it does not, create one for local calculations and insert it.
|
//If it does, update it in place, and make sure it is updated for local calculations.
|
||||||
else {
|
else {
|
||||||
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
try {
|
||||||
line: { act_price: atsAmount },
|
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
||||||
lineId: job.joblines[atsLineIndex].id
|
line: { act_price: atsAmount },
|
||||||
});
|
lineId: job.joblines[atsLineIndex].id
|
||||||
job.joblines[atsLineIndex].act_price = atsAmount;
|
});
|
||||||
|
if (result) {
|
||||||
|
job.joblines[atsLineIndex].act_price = atsAmount;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("job-totals-ssu-ats-error", "ERROR", user?.email, job.id, {
|
||||||
|
jobid: job.id,
|
||||||
|
atsLineIndex: atsLineIndex,
|
||||||
|
atsAmount: atsAmount,
|
||||||
|
jobline: job.joblines[atsLineIndex],
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,7 +345,7 @@ async function CalculateRatesTotals({ job, client }) {
|
|||||||
let hasMashLine = false;
|
let hasMashLine = false;
|
||||||
let hasMahwLine = false;
|
let hasMahwLine = false;
|
||||||
let hasCustomMahwLine;
|
let hasCustomMahwLine;
|
||||||
let mapaOpCodes = ParseCalopCode(job.materials["MAPA"]?.cal_opcode);
|
// let mapaOpCodes = ParseCalopCode(job.materials["MAPA"]?.cal_opcode);
|
||||||
let mashOpCodes = ParseCalopCode(job.materials["MASH"]?.cal_opcode);
|
let mashOpCodes = ParseCalopCode(job.materials["MASH"]?.cal_opcode);
|
||||||
|
|
||||||
jobLines.forEach((item) => {
|
jobLines.forEach((item) => {
|
||||||
@@ -564,7 +595,7 @@ function CalculatePartsTotals(jobLines, parts_tax_rates, job) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
if (!value.part_type && value.db_ref !== "900510" && value.db_ref !== "900511") return acc;
|
if (!value.part_type && value.db_ref !== "900510" && value.db_ref !== "900511") return acc;
|
||||||
|
|
||||||
const discountAmount =
|
const discountAmount =
|
||||||
@@ -631,6 +662,7 @@ function CalculatePartsTotals(jobLines, parts_tax_rates, job) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -652,7 +684,7 @@ function CalculatePartsTotals(jobLines, parts_tax_rates, job) {
|
|||||||
let adjustments = {};
|
let adjustments = {};
|
||||||
//Track all adjustments that need to be made.
|
//Track all adjustments that need to be made.
|
||||||
|
|
||||||
const linesToAdjustForDiscount = [];
|
//const linesToAdjustForDiscount = [];
|
||||||
Object.keys(parts_tax_rates).forEach((key) => {
|
Object.keys(parts_tax_rates).forEach((key) => {
|
||||||
//Check if there's a discount or a mark up.
|
//Check if there's a discount or a mark up.
|
||||||
let disc = Dinero(),
|
let disc = Dinero(),
|
||||||
@@ -1019,7 +1051,9 @@ function CalculateTaxesTotals(job, otherTotals) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-USA Key with issue", "error", null, job.id, {
|
logger.log("job-totals-USA Key with issue", "error", null, job.id, {
|
||||||
key
|
key: key,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1157,6 +1191,7 @@ function CalculateTaxesTotals(job, otherTotals) {
|
|||||||
|
|
||||||
exports.default = Totals;
|
exports.default = Totals;
|
||||||
|
|
||||||
|
//eslint-disable-next-line no-unused-vars
|
||||||
function DiscountNotAlreadyCounted(jobline, joblines) {
|
function DiscountNotAlreadyCounted(jobline, joblines) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1172,27 +1207,35 @@ function IsTrueOrYes(value) {
|
|||||||
return value === true || value === "Y" || value === "y";
|
return value === true || value === "Y" || value === "y";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function UpdateJobLines(joblinesToUpdate) {
|
// Function not in use from RO to IO Merger 02/05/2024
|
||||||
if (joblinesToUpdate.length === 0) return;
|
// async function UpdateJobLines(joblinesToUpdate) {
|
||||||
const updateQueries = joblinesToUpdate.map((line, index) =>
|
// if (joblinesToUpdate.length === 0) return;
|
||||||
generateUpdateQuery(_.pick(line, ["id", "prt_dsmk_m", "prt_dsmk_p"]), index)
|
// const updateQueries = joblinesToUpdate.map((line, index) =>
|
||||||
);
|
// generateUpdateQuery(_.pick(line, ["id", "prt_dsmk_m", "prt_dsmk_p"]), index)
|
||||||
const query = `
|
// );
|
||||||
mutation UPDATE_EST_LINES{
|
// const query = `
|
||||||
${updateQueries}
|
// mutation UPDATE_EST_LINES{
|
||||||
}
|
// ${updateQueries}
|
||||||
`;
|
// }
|
||||||
|
// `;
|
||||||
|
// try {
|
||||||
|
// const result = await adminClient.request(query);
|
||||||
|
// void result;
|
||||||
|
// } catch (error) {
|
||||||
|
// logger.log("update-job-lines", "error", null, null, {
|
||||||
|
// error: error.message,
|
||||||
|
// stack: error.stack
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const result = await adminClient.request(query);
|
// const generateUpdateQuery = (lineToUpdate, index) => {
|
||||||
}
|
// return `
|
||||||
|
// update_joblines${index}: update_joblines(where: { id: { _eq: "${
|
||||||
const generateUpdateQuery = (lineToUpdate, index) => {
|
// lineToUpdate.id
|
||||||
return `
|
// }" } }, _set: ${JSON.stringify(lineToUpdate).replace(/"(\w+)"\s*:/g, "$1:")}) {
|
||||||
update_joblines${index}: update_joblines(where: { id: { _eq: "${
|
// returning {
|
||||||
lineToUpdate.id
|
// id
|
||||||
}" } }, _set: ${JSON.stringify(lineToUpdate).replace(/"(\w+)"\s*:/g, "$1:")}) {
|
// }
|
||||||
returning {
|
// }`;
|
||||||
id
|
// };
|
||||||
}
|
|
||||||
}`;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const adminClient = require("../graphql-client/graphql-client").client;
|
|
||||||
const _ = require("lodash");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
|
|
||||||
//****************************************************** */
|
//****************************************************** */
|
||||||
@@ -44,11 +42,16 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("Failed to update job totals");
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-ssu-error", "ERROR", req.user.email, id, {
|
logger.log("job-totals-ssu-error", "ERROR", req.user.email, id, {
|
||||||
jobid: id,
|
jobid: id,
|
||||||
error
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
res.status(503).send();
|
res.status(503).send();
|
||||||
}
|
}
|
||||||
@@ -57,7 +60,7 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
//IMPORTANT*** These two functions MUST be mirrrored.
|
//IMPORTANT*** These two functions MUST be mirrrored.
|
||||||
async function TotalsServerSide(req, res) {
|
async function TotalsServerSide(req, res) {
|
||||||
const { job, client } = req.body;
|
const { job, client } = req.body;
|
||||||
await AutoAddAtsIfRequired({ job: job, client: client });
|
await AtsAdjustmentsIfRequired({ job: job, client: client, user: req?.user });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let ret = {
|
let ret = {
|
||||||
@@ -71,7 +74,8 @@ async function TotalsServerSide(req, res) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-ssu-error", "ERROR", req?.user?.email, job.id, {
|
logger.log("job-totals-ssu-error", "ERROR", req?.user?.email, job.id, {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
error
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
res.status(400).send(JSON.stringify(error));
|
res.status(400).send(JSON.stringify(error));
|
||||||
}
|
}
|
||||||
@@ -83,13 +87,12 @@ async function Totals(req, res) {
|
|||||||
const logger = req.logger;
|
const logger = req.logger;
|
||||||
const client = req.userGraphQLClient;
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
logger.log("job-totals", "DEBUG", req.user.email, job.id, {
|
logger.log("job-totals-ssu", "DEBUG", req.user.email, job.id, {
|
||||||
jobid: job.id
|
jobid: job.id,
|
||||||
|
id: id
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log("job-totals-ssu", "DEBUG", req.user.email, id, null);
|
await AtsAdjustmentsIfRequired({ job, client, user: req.user });
|
||||||
|
|
||||||
await AutoAddAtsIfRequired({ job, client });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let ret = {
|
let ret = {
|
||||||
@@ -101,48 +104,54 @@ async function Totals(req, res) {
|
|||||||
|
|
||||||
res.status(200).json(ret);
|
res.status(200).json(ret);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-error", "ERROR", req.user.email, job.id, {
|
logger.log("job-totals-ssu-error", "ERROR", req.user.email, job.id, {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
error
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
});
|
});
|
||||||
res.status(400).send(JSON.stringify(error));
|
res.status(400).send(JSON.stringify(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function AutoAddAtsIfRequired({ job, client }) {
|
async function AtsAdjustmentsIfRequired({ job, client, user }) {
|
||||||
//Check if ATS should be automatically added.
|
if (job.auto_add_ats || job.flat_rate_ats) {
|
||||||
if (job.auto_add_ats) {
|
let atsAmount = 0;
|
||||||
//Get the total sum of hours that should be the ATS amount.
|
|
||||||
//Check to see if an ATS line exists.
|
|
||||||
let atsLineIndex = null;
|
let atsLineIndex = null;
|
||||||
const atsHours = job.joblines.reduce((acc, val, index) => {
|
|
||||||
if (val.line_desc && val.line_desc.toLowerCase() === "ats amount") {
|
|
||||||
atsLineIndex = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
//Check if ATS should be automatically added.
|
||||||
val.mod_lbr_ty !== "LA1" &&
|
if (job.auto_add_ats) {
|
||||||
val.mod_lbr_ty !== "LA2" &&
|
const excludedLaborTypes = new Set(["LAA", "LAG", "LAS", "LAU", "LA1", "LA2", "LA3", "LA4"]);
|
||||||
val.mod_lbr_ty !== "LA3" &&
|
|
||||||
val.mod_lbr_ty !== "LA4" &&
|
|
||||||
val.mod_lbr_ty !== "LAU" &&
|
|
||||||
val.mod_lbr_ty !== "LAG" &&
|
|
||||||
val.mod_lbr_ty !== "LAS" &&
|
|
||||||
val.mod_lbr_ty !== "LAA"
|
|
||||||
) {
|
|
||||||
acc = acc + val.mod_lb_hrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
//Get the total sum of hours that should be the ATS amount.
|
||||||
}, 0);
|
//Check to see if an ATS line exists.
|
||||||
|
const atsHours = job.joblines.reduce((acc, val, index) => {
|
||||||
|
if (val.line_desc?.toLowerCase() === "ats amount") {
|
||||||
|
atsLineIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
const atsAmount = atsHours * (job.rate_ats || 0);
|
if (!excludedLaborTypes.has(val.mod_lbr_ty)) {
|
||||||
//If it does, update it in place, and make sure it is updated for local calculations.
|
acc = acc + val.mod_lb_hrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
atsAmount = atsHours * (job.rate_ats || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if a Flat Rate ATS should be added.
|
||||||
|
if (job.flat_rate_ats) {
|
||||||
|
atsLineIndex = ((i) => (i === -1 ? null : i))(
|
||||||
|
job.joblines.findIndex((line) => line.line_desc?.toLowerCase() === "ats amount")
|
||||||
|
);
|
||||||
|
atsAmount = job.rate_ats_flat || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If it does not, create one for local calculations and insert it.
|
||||||
if (atsLineIndex === null) {
|
if (atsLineIndex === null) {
|
||||||
const newAtsLine = {
|
const newAtsLine = {
|
||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
alt_partm: null,
|
alt_partm: null,
|
||||||
line_no: 35,
|
|
||||||
unq_seq: 0,
|
unq_seq: 0,
|
||||||
line_ind: "E",
|
line_ind: "E",
|
||||||
line_desc: "ATS Amount",
|
line_desc: "ATS Amount",
|
||||||
@@ -167,22 +176,43 @@ async function AutoAddAtsIfRequired({ job, client }) {
|
|||||||
prt_dsmk_m: 0.0
|
prt_dsmk_m: 0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
try {
|
||||||
lineInput: [newAtsLine]
|
const result = await client.request(queries.INSERT_NEW_JOB_LINE, {
|
||||||
});
|
lineInput: [newAtsLine]
|
||||||
|
});
|
||||||
|
|
||||||
job.joblines.push(newAtsLine);
|
if (result) {
|
||||||
|
job.joblines.push(newAtsLine);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("job-totals-ssu-ats-error", "ERROR", user?.email, job.id, {
|
||||||
|
jobid: job.id,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//If it does not, create one for local calculations and insert it.
|
//If it does, update it in place, and make sure it is updated for local calculations.
|
||||||
else {
|
else {
|
||||||
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
try {
|
||||||
line: { act_price: atsAmount },
|
const result = await client.request(queries.UPDATE_JOB_LINE, {
|
||||||
lineId: job.joblines[atsLineIndex].id
|
line: { act_price: atsAmount },
|
||||||
});
|
lineId: job.joblines[atsLineIndex].id
|
||||||
job.joblines[atsLineIndex].act_price = atsAmount;
|
});
|
||||||
|
if (result) {
|
||||||
|
job.joblines[atsLineIndex].act_price = atsAmount;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("job-totals-ssu-ats-error", "ERROR", user?.email, job.id, {
|
||||||
|
jobid: job.id,
|
||||||
|
atsLineIndex: atsLineIndex,
|
||||||
|
atsAmount: atsAmount,
|
||||||
|
jobline: job.joblines[atsLineIndex],
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log(job.jobLines);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
348
server/media/imgproxy-media.js
Normal file
348
server/media/imgproxy-media.js
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
const path = require("path");
|
||||||
|
require("dotenv").config({
|
||||||
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
|
});
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const {
|
||||||
|
S3Client,
|
||||||
|
PutObjectCommand,
|
||||||
|
GetObjectCommand,
|
||||||
|
CopyObjectCommand,
|
||||||
|
DeleteObjectCommand
|
||||||
|
} = require("@aws-sdk/client-s3");
|
||||||
|
const { Upload } = require("@aws-sdk/lib-storage");
|
||||||
|
|
||||||
|
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const { InstanceRegion } = require("../utils/instanceMgr");
|
||||||
|
const {
|
||||||
|
GET_DOCUMENTS_BY_JOB,
|
||||||
|
QUERY_TEMPORARY_DOCS,
|
||||||
|
GET_DOCUMENTS_BY_IDS,
|
||||||
|
DELETE_MEDIA_DOCUMENTS
|
||||||
|
} = require("../graphql-client/queries");
|
||||||
|
const archiver = require("archiver");
|
||||||
|
const stream = require("node:stream");
|
||||||
|
|
||||||
|
const imgproxyBaseUrl = process.env.IMGPROXY_BASE_URL; // `https://u4gzpp5wm437dnm75qa42tvza40fguqr.lambda-url.ca-central-1.on.aws` //Direct Lambda function access to bypass CDN.
|
||||||
|
const imgproxyKey = process.env.IMGPROXY_KEY;
|
||||||
|
const imgproxySalt = process.env.IMGPROXY_SALT;
|
||||||
|
const imgproxyDestinationBucket = process.env.IMGPROXY_DESTINATION_BUCKET;
|
||||||
|
|
||||||
|
//Generate a signed upload link for the S3 bucket.
|
||||||
|
//All uploads must be going to the same shop and jobid.
|
||||||
|
exports.generateSignedUploadUrls = async (req, res) => {
|
||||||
|
const { filenames, bodyshopid, jobid } = req.body;
|
||||||
|
try {
|
||||||
|
logger.log("imgproxy-upload-start", "DEBUG", req.user?.email, jobid, { filenames, bodyshopid, jobid });
|
||||||
|
|
||||||
|
const signedUrls = [];
|
||||||
|
for (const filename of filenames) {
|
||||||
|
const key = filename;
|
||||||
|
const client = new S3Client({ region: InstanceRegion() });
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: imgproxyDestinationBucket,
|
||||||
|
Key: key,
|
||||||
|
StorageClass: "INTELLIGENT_TIERING"
|
||||||
|
});
|
||||||
|
const presignedUrl = await getSignedUrl(client, command, { expiresIn: 360 });
|
||||||
|
signedUrls.push({ filename, presignedUrl, key });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("imgproxy-upload-success", "DEBUG", req.user?.email, jobid, { signedUrls });
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
signedUrls
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
logger.log("imgproxy-upload-error", "ERROR", req.user?.email, jobid, {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getThumbnailUrls = async (req, res) => {
|
||||||
|
const { jobid, billid } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.log("imgproxy-thumbnails", "DEBUG", req.user?.email, jobid, { billid, jobid });
|
||||||
|
|
||||||
|
//Delayed as the key structure may change slightly from what it is currently and will require evaluating mobile components.
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
//If there's no jobid and no billid, we're in temporary documents.
|
||||||
|
const data = await (jobid
|
||||||
|
? client.request(GET_DOCUMENTS_BY_JOB, { jobId: jobid })
|
||||||
|
: client.request(QUERY_TEMPORARY_DOCS));
|
||||||
|
|
||||||
|
const thumbResizeParams = `rs:fill:250:250:1/g:ce`;
|
||||||
|
const s3client = new S3Client({ region: InstanceRegion() });
|
||||||
|
const proxiedUrls = [];
|
||||||
|
|
||||||
|
for (const document of data.documents) {
|
||||||
|
//Format to follow:
|
||||||
|
//<Cloudfront_to_lambda>/<hmac with SHA of entire request URI path (with base64 encoded URL if needed), beginning with unencoded/unhashed Salt>/<remainder of url - resize params >/< base 64 URL encoded to image path>
|
||||||
|
|
||||||
|
//When working with documents from Cloudinary, the URL does not include the extension.
|
||||||
|
let key;
|
||||||
|
if (/\.[^/.]+$/.test(document.key)) {
|
||||||
|
key = document.key;
|
||||||
|
} else {
|
||||||
|
key = `${document.key}.${document.extension.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
// Build the S3 path to the object.
|
||||||
|
const fullS3Path = `s3://${imgproxyDestinationBucket}/${key}`;
|
||||||
|
const base64UrlEncodedKeyString = base64UrlEncode(fullS3Path);
|
||||||
|
//Thumbnail Generation Block
|
||||||
|
const thumbProxyPath = `${thumbResizeParams}/${base64UrlEncodedKeyString}`;
|
||||||
|
const thumbHmacSalt = createHmacSha256(`${imgproxySalt}/${thumbProxyPath}`);
|
||||||
|
|
||||||
|
//Full Size URL block
|
||||||
|
|
||||||
|
const fullSizeProxyPath = `${base64UrlEncodedKeyString}`;
|
||||||
|
const fullSizeHmacSalt = createHmacSha256(`${imgproxySalt}/${fullSizeProxyPath}`);
|
||||||
|
|
||||||
|
const s3Props = {};
|
||||||
|
if (!document.type.startsWith("image")) {
|
||||||
|
//If not a picture, we need to get a signed download link to the file using S3 (or cloudfront preferably)
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: imgproxyDestinationBucket,
|
||||||
|
Key: key
|
||||||
|
});
|
||||||
|
const presignedGetUrl = await getSignedUrl(s3client, command, { expiresIn: 360 });
|
||||||
|
s3Props.presignedGetUrl = presignedGetUrl;
|
||||||
|
|
||||||
|
const originalProxyPath = `raw:1/${base64UrlEncodedKeyString}`;
|
||||||
|
const originalHmacSalt = createHmacSha256(`${imgproxySalt}/${originalProxyPath}`);
|
||||||
|
s3Props.originalUrlViaProxyPath = `${imgproxyBaseUrl}/${originalHmacSalt}/${originalProxyPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxiedUrls.push({
|
||||||
|
originalUrl: `${imgproxyBaseUrl}/${fullSizeHmacSalt}/${fullSizeProxyPath}`,
|
||||||
|
thumbnailUrl: `${imgproxyBaseUrl}/${thumbHmacSalt}/${thumbProxyPath}`,
|
||||||
|
fullS3Path,
|
||||||
|
base64UrlEncodedKeyString,
|
||||||
|
thumbProxyPath,
|
||||||
|
...s3Props,
|
||||||
|
...document
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(proxiedUrls);
|
||||||
|
//Iterate over them, build the link based on the media type, and return the array.
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("imgproxy-thumbnails-error", "ERROR", req.user?.email, jobid, {
|
||||||
|
jobid,
|
||||||
|
billid,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
res.status(400).json({ message: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getBillFiles = async (req, res) => {
|
||||||
|
//Givena bill ID, get the documents associated to it.
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.downloadFiles = async (req, res) => {
|
||||||
|
//Given a series of document IDs or keys, generate a file (or a link) to download all images in bulk
|
||||||
|
const { jobid, billid, documentids } = req.body;
|
||||||
|
try {
|
||||||
|
logger.log("imgproxy-download", "DEBUG", req.user?.email, jobid, { billid, jobid, documentids });
|
||||||
|
|
||||||
|
//Delayed as the key structure may change slightly from what it is currently and will require evaluating mobile components.
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
//Query for the keys of the document IDs
|
||||||
|
const data = await client.request(GET_DOCUMENTS_BY_IDS, { documentIds: documentids });
|
||||||
|
//Using the Keys, get all of the S3 links, zip them, and send back to the client.
|
||||||
|
const s3client = new S3Client({ region: InstanceRegion() });
|
||||||
|
const archiveStream = archiver("zip");
|
||||||
|
archiveStream.on("error", (error) => {
|
||||||
|
console.error("Archival encountered an error:", error);
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
const passthrough = new stream.PassThrough();
|
||||||
|
|
||||||
|
archiveStream.pipe(passthrough);
|
||||||
|
for (const key of data.documents.map((d) => d.key)) {
|
||||||
|
const response = await s3client.send(new GetObjectCommand({ Bucket: imgproxyDestinationBucket, Key: key }));
|
||||||
|
// :: `response.Body` is a Buffer
|
||||||
|
console.log(path.basename(key));
|
||||||
|
archiveStream.append(response.Body, { name: path.basename(key) });
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveStream.finalize();
|
||||||
|
|
||||||
|
const archiveKey = `archives/${jobid}/archive-${new Date().toISOString()}.zip`;
|
||||||
|
|
||||||
|
const parallelUploads3 = new Upload({
|
||||||
|
client: s3client,
|
||||||
|
queueSize: 4, // optional concurrency configuration
|
||||||
|
leavePartsOnError: false, // optional manually handle dropped parts
|
||||||
|
params: { Bucket: imgproxyDestinationBucket, Key: archiveKey, Body: passthrough }
|
||||||
|
});
|
||||||
|
|
||||||
|
parallelUploads3.on("httpUploadProgress", (progress) => {
|
||||||
|
console.log(progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadResult = await parallelUploads3.done();
|
||||||
|
//Generate the presigned URL to download it.
|
||||||
|
const presignedUrl = await getSignedUrl(
|
||||||
|
s3client,
|
||||||
|
new GetObjectCommand({ Bucket: imgproxyDestinationBucket, Key: archiveKey }),
|
||||||
|
{ expiresIn: 360 }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true, url: presignedUrl });
|
||||||
|
//Iterate over them, build the link based on the media type, and return the array.
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("imgproxy-thumbnails-error", "ERROR", req.user?.email, jobid, {
|
||||||
|
jobid,
|
||||||
|
billid,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
res.status(400).json({ message: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.deleteFiles = async (req, res) => {
|
||||||
|
//Mark a file for deletion in s3. Lifecycle deletion will actually delete the copy in the future.
|
||||||
|
//Mark as deleted from the documents section of the database.
|
||||||
|
const { ids } = req.body;
|
||||||
|
try {
|
||||||
|
logger.log("imgproxy-delete-files", "DEBUG", req.user.email, null, { ids });
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
|
||||||
|
//Do this to make sure that they are only deleting things that they have access to
|
||||||
|
const data = await client.request(GET_DOCUMENTS_BY_IDS, { documentIds: ids });
|
||||||
|
|
||||||
|
const s3client = new S3Client({ region: InstanceRegion() });
|
||||||
|
|
||||||
|
const deleteTransactions = [];
|
||||||
|
data.documents.forEach((document) => {
|
||||||
|
deleteTransactions.push(
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// Delete the original object
|
||||||
|
const deleteResult = await s3client.send(
|
||||||
|
new DeleteObjectCommand({
|
||||||
|
Bucket: imgproxyDestinationBucket,
|
||||||
|
Key: document.key
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return document;
|
||||||
|
} catch (error) {
|
||||||
|
return { document, error: error, bucket: imgproxyDestinationBucket };
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await Promise.all(deleteTransactions);
|
||||||
|
const errors = result.filter((d) => d.error);
|
||||||
|
|
||||||
|
//Delete only the succesful deletes.
|
||||||
|
const deleteMutationResult = await client.request(DELETE_MEDIA_DOCUMENTS, {
|
||||||
|
ids: result.filter((t) => !t.error).map((d) => d.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ errors, deleteMutationResult });
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("imgproxy-delete-files-error", "ERROR", req.user.email, null, {
|
||||||
|
ids,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
res.status(400).json({ message: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.moveFiles = async (req, res) => {
|
||||||
|
const { documents, tojobid } = req.body;
|
||||||
|
try {
|
||||||
|
logger.log("imgproxy-move-files", "DEBUG", req.user.email, null, { documents, tojobid });
|
||||||
|
const s3client = new S3Client({ region: InstanceRegion() });
|
||||||
|
|
||||||
|
const moveTransactions = [];
|
||||||
|
documents.forEach((document) => {
|
||||||
|
moveTransactions.push(
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// Copy the object to the new key
|
||||||
|
const copyresult = await s3client.send(
|
||||||
|
new CopyObjectCommand({
|
||||||
|
Bucket: imgproxyDestinationBucket,
|
||||||
|
CopySource: `${imgproxyDestinationBucket}/${document.from}`,
|
||||||
|
Key: document.to,
|
||||||
|
StorageClass: "INTELLIGENT_TIERING"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete the original object
|
||||||
|
const deleteResult = await s3client.send(
|
||||||
|
new DeleteObjectCommand({
|
||||||
|
Bucket: imgproxyDestinationBucket,
|
||||||
|
Key: document.from
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return document;
|
||||||
|
} catch (error) {
|
||||||
|
return { id: document.id, from: document.from, error: error, bucket: imgproxyDestinationBucket };
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await Promise.all(moveTransactions);
|
||||||
|
const errors = result.filter((d) => d.error);
|
||||||
|
|
||||||
|
let mutations = "";
|
||||||
|
result
|
||||||
|
.filter((d) => !d.error)
|
||||||
|
.forEach((d, idx) => {
|
||||||
|
//Create mutation text
|
||||||
|
mutations =
|
||||||
|
mutations +
|
||||||
|
`
|
||||||
|
update_doc${idx}:update_documents_by_pk(pk_columns: { id: "${d.id}" }, _set: {key: "${d.to}", jobid: "${tojobid}"}){
|
||||||
|
id
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = req.userGraphQLClient;
|
||||||
|
if (mutations !== "") {
|
||||||
|
const mutationResult = await client.request(`mutation {
|
||||||
|
${mutations}
|
||||||
|
}`);
|
||||||
|
res.json({ errors, mutationResult });
|
||||||
|
} else {
|
||||||
|
res.json({ errors: "No images were succesfully moved on remote server. " });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("imgproxy-move-files-error", "ERROR", req.user.email, null, {
|
||||||
|
documents,
|
||||||
|
tojobid,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
res.status(400).json({ message: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function base64UrlEncode(str) {
|
||||||
|
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||||
|
}
|
||||||
|
function createHmacSha256(data) {
|
||||||
|
return crypto.createHmac("sha256", imgproxyKey).update(data).digest("base64url");
|
||||||
|
}
|
||||||
@@ -11,12 +11,14 @@ require("dotenv").config({
|
|||||||
var cloudinary = require("cloudinary").v2;
|
var cloudinary = require("cloudinary").v2;
|
||||||
cloudinary.config(process.env.CLOUDINARY_URL);
|
cloudinary.config(process.env.CLOUDINARY_URL);
|
||||||
|
|
||||||
exports.createSignedUploadURL = (req, res) => {
|
const createSignedUploadURL = (req, res) => {
|
||||||
logger.log("media-signed-upload", "DEBUG", req.user.email, null, null);
|
logger.log("media-signed-upload", "DEBUG", req.user.email, null, null);
|
||||||
res.send(cloudinary.utils.api_sign_request(req.body, process.env.CLOUDINARY_API_SECRET));
|
res.send(cloudinary.utils.api_sign_request(req.body, process.env.CLOUDINARY_API_SECRET));
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.downloadFiles = (req, res) => {
|
exports.createSignedUploadURL = createSignedUploadURL;
|
||||||
|
|
||||||
|
const downloadFiles = (req, res) => {
|
||||||
const { ids } = req.body;
|
const { ids } = req.body;
|
||||||
logger.log("media-bulk-download", "DEBUG", req.user.email, ids, null);
|
logger.log("media-bulk-download", "DEBUG", req.user.email, ids, null);
|
||||||
|
|
||||||
@@ -26,8 +28,9 @@ exports.downloadFiles = (req, res) => {
|
|||||||
});
|
});
|
||||||
res.send(url);
|
res.send(url);
|
||||||
};
|
};
|
||||||
|
exports.downloadFiles = downloadFiles;
|
||||||
|
|
||||||
exports.deleteFiles = async (req, res) => {
|
const deleteFiles = async (req, res) => {
|
||||||
const { ids } = req.body;
|
const { ids } = req.body;
|
||||||
const types = _.groupBy(ids, (x) => DetermineFileType(x.type));
|
const types = _.groupBy(ids, (x) => DetermineFileType(x.type));
|
||||||
|
|
||||||
@@ -88,7 +91,9 @@ exports.deleteFiles = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.renameKeys = async (req, res) => {
|
exports.deleteFiles = deleteFiles;
|
||||||
|
|
||||||
|
const renameKeys = async (req, res) => {
|
||||||
const { documents, tojobid } = req.body;
|
const { documents, tojobid } = req.body;
|
||||||
logger.log("media-bulk-rename", "DEBUG", req.user.email, null, documents);
|
logger.log("media-bulk-rename", "DEBUG", req.user.email, null, documents);
|
||||||
|
|
||||||
@@ -146,6 +151,7 @@ exports.renameKeys = async (req, res) => {
|
|||||||
res.json({ errors: "No images were succesfully moved on remote server. " });
|
res.json({ errors: "No images were succesfully moved on remote server. " });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
exports.renameKeys = renameKeys;
|
||||||
|
|
||||||
//Also needs to be updated in upload utility and mobile app.
|
//Also needs to be updated in upload utility and mobile app.
|
||||||
function DetermineFileType(filetype) {
|
function DetermineFileType(filetype) {
|
||||||
|
|||||||
@@ -160,6 +160,11 @@ async function OpenSearchUpdateHandler(req, res) {
|
|||||||
res.status(200).json(response.body);
|
res.status(200).json(response.body);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// We don't want this spam message existing in development/test,
|
||||||
|
if (process.env?.NODE_ENV !== "production" && error?.message === "Invalid URL") {
|
||||||
|
return res.status(400).json(JSON.stringify(error));
|
||||||
|
}
|
||||||
|
|
||||||
logger.log("os-handler-error", "ERROR", null, null, {
|
logger.log("os-handler-error", "ERROR", null, null, {
|
||||||
id: req.body.event.data.new.id,
|
id: req.body.event.data.new.id,
|
||||||
index: req.body.table.name,
|
index: req.body.table.name,
|
||||||
@@ -167,6 +172,7 @@ async function OpenSearchUpdateHandler(req, res) {
|
|||||||
stack: error.stack
|
stack: error.stack
|
||||||
// body: document
|
// body: document
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(400).json(JSON.stringify(error));
|
res.status(400).json(JSON.stringify(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { createSignedUploadURL, downloadFiles, renameKeys, deleteFiles } = require("../media/media");
|
const { createSignedUploadURL, downloadFiles, renameKeys, deleteFiles } = require("../media/media");
|
||||||
|
const {
|
||||||
|
generateSignedUploadUrls: createSignedUploadURLImgproxy,
|
||||||
|
getThumbnailUrls: getThumbnailUrlsImgproxy,
|
||||||
|
downloadFiles: downloadFilesImgproxy,
|
||||||
|
moveFiles: moveFilesImgproxy,
|
||||||
|
deleteFiles: deleteFilesImgproxy
|
||||||
|
} = require("../media/imgproxy-media");
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
|
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
||||||
|
|
||||||
router.use(validateFirebaseIdTokenMiddleware);
|
router.use(validateFirebaseIdTokenMiddleware);
|
||||||
|
router.use(withUserGraphQLClientMiddleware);
|
||||||
|
|
||||||
router.post("/sign", createSignedUploadURL);
|
router.post("/sign", createSignedUploadURL);
|
||||||
router.post("/download", downloadFiles);
|
router.post("/download", downloadFiles);
|
||||||
router.post("/rename", renameKeys);
|
router.post("/rename", renameKeys);
|
||||||
router.post("/delete", deleteFiles);
|
router.post("/delete", deleteFiles);
|
||||||
|
|
||||||
|
router.post("/imgproxy/sign", createSignedUploadURLImgproxy);
|
||||||
|
router.post("/imgproxy/thumbnails", getThumbnailUrlsImgproxy);
|
||||||
|
router.post("/imgproxy/download", downloadFilesImgproxy);
|
||||||
|
router.post("/imgproxy/rename", moveFilesImgproxy);
|
||||||
|
router.post("/imgproxy/delete", deleteFilesImgproxy);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
|||||||
const { canvastest } = require("../render/canvas-handler");
|
const { canvastest } = require("../render/canvas-handler");
|
||||||
const { alertCheck } = require("../alerts/alertcheck");
|
const { alertCheck } = require("../alerts/alertcheck");
|
||||||
const updateBodyshopCache = require("../web-sockets/updateBodyshopCache");
|
const updateBodyshopCache = require("../web-sockets/updateBodyshopCache");
|
||||||
const uuid = require("uuid").v4;
|
const { v4 } = require("uuid");
|
||||||
|
|
||||||
//Test route to ensure Express is responding.
|
//Test route to ensure Express is responding.
|
||||||
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
|
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
|
||||||
@@ -83,7 +83,7 @@ router.get("/wstest", eventAuthorizationMiddleware, (req, res) => {
|
|||||||
// image_path: [],
|
// image_path: [],
|
||||||
newMessage: {
|
newMessage: {
|
||||||
conversation: {
|
conversation: {
|
||||||
id: uuid(),
|
id: v4(),
|
||||||
archived: false,
|
archived: false,
|
||||||
bodyshop: {
|
bodyshop: {
|
||||||
id: "bfec8c8c-b7f1-49e0-be4c-524455f4e582",
|
id: "bfec8c8c-b7f1-49e0-be4c-524455f4e582",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const { status, markConversationRead } = require("../sms/status");
|
|||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
|
|
||||||
// Twilio Webhook Middleware for production
|
// Twilio Webhook Middleware for production
|
||||||
|
// TODO: Look into this because it technically is never validating anything
|
||||||
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
|
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
|
||||||
|
|
||||||
router.post("/receive", twilioWebhookMiddleware, receive);
|
router.post("/receive", twilioWebhookMiddleware, receive);
|
||||||
|
|||||||
Reference in New Issue
Block a user