element", () => {
+ // https://on.cypress.io/select
- // at first, no option should be selected
- cy.get('.action-select')
- .should('have.value', '--Select a fruit--')
+ // 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')
+ // 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'])
+ 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')
+ // 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'])
+ 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')
- })
+ // 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
+ 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')
+ // 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')
+ // 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')
+ cy.get("#scroll-vertical button").should("not.be.visible");
- // Cypress handles the scroll direction needed
- cy.get('#scroll-vertical button').scrollIntoView()
- .should('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')
+ 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')
- })
+ // 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
+ 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
+ // 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')
- })
+ // 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
+ 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 |
- // -----------------------------------
+ // 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')
+ // if you chain .scrollTo() off of cy, we will
+ // scroll the entire window
+ cy.scrollTo("bottom");
- cy.get('#scrollable-horizontal').scrollTo('right')
+ 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 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%')
+ // 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 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})
- })
-})
+ // control the duration of the scroll (in ms)
+ cy.get("#scrollable-both").scrollTo("center", { duration: 2000 });
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/aliasing.cy.js b/client/cypress/e2e/2-advanced-examples/aliasing.cy.js
index e56215eb7..152b8ece9 100644
--- a/client/cypress/e2e/2-advanced-examples/aliasing.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/aliasing.cy.js
@@ -1,39 +1,35 @@
///
-context('Aliasing', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/aliasing')
- })
+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
+ 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 @
+ // 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')
+ 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()
+ // 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')
- })
+ 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')
+ 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()
+ // 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)
- })
-})
+ // https://on.cypress.io/wait
+ cy.wait("@getComment").its("response.statusCode").should("eq", 200);
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/assertions.cy.js b/client/cypress/e2e/2-advanced-examples/assertions.cy.js
index 872ae8466..2bde3a9a0 100644
--- a/client/cypress/e2e/2-advanced-examples/assertions.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/assertions.cy.js
@@ -1,177 +1,173 @@
///
-context('Assertions', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/assertions')
- })
+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 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)
+ 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 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 element with text content matching regular expression
- .contains('td', /column content/i)
- .should('be.visible')
+ // 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 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
+ // 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('.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')
- })
- })
+ 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");
+ }
- 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'}
+ const className = $div[0].className;
- expect(o).to.equal(o)
- expect(o).to.deep.equal({foo: 'bar'})
- // matching text using regular expression
- expect('FooBar').to.match(/bar$/i)
- })
+ if (!className.match(/heading-/)) {
+ throw new Error(`Could not find class "heading-" in ${className}`);
+ }
+ });
+ });
- 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())
+ it("matches unknown text between two elements", () => {
+ /**
+ * Text from the first element.
+ * @type {string}
+ */
+ let text;
- // jquery map returns jquery object
- // and .get() convert this to simple array
- const paragraphs = texts.get()
+ /**
+ * 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();
- // array should have length of 3
- expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
+ cy.get(".two-elements")
+ .find(".first")
+ .then(($first) => {
+ // save text from the first element
+ text = normalizeText($first.text());
+ });
- // 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',
- ])
- })
- })
+ cy.get(".two-elements")
+ .find(".second")
+ .should(($div) => {
+ // we can massage text before comparing
+ const secondText = normalizeText($div.text());
- 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)
+ expect(secondText, "second text").to.equal(text);
+ });
+ });
- const className = $div[0].className
+ it("assert - assert shape of an object", () => {
+ const person = {
+ name: "Joe",
+ age: 20
+ };
- 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')
- })
- })
+ assert.isObject(person, "value is object");
+ });
- 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')
- }
+ it("retries the should callback until assertions pass", () => {
+ cy.get("#random-number").should(($div) => {
+ const n = parseFloat($div.text());
- 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)
- })
- })
- })
-})
+ expect(n).to.be.gte(1).and.be.lte(10);
+ });
+ });
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/connectors.cy.js b/client/cypress/e2e/2-advanced-examples/connectors.cy.js
index 45909bd9f..3cd60d308 100644
--- a/client/cypress/e2e/2-advanced-examples/connectors.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/connectors.cy.js
@@ -1,97 +1,96 @@
///
-context('Connectors', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/connectors')
- })
+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(".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(".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()
+ 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')
- })
+ // 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']
+ 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')
+ 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);
+ });
+ });
- 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 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 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 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);
})
-
- 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)
- })
- })
- })
-})
+ .then((num) => {
+ // this callback receives the value yielded by "cy.wrap(2)"
+ expect(num).to.equal(2);
+ });
+ });
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/cookies.cy.js b/client/cypress/e2e/2-advanced-examples/cookies.cy.js
index b62341a36..390ee76e2 100644
--- a/client/cypress/e2e/2-advanced-examples/cookies.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/cookies.cy.js
@@ -1,77 +1,79 @@
///
-context('Cookies', () => {
- beforeEach(() => {
- Cypress.Cookies.debug(true)
+context("Cookies", () => {
+ beforeEach(() => {
+ Cypress.Cookies.debug(true);
- cy.visit('https://example.cypress.io/commands/cookies')
+ 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()
- })
+ // 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()
+ 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')
- })
+ // 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')
+ it("cy.getCookies() - get browser cookies", () => {
+ // https://on.cypress.io/getcookies
+ cy.getCookies().should("be.empty");
- cy.get('#getCookies .set-a-cookie').click()
+ 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')
- })
- })
+ // 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')
+ it("cy.setCookie() - set a browser cookie", () => {
+ // https://on.cypress.io/setcookie
+ cy.getCookies().should("be.empty");
- cy.setCookie('foo', 'bar')
+ cy.setCookie("foo", "bar");
- // cy.getCookie() yields a cookie object
- cy.getCookie('foo').should('have.property', 'value', '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')
+ 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.get("#clearCookie .set-a-cookie").click();
- cy.getCookie('token').should('have.property', 'value', '123ABC')
+ cy.getCookie("token").should("have.property", "value", "123ABC");
- // cy.clearCookies() yields null
- cy.clearCookie('token').should('be.null')
+ // cy.clearCookies() yields null
+ cy.clearCookie("token").should("be.null");
- cy.getCookie('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')
+ it("cy.clearCookies() - clear browser cookies", () => {
+ // https://on.cypress.io/clearcookies
+ cy.getCookies().should("be.empty");
- cy.get('#clearCookies .set-a-cookie').click()
+ cy.get("#clearCookies .set-a-cookie").click();
- cy.getCookies().should('have.length', 1)
+ cy.getCookies().should("have.length", 1);
- // cy.clearCookies() yields null
- cy.clearCookies()
+ // cy.clearCookies() yields null
+ cy.clearCookies();
- cy.getCookies().should('be.empty')
- })
-})
+ cy.getCookies().should("be.empty");
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/cypress_api.cy.js b/client/cypress/e2e/2-advanced-examples/cypress_api.cy.js
index 913d58f4a..1cd9f975e 100644
--- a/client/cypress/e2e/2-advanced-examples/cypress_api.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/cypress_api.cy.js
@@ -1,202 +1,208 @@
///
-context('Cypress.Commands', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/cypress-api')
- })
+context("Cypress.Commands", () => {
+ beforeEach(() => {
+ cy.visit("https://example.cypress.io/cypress-api");
+ });
- // https://on.cypress.io/custom-commands
+ // 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
+ 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'
+ // 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)
+ // 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
- })
+ // 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
- })
- })
-})
+ // @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')
- })
+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)
+ // 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')
- })
+ // 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')
+ 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')
- })
+ // 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',
- })
- })
-})
+ 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')
- })
+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
- })
-})
+ 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')
- })
+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()
+ 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(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)
+ expect(Cypress.config("pageLoadTimeout")).to.eq(60000);
- // this will change the config for the rest of your tests!
- Cypress.config('pageLoadTimeout', 20000)
+ // this will change the config for the rest of your tests!
+ Cypress.config("pageLoadTimeout", 20000);
- expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
+ expect(Cypress.config("pageLoadTimeout")).to.eq(20000);
- Cypress.config('pageLoadTimeout', 60000)
- })
-})
+ Cypress.config("pageLoadTimeout", 60000);
+ });
+});
-context('Cypress.dom', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/cypress-api')
- })
+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)
+ // 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
- })
-})
+ // 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')
- })
+context("Cypress.env()", () => {
+ beforeEach(() => {
+ cy.visit("https://example.cypress.io/cypress-api");
+ });
- // We can set environment variables for highly dynamic values
+ // 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/',
- })
+ // 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')
+ // 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/')
+ // 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/')
- })
-})
+ // 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')
- })
+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
- })
-})
+ 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')
- })
+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
- })
-})
+ 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')
- })
+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
- })
-})
+ 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')
- })
+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'])
- })
-})
+ 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"]);
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/files.cy.js b/client/cypress/e2e/2-advanced-examples/files.cy.js
index 0c47da8bb..ad1bef656 100644
--- a/client/cypress/e2e/2-advanced-examples/files.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/files.cy.js
@@ -3,86 +3,84 @@
/// JSON fixture file can be loaded directly using
// the built-in JavaScript bundler
// @ts-ignore
-const requiredExample = require('../../fixtures/example')
+const requiredExample = require("../../fixtures/example");
-context('Files', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/files')
- })
+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')
- })
+ 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
+ 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.
+ // 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')
+ // 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()
+ // 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')
- })
+ 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)
+ 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)
- })
+ // 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
+ 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')
- })
- })
+ // 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
+ it("cy.writeFile() - write to a file", () => {
+ // https://on.cypress.io/writefile
- // You can write to a file
+ // 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)
- })
+ // 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
- })
+ 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',
- })
+ // 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')
- })
- })
-})
+ cy.fixture("profile").should((profile) => {
+ expect(profile.name).to.eq("Jane");
+ });
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/local_storage.cy.js b/client/cypress/e2e/2-advanced-examples/local_storage.cy.js
index b44a014c9..7007d02bf 100644
--- a/client/cypress/e2e/2-advanced-examples/local_storage.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/local_storage.cy.js
@@ -1,52 +1,58 @@
///
-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
+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')
- })
+ 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
- })
+ // 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')
- })
+ 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')
- })
+ // 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')
- })
+ 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')
- })
- })
-})
+ // 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");
+ });
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/location.cy.js b/client/cypress/e2e/2-advanced-examples/location.cy.js
index 165cdec94..f5e6230d6 100644
--- a/client/cypress/e2e/2-advanced-examples/location.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/location.cy.js
@@ -1,32 +1,32 @@
///
-context('Location', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/location')
- })
+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.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.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')
- })
-})
+ it("cy.url() - get the current URL", () => {
+ // https://on.cypress.io/url
+ cy.url().should("eq", "https://example.cypress.io/commands/location");
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/misc.cy.js b/client/cypress/e2e/2-advanced-examples/misc.cy.js
index 905261824..9ef98ae41 100644
--- a/client/cypress/e2e/2-advanced-examples/misc.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/misc.cy.js
@@ -1,106 +1,98 @@
///
-context('Misc', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/misc')
- })
+context("Misc", () => {
+ beforeEach(() => {
+ cy.visit("https://example.cypress.io/commands/misc");
+ });
- it('.end() - end the command chain', () => {
- // https://on.cypress.io/end
+ 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()
+ // 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()
- })
- })
+ // 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
+ 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}`)
+ // 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')
+ // 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')
+ if (isCircleOnWindows) {
+ cy.log("Skipping test on CircleCI");
- return
- }
+ return;
+ }
- // cy.exec problem on Shippable CI
- // https://github.com/cypress-io/cypress/issues/6718
- const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
+ // 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')
+ if (isShippable) {
+ cy.log("Skipping test on ShippableCI");
- return
- }
+ return;
+ }
- cy.exec('echo Jane Lane')
- .its('stdout').should('contain', 'Jane Lane')
+ 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')
+ 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)
- }
- })
+ 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')
+ 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')
- })
+ 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')
- })
+ 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("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')
- })
-})
+ it("cy.wrap() - wrap an object", () => {
+ // https://on.cypress.io/wrap
+ cy.wrap({ foo: "bar" }).should("have.property", "foo").and("include", "bar");
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/navigation.cy.js b/client/cypress/e2e/2-advanced-examples/navigation.cy.js
index 55303f309..a1993ee36 100644
--- a/client/cypress/e2e/2-advanced-examples/navigation.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/navigation.cy.js
@@ -1,56 +1,56 @@
///
-context('Navigation', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io')
- cy.get('.navbar-nav').contains('Commands').click()
- cy.get('.dropdown-menu').contains('Navigation').click()
- })
+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
+ it("cy.go() - go back or forward in the browser's history", () => {
+ // https://on.cypress.io/go
- cy.location('pathname').should('include', 'navigation')
+ cy.location("pathname").should("include", "navigation");
- cy.go('back')
- cy.location('pathname').should('not.include', 'navigation')
+ cy.go("back");
+ cy.location("pathname").should("not.include", "navigation");
- cy.go('forward')
- cy.location('pathname').should('include', 'navigation')
+ cy.go("forward");
+ cy.location("pathname").should("include", "navigation");
- // clicking back
- cy.go(-1)
- cy.location('pathname').should('not.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')
- })
+ // clicking forward
+ cy.go(1);
+ cy.location("pathname").should("include", "navigation");
+ });
- it('cy.reload() - reload the page', () => {
- // https://on.cypress.io/reload
- cy.reload()
+ it("cy.reload() - reload the page", () => {
+ // https://on.cypress.io/reload
+ cy.reload();
- // reload the page without using the cache
- cy.reload(true)
- })
+ // reload the page without using the cache
+ cy.reload(true);
+ });
- it('cy.visit() - visit a remote url', () => {
- // https://on.cypress.io/visit
+ it("cy.visit() - visit a remote url", () => {
+ // https://on.cypress.io/visit
- // Visit any sub-domain of your current domain
+ // 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
- },
- })
- })
-})
+ // 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;
+ }
+ });
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/network_requests.cy.js b/client/cypress/e2e/2-advanced-examples/network_requests.cy.js
index 02c2ae17a..693e5395d 100644
--- a/client/cypress/e2e/2-advanced-examples/network_requests.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/network_requests.cy.js
@@ -1,163 +1,165 @@
///
-context('Network Requests', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/network-requests')
+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
+ });
+ });
- // Manage HTTP requests in your app
+ 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"
+ });
- 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')
- })
- })
+ // 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);
- 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')
- })
- })
+ // 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() 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,
- },
+ 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')
- .should('be.an', 'array')
- .and('have.length', 1)
- .its('0') // yields first element of the array
- .should('contain', {
- postId: 1,
- id: 3,
- })
- })
+ .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.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',
- })
+ it("cy.intercept() - route responses to matching requests", () => {
+ // https://on.cypress.io/intercept
- // 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)
+ let message = "whoa, this comment does not exist";
- // 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')
- })
- })
+ // Listen to GET to comments/1
+ cy.intercept("GET", "**/comments/*").as("getComment");
- 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)
- })
- })
+ // we have code that gets a comment when
+ // the button is clicked in scripts.js
+ cy.get(".network-btn").click();
- it('cy.intercept() - route responses to matching requests', () => {
- // https://on.cypress.io/intercept
+ // https://on.cypress.io/wait
+ cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
- let message = 'whoa, this comment does not exist'
+ // Listen to POST to comments
+ cy.intercept("POST", "**/comments").as("postComment");
- // Listen to GET to comments/1
- cy.intercept('GET', '**/comments/*').as('getComment')
+ // 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()");
+ });
- // we have code that gets a comment when
- // the button is clicked in scripts.js
- cy.get('.network-btn').click()
+ // 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");
- // https://on.cypress.io/wait
- cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
+ // we have code that puts a comment when
+ // the button is clicked in scripts.js
+ cy.get(".network-put").click();
- // Listen to POST to comments
- cy.intercept('POST', '**/comments').as('postComment')
+ cy.wait("@putComment");
- // 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)
- })
-})
+ // our 404 statusCode logic in scripts.js executed
+ cy.get(".network-put-comment").should("contain", message);
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/querying.cy.js b/client/cypress/e2e/2-advanced-examples/querying.cy.js
index 809ccd675..35cf7c24a 100644
--- a/client/cypress/e2e/2-advanced-examples/querying.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/querying.cy.js
@@ -1,114 +1,100 @@
///
-context('Querying', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/querying')
- })
+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
+ // 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
+ 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('.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("#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('[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')
+ // '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 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')
- })
+ // 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')
+ 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')
+ // 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')
+ 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')
+ // 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')
- })
+ 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(".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
+ it("cy.root() - query the root DOM element", () => {
+ // https://on.cypress.io/root
- // By default, root is the document
- cy.root().should('match', 'html')
+ // 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')
- })
- })
+ 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()
+ 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()
+ // 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()
+ // 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()
+ // 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()
+ // 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()
+ // 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()
- })
- })
-})
+ // Best. Insulated from all changes.
+ cy.get("[data-cy=submit]").click();
+ });
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js b/client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js
index 4f2479f15..7c86af8fa 100644
--- a/client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js
@@ -2,205 +2,202 @@
// 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')
+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 obj = {
+ foo() {}
+ };
- const spy = cy.spy(obj, 'foo').as('anyArgs')
+ const spy = cy.spy(obj, "foo").as("anyArgs");
- obj.foo()
+ obj.foo();
- expect(spy).to.be.called
- })
+ expect(spy).to.be.called;
+ });
- it('cy.spy() retries until assertions pass', () => {
- cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
+ 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)
- },
- }
+ const obj = {
+ /**
+ * Prints the argument passed
+ * @param x {any}
+ */
+ foo(x) {
+ console.log("obj.foo called with", x);
+ }
+ };
- cy.spy(obj, 'foo').as('foo')
+ cy.spy(obj, "foo").as("foo");
- setTimeout(() => {
- obj.foo('first')
- }, 500)
+ setTimeout(() => {
+ obj.foo("first");
+ }, 500);
- setTimeout(() => {
- obj.foo('second')
- }, 2500)
+ setTimeout(() => {
+ obj.foo("second");
+ }, 2500);
- cy.get('@foo').should('have.been.calledTwice')
- })
+ 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')
+ 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 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')
+ const stub = cy.stub(obj, "foo").as("foo");
- obj.foo('foo', 'bar')
+ obj.foo("foo", "bar");
- expect(stub).to.be.called
- })
+ expect(stub).to.be.called;
+ });
- it('cy.clock() - control time in the browser', () => {
- // https://on.cypress.io/clock
+ 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()
+ // 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')
- })
+ 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
+ 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()
+ // 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.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')
- })
+ 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}!`
- },
- }
+ 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'))
+ 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
+ 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!')
- })
+ // 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
- },
- }
+ 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')
+ const spy = cy.spy(calculator, "add").as("add");
- expect(calculator.add(2, 3)).to.equal(5)
+ 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)
+ // 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)
+ // 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))
+ // 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
+ 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)
+ // 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
+ /**
+ * 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)
+ // 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 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
+ /**
+ * 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')),
- )
+ // 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)),
- )
+ 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))
+ // 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
+ // you can alias matchers for shorter test code
+ const { match: M } = Cypress.sinon;
- cy.get('@add').should('have.been.calledWith', M.number, M(3))
- })
-})
+ cy.get("@add").should("have.been.calledWith", M.number, M(3));
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/traversal.cy.js b/client/cypress/e2e/2-advanced-examples/traversal.cy.js
index 35d8108d2..e6b56fde1 100644
--- a/client/cypress/e2e/2-advanced-examples/traversal.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/traversal.cy.js
@@ -1,121 +1,97 @@
///
-context('Traversal', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/traversal')
- })
+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(".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(".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(".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(".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(".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(".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(".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(".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(".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(".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(".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(".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(".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(".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(".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(".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(".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)
- })
-})
+ it(".siblings() - get all sibling DOM elements", () => {
+ // https://on.cypress.io/siblings
+ cy.get(".traversal-pills .active").siblings().should("have.length", 2);
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/utilities.cy.js b/client/cypress/e2e/2-advanced-examples/utilities.cy.js
index d7d8d678f..c0c9e8196 100644
--- a/client/cypress/e2e/2-advanced-examples/utilities.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/utilities.cy.js
@@ -1,110 +1,108 @@
///
-context('Utilities', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/utilities')
- })
+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()
+ 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])
- })
- })
+ 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')
+ 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')
- })
+ 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 element and set its src to the dataUrl
- let img = Cypress.$(' ', {src: dataUrl})
+ 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 element and set its src to the dataUrl
+ let img = Cypress.$(" ", { 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)
+ // 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)
- })
- })
- })
+ 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,
- })
+ 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
+ expect(matching, "matching wildcard").to.be.true;
- matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
- matchBase: true,
- })
+ matching = Cypress.minimatch("/users/1/comments/2", "/users/*/comments", {
+ matchBase: true
+ });
- expect(matching, 'comments').to.be.false
+ 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,
- })
+ // ** 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
+ expect(matching, "comments").to.be.true;
- // whereas * matches only the next path segment
+ // whereas * matches only the next path segment
- matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
- matchBase: false,
- })
+ matching = Cypress.minimatch("/foo/bar/baz/123/quux?a=b&c=2", "/foo/*", {
+ matchBase: false
+ });
- expect(matching, 'comments').to.be.false
- })
+ expect(matching, "comments").to.be.false;
+ });
- it('Cypress.Promise - instantiate a bluebird promise', () => {
- // https://on.cypress.io/promise
- let waited = false
+ it("Cypress.Promise - instantiate a bluebird promise", () => {
+ // https://on.cypress.io/promise
+ let waited = false;
- /**
- * @return Bluebird
- */
- 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
+ /**
+ * @return Bluebird
+ */
+ 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)
- })
- }
+ // 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
- })
- })
- })
-})
+ 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;
+ });
+ });
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/viewport.cy.js b/client/cypress/e2e/2-advanced-examples/viewport.cy.js
index e640a1554..1d73b915d 100644
--- a/client/cypress/e2e/2-advanced-examples/viewport.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/viewport.cy.js
@@ -1,59 +1,59 @@
///
-context('Viewport', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/viewport')
- })
+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
+ it("cy.viewport() - set the viewport size and dimension", () => {
+ // https://on.cypress.io/viewport
- cy.get('#navbar').should('be.visible')
- cy.viewport(320, 480)
+ 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')
+ // 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)
+ // 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
+ // 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 :)
+ // 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("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)
+ // 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)
- })
-})
+ // The viewport will be reset back to the default dimensions
+ // in between tests (the default can be set in cypress.json)
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/waiting.cy.js b/client/cypress/e2e/2-advanced-examples/waiting.cy.js
index b33f679d2..49915ae75 100644
--- a/client/cypress/e2e/2-advanced-examples/waiting.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/waiting.cy.js
@@ -1,31 +1,31 @@
///
-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
+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)
- })
+ // 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')
+ 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()
+ // 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])
- })
-})
+ // wait for GET comments/1
+ cy.wait("@getComment").its("response.statusCode").should("be.oneOf", [200, 304]);
+ });
+});
diff --git a/client/cypress/e2e/2-advanced-examples/window.cy.js b/client/cypress/e2e/2-advanced-examples/window.cy.js
index d8b3b237e..9740ba049 100644
--- a/client/cypress/e2e/2-advanced-examples/window.cy.js
+++ b/client/cypress/e2e/2-advanced-examples/window.cy.js
@@ -1,22 +1,22 @@
///
-context('Window', () => {
- beforeEach(() => {
- cy.visit('https://example.cypress.io/commands/window')
- })
+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.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.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')
- })
-})
+ it("cy.title() - get the title", () => {
+ // https://on.cypress.io/title
+ cy.title().should("include", "Kitchen Sink");
+ });
+});
diff --git a/client/cypress/fixtures/profile.json b/client/cypress/fixtures/profile.json
index b6c355ca5..a95e88f9c 100644
--- a/client/cypress/fixtures/profile.json
+++ b/client/cypress/fixtures/profile.json
@@ -2,4 +2,4 @@
"id": 8739,
"name": "Jane",
"email": "jane@example.com"
-}
\ No newline at end of file
+}
diff --git a/client/cypress/plugins/index.js b/client/cypress/plugins/index.js
index fe51f29d4..8229063ad 100644
--- a/client/cypress/plugins/index.js
+++ b/client/cypress/plugins/index.js
@@ -17,6 +17,6 @@
*/
// 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
-}
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+};
diff --git a/client/cypress/support/e2e.js b/client/cypress/support/e2e.js
index d68db96df..d076cec9f 100644
--- a/client/cypress/support/e2e.js
+++ b/client/cypress/support/e2e.js
@@ -14,7 +14,7 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
-import './commands'
+import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
diff --git a/client/cypress/tsconfig.json b/client/cypress/tsconfig.json
index 8e6ac9683..36de33dee 100644
--- a/client/cypress/tsconfig.json
+++ b/client/cypress/tsconfig.json
@@ -2,11 +2,7 @@
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
- "types": [
- "cypress"
- ]
+ "types": ["cypress"]
},
- "include": [
- "**/*.*"
- ]
+ "include": ["**/*.*"]
}
diff --git a/client/dev-dist/registerSW.js b/client/dev-dist/registerSW.js
index 1d5625f45..f17aa66d6 100644
--- a/client/dev-dist/registerSW.js
+++ b/client/dev-dist/registerSW.js
@@ -1 +1,2 @@
-if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
\ No newline at end of file
+if ("serviceWorker" in navigator)
+ navigator.serviceWorker.register("/dev-sw.js?dev-sw", { scope: "/", type: "classic" });
diff --git a/client/dev-dist/sw.js b/client/dev-dist/sw.js
index d14df90f7..0cc1553d3 100644
--- a/client/dev-dist/sw.js
+++ b/client/dev-dist/sw.js
@@ -21,22 +21,20 @@ if (!self.define) {
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
- return registry[uri] || (
-
- new Promise(resolve => {
- if ("document" in self) {
- const script = document.createElement("script");
- script.src = uri;
- script.onload = resolve;
- document.head.appendChild(script);
- } else {
- nextDefineUri = uri;
- importScripts(uri);
- resolve();
- }
- })
-
- .then(() => {
+ return (
+ registry[uri] ||
+ new Promise((resolve) => {
+ if ("document" in self) {
+ const script = document.createElement("script");
+ script.src = uri;
+ script.onload = resolve;
+ document.head.appendChild(script);
+ } else {
+ nextDefineUri = uri;
+ importScripts(uri);
+ resolve();
+ }
+ }).then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didn’t register its module`);
@@ -53,21 +51,20 @@ if (!self.define) {
return;
}
let exports = {};
- const require = depUri => singleRequire(depUri, uri);
+ const require = (depUri) => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
- registry[uri] = Promise.all(depsNames.map(
- depName => specialDeps[depName] || require(depName)
- )).then(deps => {
+ registry[uri] = Promise.all(depsNames.map((depName) => specialDeps[depName] || require(depName))).then((deps) => {
factory(...deps);
return exports;
});
};
}
-define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
+define(["./workbox-b5f7729d"], function (workbox) {
+ "use strict";
self.skipWaiting();
workbox.clientsClaim();
@@ -77,16 +74,23 @@ define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
- workbox.precacheAndRoute([{
- "url": "registerSW.js",
- "revision": "3ca0b8505b4bec776b69afdba2768812"
- }, {
- "url": "index.html",
- "revision": "0.sa702m4aq68"
- }], {});
+ workbox.precacheAndRoute(
+ [
+ {
+ url: "registerSW.js",
+ revision: "3ca0b8505b4bec776b69afdba2768812"
+ },
+ {
+ url: "index.html",
+ revision: "0.sa702m4aq68"
+ }
+ ],
+ {}
+ );
workbox.cleanupOutdatedCaches();
- workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
- allowlist: [/^\/$/]
- }));
-
-}));
+ workbox.registerRoute(
+ new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
+ allowlist: [/^\/$/]
+ })
+ );
+});
diff --git a/client/dev-dist/workbox-b5f7729d.js b/client/dev-dist/workbox-b5f7729d.js
index 077fa26dd..a0a2a61d8 100644
--- a/client/dev-dist/workbox-b5f7729d.js
+++ b/client/dev-dist/workbox-b5f7729d.js
@@ -1,782 +1,780 @@
-define(['exports'], (function (exports) { 'use strict';
+define(["exports"], function (exports) {
+ "use strict";
- // @ts-ignore
- try {
- self['workbox:core:7.0.0'] && _();
- } catch (e) {}
+ // @ts-ignore
+ try {
+ self["workbox:core:7.0.0"] && _();
+ } catch (e) {}
- /*
+ /*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
- /**
- * Claim any currently available clients once the service worker
- * becomes active. This is normally used in conjunction with `skipWaiting()`.
- *
- * @memberof workbox-core
- */
- function clientsClaim() {
- self.addEventListener('activate', () => self.clients.claim());
+ /**
+ * Claim any currently available clients once the service worker
+ * becomes active. This is normally used in conjunction with `skipWaiting()`.
+ *
+ * @memberof workbox-core
+ */
+ function clientsClaim() {
+ self.addEventListener("activate", () => self.clients.claim());
+ }
+
+ /*
+ Copyright 2019 Google LLC
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const logger = (() => {
+ // Don't overwrite this value if it's already set.
+ // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923
+ if (!("__WB_DISABLE_DEV_LOGS" in globalThis)) {
+ self.__WB_DISABLE_DEV_LOGS = false;
}
-
- /*
- Copyright 2019 Google LLC
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- const logger = (() => {
- // Don't overwrite this value if it's already set.
- // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923
- if (!('__WB_DISABLE_DEV_LOGS' in globalThis)) {
- self.__WB_DISABLE_DEV_LOGS = false;
+ let inGroup = false;
+ const methodToColorMap = {
+ debug: `#7f8c8d`,
+ log: `#2ecc71`,
+ warn: `#f39c12`,
+ error: `#c0392b`,
+ groupCollapsed: `#3498db`,
+ groupEnd: null // No colored prefix on groupEnd
+ };
+ const print = function (method, args) {
+ if (self.__WB_DISABLE_DEV_LOGS) {
+ return;
}
- let inGroup = false;
- const methodToColorMap = {
- debug: `#7f8c8d`,
- log: `#2ecc71`,
- warn: `#f39c12`,
- error: `#c0392b`,
- groupCollapsed: `#3498db`,
- groupEnd: null // No colored prefix on groupEnd
- };
- const print = function (method, args) {
- if (self.__WB_DISABLE_DEV_LOGS) {
+ if (method === "groupCollapsed") {
+ // Safari doesn't print all console.groupCollapsed() arguments:
+ // https://bugs.webkit.org/show_bug.cgi?id=182754
+ if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
+ console[method](...args);
return;
}
- if (method === 'groupCollapsed') {
- // Safari doesn't print all console.groupCollapsed() arguments:
- // https://bugs.webkit.org/show_bug.cgi?id=182754
- if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
- console[method](...args);
- return;
- }
- }
- const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`];
- // When in a group, the workbox prefix is not displayed.
- const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')];
- console[method](...logPrefix, ...args);
- if (method === 'groupCollapsed') {
- inGroup = true;
- }
- if (method === 'groupEnd') {
- inGroup = false;
- }
+ }
+ const styles = [
+ `background: ${methodToColorMap[method]}`,
+ `border-radius: 0.5em`,
+ `color: white`,
+ `font-weight: bold`,
+ `padding: 2px 0.5em`
+ ];
+ // When in a group, the workbox prefix is not displayed.
+ const logPrefix = inGroup ? [] : ["%cworkbox", styles.join(";")];
+ console[method](...logPrefix, ...args);
+ if (method === "groupCollapsed") {
+ inGroup = true;
+ }
+ if (method === "groupEnd") {
+ inGroup = false;
+ }
+ };
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ const api = {};
+ const loggerMethods = Object.keys(methodToColorMap);
+ for (const key of loggerMethods) {
+ const method = key;
+ api[method] = (...args) => {
+ print(method, args);
};
- // eslint-disable-next-line @typescript-eslint/ban-types
- const api = {};
- const loggerMethods = Object.keys(methodToColorMap);
- for (const key of loggerMethods) {
- const method = key;
- api[method] = (...args) => {
- print(method, args);
- };
- }
- return api;
- })();
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- const messages = {
- 'invalid-value': ({
- paramName,
- validValueDescription,
- value
- }) => {
- if (!paramName || !validValueDescription) {
- throw new Error(`Unexpected input to 'invalid-value' error.`);
- }
- return `The '${paramName}' parameter was given a value with an ` + `unexpected value. ${validValueDescription} Received a value of ` + `${JSON.stringify(value)}.`;
- },
- 'not-an-array': ({
- moduleName,
- className,
- funcName,
- paramName
- }) => {
- if (!moduleName || !className || !funcName || !paramName) {
- throw new Error(`Unexpected input to 'not-an-array' error.`);
- }
- return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.`;
- },
- 'incorrect-type': ({
- expectedType,
- paramName,
- moduleName,
- className,
- funcName
- }) => {
- if (!expectedType || !paramName || !moduleName || !funcName) {
- throw new Error(`Unexpected input to 'incorrect-type' error.`);
- }
- const classNameStr = className ? `${className}.` : '';
- return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}` + `${funcName}()' must be of type ${expectedType}.`;
- },
- 'incorrect-class': ({
- expectedClassName,
- paramName,
- moduleName,
- className,
- funcName,
- isReturnValueProblem
- }) => {
- if (!expectedClassName || !moduleName || !funcName) {
- throw new Error(`Unexpected input to 'incorrect-class' error.`);
- }
- const classNameStr = className ? `${className}.` : '';
- if (isReturnValueProblem) {
- return `The return value from ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`;
- }
- return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`;
- },
- 'missing-a-method': ({
- expectedMethod,
- paramName,
- moduleName,
- className,
- funcName
- }) => {
- if (!expectedMethod || !paramName || !moduleName || !className || !funcName) {
- throw new Error(`Unexpected input to 'missing-a-method' error.`);
- }
- return `${moduleName}.${className}.${funcName}() expected the ` + `'${paramName}' parameter to expose a '${expectedMethod}' method.`;
- },
- 'add-to-cache-list-unexpected-type': ({
- entry
- }) => {
- return `An unexpected entry was passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` + `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` + `strings with one or more characters, objects with a url property or ` + `Request objects.`;
- },
- 'add-to-cache-list-conflicting-entries': ({
- firstEntry,
- secondEntry
- }) => {
- if (!firstEntry || !secondEntry) {
- throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`);
- }
- return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${firstEntry} but different revision details. Workbox is ` + `unable to cache and version the asset correctly. Please remove one ` + `of the entries.`;
- },
- 'plugin-error-request-will-fetch': ({
- thrownErrorMessage
- }) => {
- if (!thrownErrorMessage) {
- throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`);
- }
- return `An error was thrown by a plugins 'requestWillFetch()' method. ` + `The thrown error message was: '${thrownErrorMessage}'.`;
- },
- 'invalid-cache-name': ({
- cacheNameId,
- value
- }) => {
- if (!cacheNameId) {
- throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`);
- }
- return `You must provide a name containing at least one character for ` + `setCacheDetails({${cacheNameId}: '...'}). Received a value of ` + `'${JSON.stringify(value)}'`;
- },
- 'unregister-route-but-not-found-with-method': ({
- method
- }) => {
- if (!method) {
- throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`);
- }
- return `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.`;
- },
- 'unregister-route-route-not-registered': () => {
- return `The route you're trying to unregister was not previously ` + `registered.`;
- },
- 'queue-replay-failed': ({
- name
- }) => {
- return `Replaying the background sync queue '${name}' failed.`;
- },
- 'duplicate-queue-name': ({
- name
- }) => {
- return `The Queue name '${name}' is already being used. ` + `All instances of backgroundSync.Queue must be given unique names.`;
- },
- 'expired-test-without-max-age': ({
- methodName,
- paramName
- }) => {
- return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`;
- },
- 'unsupported-route-type': ({
- moduleName,
- className,
- funcName,
- paramName
- }) => {
- return `The supplied '${paramName}' parameter was an unsupported type. ` + `Please check the docs for ${moduleName}.${className}.${funcName} for ` + `valid input types.`;
- },
- 'not-array-of-class': ({
- value,
- expectedClass,
- moduleName,
- className,
- funcName,
- paramName
- }) => {
- return `The supplied '${paramName}' parameter must be an array of ` + `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` + `Please check the call to ${moduleName}.${className}.${funcName}() ` + `to fix the issue.`;
- },
- 'max-entries-or-age-required': ({
- moduleName,
- className,
- funcName
- }) => {
- return `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}`;
- },
- 'statuses-or-headers-required': ({
- moduleName,
- className,
- funcName
- }) => {
- return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`;
- },
- 'invalid-string': ({
- moduleName,
- funcName,
- paramName
- }) => {
- if (!paramName || !moduleName || !funcName) {
- throw new Error(`Unexpected input to 'invalid-string' error.`);
- }
- return `When using strings, the '${paramName}' parameter must start with ` + `'http' (for cross-origin matches) or '/' (for same-origin matches). ` + `Please see the docs for ${moduleName}.${funcName}() for ` + `more info.`;
- },
- 'channel-name-required': () => {
- return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`;
- },
- 'invalid-responses-are-same-args': () => {
- return `The arguments passed into responsesAreSame() appear to be ` + `invalid. Please ensure valid Responses are used.`;
- },
- 'expire-custom-caches-only': () => {
- return `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.`;
- },
- 'unit-must-be-bytes': ({
- normalizedRangeHeader
- }) => {
- if (!normalizedRangeHeader) {
- throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`);
- }
- return `The 'unit' portion of the Range header must be set to 'bytes'. ` + `The Range header provided was "${normalizedRangeHeader}"`;
- },
- 'single-range-only': ({
- normalizedRangeHeader
- }) => {
- if (!normalizedRangeHeader) {
- throw new Error(`Unexpected input to 'single-range-only' error.`);
- }
- return `Multiple ranges are not supported. Please use a single start ` + `value, and optional end value. The Range header provided was ` + `"${normalizedRangeHeader}"`;
- },
- 'invalid-range-values': ({
- normalizedRangeHeader
- }) => {
- if (!normalizedRangeHeader) {
- throw new Error(`Unexpected input to 'invalid-range-values' error.`);
- }
- return `The Range header is missing both start and end values. At least ` + `one of those values is needed. The Range header provided was ` + `"${normalizedRangeHeader}"`;
- },
- 'no-range-header': () => {
- return `No Range header was found in the Request provided.`;
- },
- 'range-not-satisfiable': ({
- size,
- start,
- end
- }) => {
- return `The start (${start}) and end (${end}) values in the Range are ` + `not satisfiable by the cached response, which is ${size} bytes.`;
- },
- 'attempt-to-cache-non-get-request': ({
- url,
- method
- }) => {
- return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`;
- },
- 'cache-put-with-no-response': ({
- url
- }) => {
- return `There was an attempt to cache '${url}' but the response was not ` + `defined.`;
- },
- 'no-response': ({
- url,
- error
- }) => {
- let message = `The strategy could not generate a response for '${url}'.`;
- if (error) {
- message += ` The underlying error is ${error}.`;
- }
- return message;
- },
- 'bad-precaching-response': ({
- url,
- status
- }) => {
- return `The precaching request for '${url}' failed` + (status ? ` with an HTTP status of ${status}.` : `.`);
- },
- 'non-precached-url': ({
- url
- }) => {
- return `createHandlerBoundToURL('${url}') was called, but that URL is ` + `not precached. Please pass in a URL that is precached instead.`;
- },
- 'add-to-cache-list-conflicting-integrities': ({
- url
- }) => {
- return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${url} with different integrity values. Please remove one of them.`;
- },
- 'missing-precache-entry': ({
- cacheName,
- url
- }) => {
- return `Unable to find a precached response in ${cacheName} for ${url}.`;
- },
- 'cross-origin-copy-response': ({
- origin
- }) => {
- return `workbox-core.copyResponse() can only be used with same-origin ` + `responses. It was passed a response with origin ${origin}.`;
- },
- 'opaque-streams-source': ({
- type
- }) => {
- const message = `One of the workbox-streams sources resulted in an ` + `'${type}' response.`;
- if (type === 'opaqueredirect') {
- return `${message} Please do not use a navigation request that results ` + `in a redirect as a source.`;
- }
- return `${message} Please ensure your sources are CORS-enabled.`;
- }
- };
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- const generatorFunction = (code, details = {}) => {
- const message = messages[code];
- if (!message) {
- throw new Error(`Unable to find message for code '${code}'.`);
- }
- return message(details);
- };
- const messageGenerator = generatorFunction;
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * Workbox errors should be thrown with this class.
- * This allows use to ensure the type easily in tests,
- * helps developers identify errors from workbox
- * easily and allows use to optimise error
- * messages correctly.
- *
- * @private
- */
- class WorkboxError extends Error {
- /**
- *
- * @param {string} errorCode The error code that
- * identifies this particular error.
- * @param {Object=} details Any relevant arguments
- * that will help developers identify issues should
- * be added as a key on the context object.
- */
- constructor(errorCode, details) {
- const message = messageGenerator(errorCode, details);
- super(message);
- this.name = errorCode;
- this.details = details;
- }
}
+ return api;
+ })();
- /*
+ /*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
- /*
- * This method throws if the supplied value is not an array.
- * The destructed values are required to produce a meaningful error for users.
- * The destructed and restructured object is so it's clear what is
- * needed.
+ const messages = {
+ "invalid-value": ({ paramName, validValueDescription, value }) => {
+ if (!paramName || !validValueDescription) {
+ throw new Error(`Unexpected input to 'invalid-value' error.`);
+ }
+ return (
+ `The '${paramName}' parameter was given a value with an ` +
+ `unexpected value. ${validValueDescription} Received a value of ` +
+ `${JSON.stringify(value)}.`
+ );
+ },
+ "not-an-array": ({ moduleName, className, funcName, paramName }) => {
+ if (!moduleName || !className || !funcName || !paramName) {
+ throw new Error(`Unexpected input to 'not-an-array' error.`);
+ }
+ return (
+ `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.`
+ );
+ },
+ "incorrect-type": ({ expectedType, paramName, moduleName, className, funcName }) => {
+ if (!expectedType || !paramName || !moduleName || !funcName) {
+ throw new Error(`Unexpected input to 'incorrect-type' error.`);
+ }
+ const classNameStr = className ? `${className}.` : "";
+ return (
+ `The parameter '${paramName}' passed into ` +
+ `'${moduleName}.${classNameStr}` +
+ `${funcName}()' must be of type ${expectedType}.`
+ );
+ },
+ "incorrect-class": ({ expectedClassName, paramName, moduleName, className, funcName, isReturnValueProblem }) => {
+ if (!expectedClassName || !moduleName || !funcName) {
+ throw new Error(`Unexpected input to 'incorrect-class' error.`);
+ }
+ const classNameStr = className ? `${className}.` : "";
+ if (isReturnValueProblem) {
+ return (
+ `The return value from ` +
+ `'${moduleName}.${classNameStr}${funcName}()' ` +
+ `must be an instance of class ${expectedClassName}.`
+ );
+ }
+ return (
+ `The parameter '${paramName}' passed into ` +
+ `'${moduleName}.${classNameStr}${funcName}()' ` +
+ `must be an instance of class ${expectedClassName}.`
+ );
+ },
+ "missing-a-method": ({ expectedMethod, paramName, moduleName, className, funcName }) => {
+ if (!expectedMethod || !paramName || !moduleName || !className || !funcName) {
+ throw new Error(`Unexpected input to 'missing-a-method' error.`);
+ }
+ return (
+ `${moduleName}.${className}.${funcName}() expected the ` +
+ `'${paramName}' parameter to expose a '${expectedMethod}' method.`
+ );
+ },
+ "add-to-cache-list-unexpected-type": ({ entry }) => {
+ return (
+ `An unexpected entry was passed to ` +
+ `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` +
+ `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` +
+ `strings with one or more characters, objects with a url property or ` +
+ `Request objects.`
+ );
+ },
+ "add-to-cache-list-conflicting-entries": ({ firstEntry, secondEntry }) => {
+ if (!firstEntry || !secondEntry) {
+ throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`);
+ }
+ return (
+ `Two of the entries passed to ` +
+ `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` +
+ `${firstEntry} but different revision details. Workbox is ` +
+ `unable to cache and version the asset correctly. Please remove one ` +
+ `of the entries.`
+ );
+ },
+ "plugin-error-request-will-fetch": ({ thrownErrorMessage }) => {
+ if (!thrownErrorMessage) {
+ throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`);
+ }
+ return (
+ `An error was thrown by a plugins 'requestWillFetch()' method. ` +
+ `The thrown error message was: '${thrownErrorMessage}'.`
+ );
+ },
+ "invalid-cache-name": ({ cacheNameId, value }) => {
+ if (!cacheNameId) {
+ throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`);
+ }
+ return (
+ `You must provide a name containing at least one character for ` +
+ `setCacheDetails({${cacheNameId}: '...'}). Received a value of ` +
+ `'${JSON.stringify(value)}'`
+ );
+ },
+ "unregister-route-but-not-found-with-method": ({ method }) => {
+ if (!method) {
+ throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`);
+ }
+ return (
+ `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.`
+ );
+ },
+ "unregister-route-route-not-registered": () => {
+ return `The route you're trying to unregister was not previously ` + `registered.`;
+ },
+ "queue-replay-failed": ({ name }) => {
+ return `Replaying the background sync queue '${name}' failed.`;
+ },
+ "duplicate-queue-name": ({ name }) => {
+ return (
+ `The Queue name '${name}' is already being used. ` +
+ `All instances of backgroundSync.Queue must be given unique names.`
+ );
+ },
+ "expired-test-without-max-age": ({ methodName, paramName }) => {
+ return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`;
+ },
+ "unsupported-route-type": ({ moduleName, className, funcName, paramName }) => {
+ return (
+ `The supplied '${paramName}' parameter was an unsupported type. ` +
+ `Please check the docs for ${moduleName}.${className}.${funcName} for ` +
+ `valid input types.`
+ );
+ },
+ "not-array-of-class": ({ value, expectedClass, moduleName, className, funcName, paramName }) => {
+ return (
+ `The supplied '${paramName}' parameter must be an array of ` +
+ `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` +
+ `Please check the call to ${moduleName}.${className}.${funcName}() ` +
+ `to fix the issue.`
+ );
+ },
+ "max-entries-or-age-required": ({ moduleName, className, funcName }) => {
+ return (
+ `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}`
+ );
+ },
+ "statuses-or-headers-required": ({ moduleName, className, funcName }) => {
+ return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`;
+ },
+ "invalid-string": ({ moduleName, funcName, paramName }) => {
+ if (!paramName || !moduleName || !funcName) {
+ throw new Error(`Unexpected input to 'invalid-string' error.`);
+ }
+ return (
+ `When using strings, the '${paramName}' parameter must start with ` +
+ `'http' (for cross-origin matches) or '/' (for same-origin matches). ` +
+ `Please see the docs for ${moduleName}.${funcName}() for ` +
+ `more info.`
+ );
+ },
+ "channel-name-required": () => {
+ return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`;
+ },
+ "invalid-responses-are-same-args": () => {
+ return (
+ `The arguments passed into responsesAreSame() appear to be ` +
+ `invalid. Please ensure valid Responses are used.`
+ );
+ },
+ "expire-custom-caches-only": () => {
+ return (
+ `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.`
+ );
+ },
+ "unit-must-be-bytes": ({ normalizedRangeHeader }) => {
+ if (!normalizedRangeHeader) {
+ throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`);
+ }
+ return (
+ `The 'unit' portion of the Range header must be set to 'bytes'. ` +
+ `The Range header provided was "${normalizedRangeHeader}"`
+ );
+ },
+ "single-range-only": ({ normalizedRangeHeader }) => {
+ if (!normalizedRangeHeader) {
+ throw new Error(`Unexpected input to 'single-range-only' error.`);
+ }
+ return (
+ `Multiple ranges are not supported. Please use a single start ` +
+ `value, and optional end value. The Range header provided was ` +
+ `"${normalizedRangeHeader}"`
+ );
+ },
+ "invalid-range-values": ({ normalizedRangeHeader }) => {
+ if (!normalizedRangeHeader) {
+ throw new Error(`Unexpected input to 'invalid-range-values' error.`);
+ }
+ return (
+ `The Range header is missing both start and end values. At least ` +
+ `one of those values is needed. The Range header provided was ` +
+ `"${normalizedRangeHeader}"`
+ );
+ },
+ "no-range-header": () => {
+ return `No Range header was found in the Request provided.`;
+ },
+ "range-not-satisfiable": ({ size, start, end }) => {
+ return (
+ `The start (${start}) and end (${end}) values in the Range are ` +
+ `not satisfiable by the cached response, which is ${size} bytes.`
+ );
+ },
+ "attempt-to-cache-non-get-request": ({ url, method }) => {
+ return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`;
+ },
+ "cache-put-with-no-response": ({ url }) => {
+ return `There was an attempt to cache '${url}' but the response was not ` + `defined.`;
+ },
+ "no-response": ({ url, error }) => {
+ let message = `The strategy could not generate a response for '${url}'.`;
+ if (error) {
+ message += ` The underlying error is ${error}.`;
+ }
+ return message;
+ },
+ "bad-precaching-response": ({ url, status }) => {
+ return `The precaching request for '${url}' failed` + (status ? ` with an HTTP status of ${status}.` : `.`);
+ },
+ "non-precached-url": ({ url }) => {
+ return (
+ `createHandlerBoundToURL('${url}') was called, but that URL is ` +
+ `not precached. Please pass in a URL that is precached instead.`
+ );
+ },
+ "add-to-cache-list-conflicting-integrities": ({ url }) => {
+ return (
+ `Two of the entries passed to ` +
+ `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` +
+ `${url} with different integrity values. Please remove one of them.`
+ );
+ },
+ "missing-precache-entry": ({ cacheName, url }) => {
+ return `Unable to find a precached response in ${cacheName} for ${url}.`;
+ },
+ "cross-origin-copy-response": ({ origin }) => {
+ return (
+ `workbox-core.copyResponse() can only be used with same-origin ` +
+ `responses. It was passed a response with origin ${origin}.`
+ );
+ },
+ "opaque-streams-source": ({ type }) => {
+ const message = `One of the workbox-streams sources resulted in an ` + `'${type}' response.`;
+ if (type === "opaqueredirect") {
+ return `${message} Please do not use a navigation request that results ` + `in a redirect as a source.`;
+ }
+ return `${message} Please ensure your sources are CORS-enabled.`;
+ }
+ };
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const generatorFunction = (code, details = {}) => {
+ const message = messages[code];
+ if (!message) {
+ throw new Error(`Unable to find message for code '${code}'.`);
+ }
+ return message(details);
+ };
+ const messageGenerator = generatorFunction;
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Workbox errors should be thrown with this class.
+ * This allows use to ensure the type easily in tests,
+ * helps developers identify errors from workbox
+ * easily and allows use to optimise error
+ * messages correctly.
+ *
+ * @private
+ */
+ class WorkboxError extends Error {
+ /**
+ *
+ * @param {string} errorCode The error code that
+ * identifies this particular error.
+ * @param {Object=} details Any relevant arguments
+ * that will help developers identify issues should
+ * be added as a key on the context object.
*/
- const isArray = (value, details) => {
- if (!Array.isArray(value)) {
- throw new WorkboxError('not-an-array', details);
- }
- };
- const hasMethod = (object, expectedMethod, details) => {
- const type = typeof object[expectedMethod];
- if (type !== 'function') {
- details['expectedMethod'] = expectedMethod;
- throw new WorkboxError('missing-a-method', details);
- }
- };
- const isType = (object, expectedType, details) => {
- if (typeof object !== expectedType) {
- details['expectedType'] = expectedType;
- throw new WorkboxError('incorrect-type', details);
- }
- };
- const isInstance = (object,
+ constructor(errorCode, details) {
+ const message = messageGenerator(errorCode, details);
+ super(message);
+ this.name = errorCode;
+ this.details = details;
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /*
+ * This method throws if the supplied value is not an array.
+ * The destructed values are required to produce a meaningful error for users.
+ * The destructed and restructured object is so it's clear what is
+ * needed.
+ */
+ const isArray = (value, details) => {
+ if (!Array.isArray(value)) {
+ throw new WorkboxError("not-an-array", details);
+ }
+ };
+ const hasMethod = (object, expectedMethod, details) => {
+ const type = typeof object[expectedMethod];
+ if (type !== "function") {
+ details["expectedMethod"] = expectedMethod;
+ throw new WorkboxError("missing-a-method", details);
+ }
+ };
+ const isType = (object, expectedType, details) => {
+ if (typeof object !== expectedType) {
+ details["expectedType"] = expectedType;
+ throw new WorkboxError("incorrect-type", details);
+ }
+ };
+ const isInstance = (
+ object,
// Need the general type to do the check later.
// eslint-disable-next-line @typescript-eslint/ban-types
- expectedClass, details) => {
- if (!(object instanceof expectedClass)) {
- details['expectedClassName'] = expectedClass.name;
- throw new WorkboxError('incorrect-class', details);
- }
- };
- const isOneOf = (value, validValues, details) => {
- if (!validValues.includes(value)) {
- details['validValueDescription'] = `Valid values are ${JSON.stringify(validValues)}.`;
- throw new WorkboxError('invalid-value', details);
- }
- };
- const isArrayOfClass = (value,
+ expectedClass,
+ details
+ ) => {
+ if (!(object instanceof expectedClass)) {
+ details["expectedClassName"] = expectedClass.name;
+ throw new WorkboxError("incorrect-class", details);
+ }
+ };
+ const isOneOf = (value, validValues, details) => {
+ if (!validValues.includes(value)) {
+ details["validValueDescription"] = `Valid values are ${JSON.stringify(validValues)}.`;
+ throw new WorkboxError("invalid-value", details);
+ }
+ };
+ const isArrayOfClass = (
+ value,
// Need general type to do check later.
expectedClass,
// eslint-disable-line
- details) => {
- const error = new WorkboxError('not-array-of-class', details);
- if (!Array.isArray(value)) {
+ details
+ ) => {
+ const error = new WorkboxError("not-array-of-class", details);
+ if (!Array.isArray(value)) {
+ throw error;
+ }
+ for (const item of value) {
+ if (!(item instanceof expectedClass)) {
throw error;
}
- for (const item of value) {
- if (!(item instanceof expectedClass)) {
- throw error;
- }
- }
- };
- const finalAssertExports = {
- hasMethod,
- isArray,
- isInstance,
- isOneOf,
- isType,
- isArrayOfClass
- };
-
- // @ts-ignore
- try {
- self['workbox:routing:7.0.0'] && _();
- } catch (e) {}
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * The default HTTP method, 'GET', used when there's no specific method
- * configured for a route.
- *
- * @type {string}
- *
- * @private
- */
- const defaultMethod = 'GET';
- /**
- * The list of valid HTTP methods associated with requests that could be routed.
- *
- * @type {Array}
- *
- * @private
- */
- const validMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'];
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * @param {function()|Object} handler Either a function, or an object with a
- * 'handle' method.
- * @return {Object} An object with a handle method.
- *
- * @private
- */
- const normalizeHandler = handler => {
- if (handler && typeof handler === 'object') {
- {
- finalAssertExports.hasMethod(handler, 'handle', {
- moduleName: 'workbox-routing',
- className: 'Route',
- funcName: 'constructor',
- paramName: 'handler'
- });
- }
- return handler;
- } else {
- {
- finalAssertExports.isType(handler, 'function', {
- moduleName: 'workbox-routing',
- className: 'Route',
- funcName: 'constructor',
- paramName: 'handler'
- });
- }
- return {
- handle: handler
- };
- }
- };
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * A `Route` consists of a pair of callback functions, "match" and "handler".
- * The "match" callback determine if a route should be used to "handle" a
- * request by returning a non-falsy value if it can. The "handler" callback
- * is called when there is a match and should return a Promise that resolves
- * to a `Response`.
- *
- * @memberof workbox-routing
- */
- class Route {
- /**
- * Constructor for Route class.
- *
- * @param {workbox-routing~matchCallback} match
- * A callback function that determines whether the route matches a given
- * `fetch` event by returning a non-falsy value.
- * @param {workbox-routing~handlerCallback} handler A callback
- * function that returns a Promise resolving to a Response.
- * @param {string} [method='GET'] The HTTP method to match the Route
- * against.
- */
- constructor(match, handler, method = defaultMethod) {
- {
- finalAssertExports.isType(match, 'function', {
- moduleName: 'workbox-routing',
- className: 'Route',
- funcName: 'constructor',
- paramName: 'match'
- });
- if (method) {
- finalAssertExports.isOneOf(method, validMethods, {
- paramName: 'method'
- });
- }
- }
- // These values are referenced directly by Router so cannot be
- // altered by minificaton.
- this.handler = normalizeHandler(handler);
- this.match = match;
- this.method = method;
- }
- /**
- *
- * @param {workbox-routing-handlerCallback} handler A callback
- * function that returns a Promise resolving to a Response
- */
- setCatchHandler(handler) {
- this.catchHandler = normalizeHandler(handler);
- }
}
+ };
+ const finalAssertExports = {
+ hasMethod,
+ isArray,
+ isInstance,
+ isOneOf,
+ isType,
+ isArrayOfClass
+ };
- /*
+ // @ts-ignore
+ try {
+ self["workbox:routing:7.0.0"] && _();
+ } catch (e) {}
+
+ /*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
- /**
- * RegExpRoute makes it easy to create a regular expression based
- * {@link workbox-routing.Route}.
- *
- * For same-origin requests the RegExp only needs to match part of the URL. For
- * requests against third-party servers, you must define a RegExp that matches
- * the start of the URL.
- *
- * @memberof workbox-routing
- * @extends workbox-routing.Route
- */
- class RegExpRoute extends Route {
- /**
- * If the regular expression contains
- * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references},
- * the captured values will be passed to the
- * {@link workbox-routing~handlerCallback} `params`
- * argument.
- *
- * @param {RegExp} regExp The regular expression to match against URLs.
- * @param {workbox-routing~handlerCallback} handler A callback
- * function that returns a Promise resulting in a Response.
- * @param {string} [method='GET'] The HTTP method to match the Route
- * against.
- */
- constructor(regExp, handler, method) {
- {
- finalAssertExports.isInstance(regExp, RegExp, {
- moduleName: 'workbox-routing',
- className: 'RegExpRoute',
- funcName: 'constructor',
- paramName: 'pattern'
- });
- }
- const match = ({
- url
- }) => {
- const result = regExp.exec(url.href);
- // Return immediately if there's no match.
- if (!result) {
- return;
- }
- // Require that the match start at the first character in the URL string
- // if it's a cross-origin request.
- // See https://github.com/GoogleChrome/workbox/issues/281 for the context
- // behind this behavior.
- if (url.origin !== location.origin && result.index !== 0) {
- {
- logger.debug(`The regular expression '${regExp.toString()}' only partially matched ` + `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` + `handle cross-origin requests if they match the entire URL.`);
- }
- return;
- }
- // If the route matches, but there aren't any capture groups defined, then
- // this will return [], which is truthy and therefore sufficient to
- // indicate a match.
- // If there are capture groups, then it will return their values.
- return result.slice(1);
- };
- super(match, handler, method);
- }
- }
+ /**
+ * The default HTTP method, 'GET', used when there's no specific method
+ * configured for a route.
+ *
+ * @type {string}
+ *
+ * @private
+ */
+ const defaultMethod = "GET";
+ /**
+ * The list of valid HTTP methods associated with requests that could be routed.
+ *
+ * @type {Array}
+ *
+ * @private
+ */
+ const validMethods = ["DELETE", "GET", "HEAD", "PATCH", "POST", "PUT"];
- /*
+ /*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
- const getFriendlyURL = url => {
- const urlObj = new URL(String(url), location.href);
- // See https://github.com/GoogleChrome/workbox/issues/2323
- // We want to include everything, except for the origin if it's same-origin.
- return urlObj.href.replace(new RegExp(`^${location.origin}`), '');
- };
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * The Router can be used to process a `FetchEvent` using one or more
- * {@link workbox-routing.Route}, responding with a `Response` if
- * a matching route exists.
- *
- * If no route matches a given a request, the Router will use a "default"
- * handler if one is defined.
- *
- * Should the matching Route throw an error, the Router will use a "catch"
- * handler if one is defined to gracefully deal with issues and respond with a
- * Request.
- *
- * If a request matches multiple routes, the **earliest** registered route will
- * be used to respond to the request.
- *
- * @memberof workbox-routing
- */
- class Router {
- /**
- * Initializes a new Router.
- */
- constructor() {
- this._routes = new Map();
- this._defaultHandlerMap = new Map();
- }
- /**
- * @return {Map>} routes A `Map` of HTTP
- * method name ('GET', etc.) to an array of all the corresponding `Route`
- * instances that are registered.
- */
- get routes() {
- return this._routes;
- }
- /**
- * Adds a fetch event listener to respond to events when a route matches
- * the event's request.
- */
- addFetchListener() {
- // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
- self.addEventListener('fetch', event => {
- const {
- request
- } = event;
- const responsePromise = this.handleRequest({
- request,
- event
- });
- if (responsePromise) {
- event.respondWith(responsePromise);
- }
+ /**
+ * @param {function()|Object} handler Either a function, or an object with a
+ * 'handle' method.
+ * @return {Object} An object with a handle method.
+ *
+ * @private
+ */
+ const normalizeHandler = (handler) => {
+ if (handler && typeof handler === "object") {
+ {
+ finalAssertExports.hasMethod(handler, "handle", {
+ moduleName: "workbox-routing",
+ className: "Route",
+ funcName: "constructor",
+ paramName: "handler"
});
}
- /**
- * Adds a message event listener for URLs to cache from the window.
- * This is useful to cache resources loaded on the page prior to when the
- * service worker started controlling it.
- *
- * The format of the message data sent from the window should be as follows.
- * Where the `urlsToCache` array may consist of URL strings or an array of
- * URL string + `requestInit` object (the same as you'd pass to `fetch()`).
- *
- * ```
- * {
- * type: 'CACHE_URLS',
- * payload: {
- * urlsToCache: [
- * './script1.js',
- * './script2.js',
- * ['./script3.js', {mode: 'no-cors'}],
- * ],
- * },
- * }
- * ```
- */
- addCacheListener() {
- // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
- self.addEventListener('message', event => {
- // event.data is type 'any'
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- if (event.data && event.data.type === 'CACHE_URLS') {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- const {
- payload
- } = event.data;
- {
- logger.debug(`Caching URLs from the window`, payload.urlsToCache);
- }
- const requestPromises = Promise.all(payload.urlsToCache.map(entry => {
- if (typeof entry === 'string') {
+ return handler;
+ } else {
+ {
+ finalAssertExports.isType(handler, "function", {
+ moduleName: "workbox-routing",
+ className: "Route",
+ funcName: "constructor",
+ paramName: "handler"
+ });
+ }
+ return {
+ handle: handler
+ };
+ }
+ };
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * A `Route` consists of a pair of callback functions, "match" and "handler".
+ * The "match" callback determine if a route should be used to "handle" a
+ * request by returning a non-falsy value if it can. The "handler" callback
+ * is called when there is a match and should return a Promise that resolves
+ * to a `Response`.
+ *
+ * @memberof workbox-routing
+ */
+ class Route {
+ /**
+ * Constructor for Route class.
+ *
+ * @param {workbox-routing~matchCallback} match
+ * A callback function that determines whether the route matches a given
+ * `fetch` event by returning a non-falsy value.
+ * @param {workbox-routing~handlerCallback} handler A callback
+ * function that returns a Promise resolving to a Response.
+ * @param {string} [method='GET'] The HTTP method to match the Route
+ * against.
+ */
+ constructor(match, handler, method = defaultMethod) {
+ {
+ finalAssertExports.isType(match, "function", {
+ moduleName: "workbox-routing",
+ className: "Route",
+ funcName: "constructor",
+ paramName: "match"
+ });
+ if (method) {
+ finalAssertExports.isOneOf(method, validMethods, {
+ paramName: "method"
+ });
+ }
+ }
+ // These values are referenced directly by Router so cannot be
+ // altered by minificaton.
+ this.handler = normalizeHandler(handler);
+ this.match = match;
+ this.method = method;
+ }
+ /**
+ *
+ * @param {workbox-routing-handlerCallback} handler A callback
+ * function that returns a Promise resolving to a Response
+ */
+ setCatchHandler(handler) {
+ this.catchHandler = normalizeHandler(handler);
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * RegExpRoute makes it easy to create a regular expression based
+ * {@link workbox-routing.Route}.
+ *
+ * For same-origin requests the RegExp only needs to match part of the URL. For
+ * requests against third-party servers, you must define a RegExp that matches
+ * the start of the URL.
+ *
+ * @memberof workbox-routing
+ * @extends workbox-routing.Route
+ */
+ class RegExpRoute extends Route {
+ /**
+ * If the regular expression contains
+ * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references},
+ * the captured values will be passed to the
+ * {@link workbox-routing~handlerCallback} `params`
+ * argument.
+ *
+ * @param {RegExp} regExp The regular expression to match against URLs.
+ * @param {workbox-routing~handlerCallback} handler A callback
+ * function that returns a Promise resulting in a Response.
+ * @param {string} [method='GET'] The HTTP method to match the Route
+ * against.
+ */
+ constructor(regExp, handler, method) {
+ {
+ finalAssertExports.isInstance(regExp, RegExp, {
+ moduleName: "workbox-routing",
+ className: "RegExpRoute",
+ funcName: "constructor",
+ paramName: "pattern"
+ });
+ }
+ const match = ({ url }) => {
+ const result = regExp.exec(url.href);
+ // Return immediately if there's no match.
+ if (!result) {
+ return;
+ }
+ // Require that the match start at the first character in the URL string
+ // if it's a cross-origin request.
+ // See https://github.com/GoogleChrome/workbox/issues/281 for the context
+ // behind this behavior.
+ if (url.origin !== location.origin && result.index !== 0) {
+ {
+ logger.debug(
+ `The regular expression '${regExp.toString()}' only partially matched ` +
+ `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` +
+ `handle cross-origin requests if they match the entire URL.`
+ );
+ }
+ return;
+ }
+ // If the route matches, but there aren't any capture groups defined, then
+ // this will return [], which is truthy and therefore sufficient to
+ // indicate a match.
+ // If there are capture groups, then it will return their values.
+ return result.slice(1);
+ };
+ super(match, handler, method);
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const getFriendlyURL = (url) => {
+ const urlObj = new URL(String(url), location.href);
+ // See https://github.com/GoogleChrome/workbox/issues/2323
+ // We want to include everything, except for the origin if it's same-origin.
+ return urlObj.href.replace(new RegExp(`^${location.origin}`), "");
+ };
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * The Router can be used to process a `FetchEvent` using one or more
+ * {@link workbox-routing.Route}, responding with a `Response` if
+ * a matching route exists.
+ *
+ * If no route matches a given a request, the Router will use a "default"
+ * handler if one is defined.
+ *
+ * Should the matching Route throw an error, the Router will use a "catch"
+ * handler if one is defined to gracefully deal with issues and respond with a
+ * Request.
+ *
+ * If a request matches multiple routes, the **earliest** registered route will
+ * be used to respond to the request.
+ *
+ * @memberof workbox-routing
+ */
+ class Router {
+ /**
+ * Initializes a new Router.
+ */
+ constructor() {
+ this._routes = new Map();
+ this._defaultHandlerMap = new Map();
+ }
+ /**
+ * @return {Map>} routes A `Map` of HTTP
+ * method name ('GET', etc.) to an array of all the corresponding `Route`
+ * instances that are registered.
+ */
+ get routes() {
+ return this._routes;
+ }
+ /**
+ * Adds a fetch event listener to respond to events when a route matches
+ * the event's request.
+ */
+ addFetchListener() {
+ // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
+ self.addEventListener("fetch", (event) => {
+ const { request } = event;
+ const responsePromise = this.handleRequest({
+ request,
+ event
+ });
+ if (responsePromise) {
+ event.respondWith(responsePromise);
+ }
+ });
+ }
+ /**
+ * Adds a message event listener for URLs to cache from the window.
+ * This is useful to cache resources loaded on the page prior to when the
+ * service worker started controlling it.
+ *
+ * The format of the message data sent from the window should be as follows.
+ * Where the `urlsToCache` array may consist of URL strings or an array of
+ * URL string + `requestInit` object (the same as you'd pass to `fetch()`).
+ *
+ * ```
+ * {
+ * type: 'CACHE_URLS',
+ * payload: {
+ * urlsToCache: [
+ * './script1.js',
+ * './script2.js',
+ * ['./script3.js', {mode: 'no-cors'}],
+ * ],
+ * },
+ * }
+ * ```
+ */
+ addCacheListener() {
+ // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
+ self.addEventListener("message", (event) => {
+ // event.data is type 'any'
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ if (event.data && event.data.type === "CACHE_URLS") {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const { payload } = event.data;
+ {
+ logger.debug(`Caching URLs from the window`, payload.urlsToCache);
+ }
+ const requestPromises = Promise.all(
+ payload.urlsToCache.map((entry) => {
+ if (typeof entry === "string") {
entry = [entry];
}
const request = new Request(...entry);
@@ -787,2351 +785,1955 @@ define(['exports'], (function (exports) { 'use strict';
// TODO(philipwalton): TypeScript errors without this typecast for
// some reason (probably a bug). The real type here should work but
// doesn't: `Array | undefined>`.
- })); // TypeScript
- event.waitUntil(requestPromises);
- // If a MessageChannel was used, reply to the message on success.
- if (event.ports && event.ports[0]) {
- void requestPromises.then(() => event.ports[0].postMessage(true));
- }
+ })
+ ); // TypeScript
+ event.waitUntil(requestPromises);
+ // If a MessageChannel was used, reply to the message on success.
+ if (event.ports && event.ports[0]) {
+ void requestPromises.then(() => event.ports[0].postMessage(true));
}
+ }
+ });
+ }
+ /**
+ * Apply the routing rules to a FetchEvent object to get a Response from an
+ * appropriate Route's handler.
+ *
+ * @param {Object} options
+ * @param {Request} options.request The request to handle.
+ * @param {ExtendableEvent} options.event The event that triggered the
+ * request.
+ * @return {Promise|undefined} A promise is returned if a
+ * registered route can handle the request. If there is no matching
+ * route and there's no `defaultHandler`, `undefined` is returned.
+ */
+ handleRequest({ request, event }) {
+ {
+ finalAssertExports.isInstance(request, Request, {
+ moduleName: "workbox-routing",
+ className: "Router",
+ funcName: "handleRequest",
+ paramName: "options.request"
});
}
- /**
- * Apply the routing rules to a FetchEvent object to get a Response from an
- * appropriate Route's handler.
- *
- * @param {Object} options
- * @param {Request} options.request The request to handle.
- * @param {ExtendableEvent} options.event The event that triggered the
- * request.
- * @return {Promise|undefined} A promise is returned if a
- * registered route can handle the request. If there is no matching
- * route and there's no `defaultHandler`, `undefined` is returned.
- */
- handleRequest({
- request,
- event
- }) {
+ const url = new URL(request.url, location.href);
+ if (!url.protocol.startsWith("http")) {
{
- finalAssertExports.isInstance(request, Request, {
- moduleName: 'workbox-routing',
- className: 'Router',
- funcName: 'handleRequest',
- paramName: 'options.request'
- });
+ logger.debug(`Workbox Router only supports URLs that start with 'http'.`);
}
- const url = new URL(request.url, location.href);
- if (!url.protocol.startsWith('http')) {
- {
- logger.debug(`Workbox Router only supports URLs that start with 'http'.`);
- }
- return;
- }
- const sameOrigin = url.origin === location.origin;
- const {
- params,
- route
- } = this.findMatchingRoute({
- event,
- request,
- sameOrigin,
- url
- });
- let handler = route && route.handler;
- const debugMessages = [];
- {
- if (handler) {
- debugMessages.push([`Found a route to handle this request:`, route]);
- if (params) {
- debugMessages.push([`Passing the following params to the route's handler:`, params]);
- }
- }
- }
- // If we don't have a handler because there was no matching route, then
- // fall back to defaultHandler if that's defined.
- const method = request.method;
- if (!handler && this._defaultHandlerMap.has(method)) {
- {
- debugMessages.push(`Failed to find a matching route. Falling ` + `back to the default handler for ${method}.`);
- }
- handler = this._defaultHandlerMap.get(method);
- }
- if (!handler) {
- {
- // No handler so Workbox will do nothing. If logs is set of debug
- // i.e. verbose, we should print out this information.
- logger.debug(`No route found for: ${getFriendlyURL(url)}`);
- }
- return;
- }
- {
- // We have a handler, meaning Workbox is going to handle the route.
- // print the routing details to the console.
- logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`);
- debugMessages.forEach(msg => {
- if (Array.isArray(msg)) {
- logger.log(...msg);
- } else {
- logger.log(msg);
- }
- });
- logger.groupEnd();
- }
- // Wrap in try and catch in case the handle method throws a synchronous
- // error. It should still callback to the catch handler.
- let responsePromise;
- try {
- responsePromise = handler.handle({
- url,
- request,
- event,
- params
- });
- } catch (err) {
- responsePromise = Promise.reject(err);
- }
- // Get route's catch handler, if it exists
- const catchHandler = route && route.catchHandler;
- if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) {
- responsePromise = responsePromise.catch(async err => {
- // If there's a route catch handler, process that first
- if (catchHandler) {
- {
- // Still include URL here as it will be async from the console group
- // and may not make sense without the URL
- logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`);
- logger.error(`Error thrown by:`, route);
- logger.error(err);
- logger.groupEnd();
- }
- try {
- return await catchHandler.handle({
- url,
- request,
- event,
- params
- });
- } catch (catchErr) {
- if (catchErr instanceof Error) {
- err = catchErr;
- }
- }
- }
- if (this._catchHandler) {
- {
- // Still include URL here as it will be async from the console group
- // and may not make sense without the URL
- logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to global Catch Handler.`);
- logger.error(`Error thrown by:`, route);
- logger.error(err);
- logger.groupEnd();
- }
- return this._catchHandler.handle({
- url,
- request,
- event
- });
- }
- throw err;
- });
- }
- return responsePromise;
- }
- /**
- * Checks a request and URL (and optionally an event) against the list of
- * registered routes, and if there's a match, returns the corresponding
- * route along with any params generated by the match.
- *
- * @param {Object} options
- * @param {URL} options.url
- * @param {boolean} options.sameOrigin The result of comparing `url.origin`
- * against the current origin.
- * @param {Request} options.request The request to match.
- * @param {Event} options.event The corresponding event.
- * @return {Object} An object with `route` and `params` properties.
- * They are populated if a matching route was found or `undefined`
- * otherwise.
- */
- findMatchingRoute({
- url,
- sameOrigin,
- request,
- event
- }) {
- const routes = this._routes.get(request.method) || [];
- for (const route of routes) {
- let params;
- // route.match returns type any, not possible to change right now.
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- const matchResult = route.match({
- url,
- sameOrigin,
- request,
- event
- });
- if (matchResult) {
- {
- // Warn developers that using an async matchCallback is almost always
- // not the right thing to do.
- if (matchResult instanceof Promise) {
- logger.warn(`While routing ${getFriendlyURL(url)}, an async ` + `matchCallback function was used. Please convert the ` + `following route to use a synchronous matchCallback function:`, route);
- }
- }
- // See https://github.com/GoogleChrome/workbox/issues/2079
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- params = matchResult;
- if (Array.isArray(params) && params.length === 0) {
- // Instead of passing an empty array in as params, use undefined.
- params = undefined;
- } else if (matchResult.constructor === Object &&
- // eslint-disable-line
- Object.keys(matchResult).length === 0) {
- // Instead of passing an empty object in as params, use undefined.
- params = undefined;
- } else if (typeof matchResult === 'boolean') {
- // For the boolean value true (rather than just something truth-y),
- // don't set params.
- // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353
- params = undefined;
- }
- // Return early if have a match.
- return {
- route,
- params
- };
- }
- }
- // If no match was found above, return and empty object.
- return {};
- }
- /**
- * Define a default `handler` that's called when no routes explicitly
- * match the incoming request.
- *
- * Each HTTP method ('GET', 'POST', etc.) gets its own default handler.
- *
- * Without a default handler, unmatched requests will go against the
- * network as if there were no service worker present.
- *
- * @param {workbox-routing~handlerCallback} handler A callback
- * function that returns a Promise resulting in a Response.
- * @param {string} [method='GET'] The HTTP method to associate with this
- * default handler. Each method has its own default.
- */
- setDefaultHandler(handler, method = defaultMethod) {
- this._defaultHandlerMap.set(method, normalizeHandler(handler));
- }
- /**
- * If a Route throws an error while handling a request, this `handler`
- * will be called and given a chance to provide a response.
- *
- * @param {workbox-routing~handlerCallback} handler A callback
- * function that returns a Promise resulting in a Response.
- */
- setCatchHandler(handler) {
- this._catchHandler = normalizeHandler(handler);
- }
- /**
- * Registers a route with the router.
- *
- * @param {workbox-routing.Route} route The route to register.
- */
- registerRoute(route) {
- {
- finalAssertExports.isType(route, 'object', {
- moduleName: 'workbox-routing',
- className: 'Router',
- funcName: 'registerRoute',
- paramName: 'route'
- });
- finalAssertExports.hasMethod(route, 'match', {
- moduleName: 'workbox-routing',
- className: 'Router',
- funcName: 'registerRoute',
- paramName: 'route'
- });
- finalAssertExports.isType(route.handler, 'object', {
- moduleName: 'workbox-routing',
- className: 'Router',
- funcName: 'registerRoute',
- paramName: 'route'
- });
- finalAssertExports.hasMethod(route.handler, 'handle', {
- moduleName: 'workbox-routing',
- className: 'Router',
- funcName: 'registerRoute',
- paramName: 'route.handler'
- });
- finalAssertExports.isType(route.method, 'string', {
- moduleName: 'workbox-routing',
- className: 'Router',
- funcName: 'registerRoute',
- paramName: 'route.method'
- });
- }
- if (!this._routes.has(route.method)) {
- this._routes.set(route.method, []);
- }
- // Give precedence to all of the earlier routes by adding this additional
- // route to the end of the array.
- this._routes.get(route.method).push(route);
- }
- /**
- * Unregisters a route with the router.
- *
- * @param {workbox-routing.Route} route The route to unregister.
- */
- unregisterRoute(route) {
- if (!this._routes.has(route.method)) {
- throw new WorkboxError('unregister-route-but-not-found-with-method', {
- method: route.method
- });
- }
- const routeIndex = this._routes.get(route.method).indexOf(route);
- if (routeIndex > -1) {
- this._routes.get(route.method).splice(routeIndex, 1);
- } else {
- throw new WorkboxError('unregister-route-route-not-registered');
- }
- }
- }
-
- /*
- Copyright 2019 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- let defaultRouter;
- /**
- * Creates a new, singleton Router instance if one does not exist. If one
- * does already exist, that instance is returned.
- *
- * @private
- * @return {Router}
- */
- const getOrCreateDefaultRouter = () => {
- if (!defaultRouter) {
- defaultRouter = new Router();
- // The helpers that use the default Router assume these listeners exist.
- defaultRouter.addFetchListener();
- defaultRouter.addCacheListener();
- }
- return defaultRouter;
- };
-
- /*
- Copyright 2019 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * Easily register a RegExp, string, or function with a caching
- * strategy to a singleton Router instance.
- *
- * This method will generate a Route for you if needed and
- * call {@link workbox-routing.Router#registerRoute}.
- *
- * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture
- * If the capture param is a `Route`, all other arguments will be ignored.
- * @param {workbox-routing~handlerCallback} [handler] A callback
- * function that returns a Promise resulting in a Response. This parameter
- * is required if `capture` is not a `Route` object.
- * @param {string} [method='GET'] The HTTP method to match the Route
- * against.
- * @return {workbox-routing.Route} The generated `Route`.
- *
- * @memberof workbox-routing
- */
- function registerRoute(capture, handler, method) {
- let route;
- if (typeof capture === 'string') {
- const captureUrl = new URL(capture, location.href);
- {
- if (!(capture.startsWith('/') || capture.startsWith('http'))) {
- throw new WorkboxError('invalid-string', {
- moduleName: 'workbox-routing',
- funcName: 'registerRoute',
- paramName: 'capture'
- });
- }
- // We want to check if Express-style wildcards are in the pathname only.
- // TODO: Remove this log message in v4.
- const valueToCheck = capture.startsWith('http') ? captureUrl.pathname : capture;
- // See https://github.com/pillarjs/path-to-regexp#parameters
- const wildcards = '[*:?+]';
- if (new RegExp(`${wildcards}`).exec(valueToCheck)) {
- logger.debug(`The '$capture' parameter contains an Express-style wildcard ` + `character (${wildcards}). Strings are now always interpreted as ` + `exact matches; use a RegExp for partial or wildcard matches.`);
- }
- }
- const matchCallback = ({
- url
- }) => {
- {
- if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) {
- logger.debug(`${capture} only partially matches the cross-origin URL ` + `${url.toString()}. This route will only handle cross-origin requests ` + `if they match the entire URL.`);
- }
- }
- return url.href === captureUrl.href;
- };
- // If `capture` is a string then `handler` and `method` must be present.
- route = new Route(matchCallback, handler, method);
- } else if (capture instanceof RegExp) {
- // If `capture` is a `RegExp` then `handler` and `method` must be present.
- route = new RegExpRoute(capture, handler, method);
- } else if (typeof capture === 'function') {
- // If `capture` is a function then `handler` and `method` must be present.
- route = new Route(capture, handler, method);
- } else if (capture instanceof Route) {
- route = capture;
- } else {
- throw new WorkboxError('unsupported-route-type', {
- moduleName: 'workbox-routing',
- funcName: 'registerRoute',
- paramName: 'capture'
- });
- }
- const defaultRouter = getOrCreateDefaultRouter();
- defaultRouter.registerRoute(route);
- return route;
- }
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- const _cacheNameDetails = {
- googleAnalytics: 'googleAnalytics',
- precache: 'precache-v2',
- prefix: 'workbox',
- runtime: 'runtime',
- suffix: typeof registration !== 'undefined' ? registration.scope : ''
- };
- const _createCacheName = cacheName => {
- return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix].filter(value => value && value.length > 0).join('-');
- };
- const eachCacheNameDetail = fn => {
- for (const key of Object.keys(_cacheNameDetails)) {
- fn(key);
- }
- };
- const cacheNames = {
- updateDetails: details => {
- eachCacheNameDetail(key => {
- if (typeof details[key] === 'string') {
- _cacheNameDetails[key] = details[key];
- }
- });
- },
- getGoogleAnalyticsName: userCacheName => {
- return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics);
- },
- getPrecacheName: userCacheName => {
- return userCacheName || _createCacheName(_cacheNameDetails.precache);
- },
- getPrefix: () => {
- return _cacheNameDetails.prefix;
- },
- getRuntimeName: userCacheName => {
- return userCacheName || _createCacheName(_cacheNameDetails.runtime);
- },
- getSuffix: () => {
- return _cacheNameDetails.suffix;
- }
- };
-
- /*
- Copyright 2020 Google LLC
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * A utility method that makes it easier to use `event.waitUntil` with
- * async functions and return the result.
- *
- * @param {ExtendableEvent} event
- * @param {Function} asyncFn
- * @return {Function}
- * @private
- */
- function waitUntil(event, asyncFn) {
- const returnPromise = asyncFn();
- event.waitUntil(returnPromise);
- return returnPromise;
- }
-
- // @ts-ignore
- try {
- self['workbox:precaching:7.0.0'] && _();
- } catch (e) {}
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- // Name of the search parameter used to store revision info.
- const REVISION_SEARCH_PARAM = '__WB_REVISION__';
- /**
- * Converts a manifest entry into a versioned URL suitable for precaching.
- *
- * @param {Object|string} entry
- * @return {string} A URL with versioning info.
- *
- * @private
- * @memberof workbox-precaching
- */
- function createCacheKey(entry) {
- if (!entry) {
- throw new WorkboxError('add-to-cache-list-unexpected-type', {
- entry
- });
- }
- // If a precache manifest entry is a string, it's assumed to be a versioned
- // URL, like '/app.abcd1234.js'. Return as-is.
- if (typeof entry === 'string') {
- const urlObject = new URL(entry, location.href);
- return {
- cacheKey: urlObject.href,
- url: urlObject.href
- };
- }
- const {
- revision,
- url
- } = entry;
- if (!url) {
- throw new WorkboxError('add-to-cache-list-unexpected-type', {
- entry
- });
- }
- // If there's just a URL and no revision, then it's also assumed to be a
- // versioned URL.
- if (!revision) {
- const urlObject = new URL(url, location.href);
- return {
- cacheKey: urlObject.href,
- url: urlObject.href
- };
- }
- // Otherwise, construct a properly versioned URL using the custom Workbox
- // search parameter along with the revision info.
- const cacheKeyURL = new URL(url, location.href);
- const originalURL = new URL(url, location.href);
- cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision);
- return {
- cacheKey: cacheKeyURL.href,
- url: originalURL.href
- };
- }
-
- /*
- Copyright 2020 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * A plugin, designed to be used with PrecacheController, to determine the
- * of assets that were updated (or not updated) during the install event.
- *
- * @private
- */
- class PrecacheInstallReportPlugin {
- constructor() {
- this.updatedURLs = [];
- this.notUpdatedURLs = [];
- this.handlerWillStart = async ({
- request,
- state
- }) => {
- // TODO: `state` should never be undefined...
- if (state) {
- state.originalRequest = request;
- }
- };
- this.cachedResponseWillBeUsed = async ({
- event,
- state,
- cachedResponse
- }) => {
- if (event.type === 'install') {
- if (state && state.originalRequest && state.originalRequest instanceof Request) {
- // TODO: `state` should never be undefined...
- const url = state.originalRequest.url;
- if (cachedResponse) {
- this.notUpdatedURLs.push(url);
- } else {
- this.updatedURLs.push(url);
- }
- }
- }
- return cachedResponse;
- };
- }
- }
-
- /*
- Copyright 2020 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * A plugin, designed to be used with PrecacheController, to translate URLs into
- * the corresponding cache key, based on the current revision info.
- *
- * @private
- */
- class PrecacheCacheKeyPlugin {
- constructor({
- precacheController
- }) {
- this.cacheKeyWillBeUsed = async ({
- request,
- params
- }) => {
- // Params is type any, can't change right now.
- /* eslint-disable */
- const cacheKey = (params === null || params === void 0 ? void 0 : params.cacheKey) || this._precacheController.getCacheKeyForURL(request.url);
- /* eslint-enable */
- return cacheKey ? new Request(cacheKey, {
- headers: request.headers
- }) : request;
- };
- this._precacheController = precacheController;
- }
- }
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * @param {string} groupTitle
- * @param {Array} deletedURLs
- *
- * @private
- */
- const logGroup = (groupTitle, deletedURLs) => {
- logger.groupCollapsed(groupTitle);
- for (const url of deletedURLs) {
- logger.log(url);
- }
- logger.groupEnd();
- };
- /**
- * @param {Array} deletedURLs
- *
- * @private
- * @memberof workbox-precaching
- */
- function printCleanupDetails(deletedURLs) {
- const deletionCount = deletedURLs.length;
- if (deletionCount > 0) {
- logger.groupCollapsed(`During precaching cleanup, ` + `${deletionCount} cached ` + `request${deletionCount === 1 ? ' was' : 's were'} deleted.`);
- logGroup('Deleted Cache Requests', deletedURLs);
- logger.groupEnd();
- }
- }
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * @param {string} groupTitle
- * @param {Array} urls
- *
- * @private
- */
- function _nestedGroup(groupTitle, urls) {
- if (urls.length === 0) {
return;
}
- logger.groupCollapsed(groupTitle);
- for (const url of urls) {
- logger.log(url);
- }
- logger.groupEnd();
- }
- /**
- * @param {Array} urlsToPrecache
- * @param {Array} urlsAlreadyPrecached
- *
- * @private
- * @memberof workbox-precaching
- */
- function printInstallDetails(urlsToPrecache, urlsAlreadyPrecached) {
- const precachedCount = urlsToPrecache.length;
- const alreadyPrecachedCount = urlsAlreadyPrecached.length;
- if (precachedCount || alreadyPrecachedCount) {
- let message = `Precaching ${precachedCount} file${precachedCount === 1 ? '' : 's'}.`;
- if (alreadyPrecachedCount > 0) {
- message += ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? ' is' : 's are'} already cached.`;
+ const sameOrigin = url.origin === location.origin;
+ const { params, route } = this.findMatchingRoute({
+ event,
+ request,
+ sameOrigin,
+ url
+ });
+ let handler = route && route.handler;
+ const debugMessages = [];
+ {
+ if (handler) {
+ debugMessages.push([`Found a route to handle this request:`, route]);
+ if (params) {
+ debugMessages.push([`Passing the following params to the route's handler:`, params]);
+ }
}
- logger.groupCollapsed(message);
- _nestedGroup(`View newly precached URLs.`, urlsToPrecache);
- _nestedGroup(`View previously precached URLs.`, urlsAlreadyPrecached);
+ }
+ // If we don't have a handler because there was no matching route, then
+ // fall back to defaultHandler if that's defined.
+ const method = request.method;
+ if (!handler && this._defaultHandlerMap.has(method)) {
+ {
+ debugMessages.push(
+ `Failed to find a matching route. Falling ` + `back to the default handler for ${method}.`
+ );
+ }
+ handler = this._defaultHandlerMap.get(method);
+ }
+ if (!handler) {
+ {
+ // No handler so Workbox will do nothing. If logs is set of debug
+ // i.e. verbose, we should print out this information.
+ logger.debug(`No route found for: ${getFriendlyURL(url)}`);
+ }
+ return;
+ }
+ {
+ // We have a handler, meaning Workbox is going to handle the route.
+ // print the routing details to the console.
+ logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`);
+ debugMessages.forEach((msg) => {
+ if (Array.isArray(msg)) {
+ logger.log(...msg);
+ } else {
+ logger.log(msg);
+ }
+ });
logger.groupEnd();
}
- }
-
- /*
- Copyright 2019 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- let supportStatus;
- /**
- * A utility function that determines whether the current browser supports
- * constructing a new `Response` from a `response.body` stream.
- *
- * @return {boolean} `true`, if the current browser can successfully
- * construct a `Response` from a `response.body` stream, `false` otherwise.
- *
- * @private
- */
- function canConstructResponseFromBodyStream() {
- if (supportStatus === undefined) {
- const testResponse = new Response('');
- if ('body' in testResponse) {
- try {
- new Response(testResponse.body);
- supportStatus = true;
- } catch (error) {
- supportStatus = false;
- }
- }
- supportStatus = false;
- }
- return supportStatus;
- }
-
- /*
- Copyright 2019 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * Allows developers to copy a response and modify its `headers`, `status`,
- * or `statusText` values (the values settable via a
- * [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax}
- * object in the constructor).
- * To modify these values, pass a function as the second argument. That
- * function will be invoked with a single object with the response properties
- * `{headers, status, statusText}`. The return value of this function will
- * be used as the `ResponseInit` for the new `Response`. To change the values
- * either modify the passed parameter(s) and return it, or return a totally
- * new object.
- *
- * This method is intentionally limited to same-origin responses, regardless of
- * whether CORS was used or not.
- *
- * @param {Response} response
- * @param {Function} modifier
- * @memberof workbox-core
- */
- async function copyResponse(response, modifier) {
- let origin = null;
- // If response.url isn't set, assume it's cross-origin and keep origin null.
- if (response.url) {
- const responseURL = new URL(response.url);
- origin = responseURL.origin;
- }
- if (origin !== self.location.origin) {
- throw new WorkboxError('cross-origin-copy-response', {
- origin
+ // Wrap in try and catch in case the handle method throws a synchronous
+ // error. It should still callback to the catch handler.
+ let responsePromise;
+ try {
+ responsePromise = handler.handle({
+ url,
+ request,
+ event,
+ params
});
+ } catch (err) {
+ responsePromise = Promise.reject(err);
}
- const clonedResponse = response.clone();
- // Create a fresh `ResponseInit` object by cloning the headers.
- const responseInit = {
- headers: new Headers(clonedResponse.headers),
- status: clonedResponse.status,
- statusText: clonedResponse.statusText
- };
- // Apply any user modifications.
- const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit;
- // Create the new response from the body stream and `ResponseInit`
- // modifications. Note: not all browsers support the Response.body stream,
- // so fall back to reading the entire body into memory as a blob.
- const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob();
- return new Response(body, modifiedResponseInit);
- }
-
- /*
- Copyright 2020 Google LLC
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- function stripParams(fullURL, ignoreParams) {
- const strippedURL = new URL(fullURL);
- for (const param of ignoreParams) {
- strippedURL.searchParams.delete(param);
- }
- return strippedURL.href;
- }
- /**
- * Matches an item in the cache, ignoring specific URL params. This is similar
- * to the `ignoreSearch` option, but it allows you to ignore just specific
- * params (while continuing to match on the others).
- *
- * @private
- * @param {Cache} cache
- * @param {Request} request
- * @param {Object} matchOptions
- * @param {Array} ignoreParams
- * @return {Promise}
- */
- async function cacheMatchIgnoreParams(cache, request, ignoreParams, matchOptions) {
- const strippedRequestURL = stripParams(request.url, ignoreParams);
- // If the request doesn't include any ignored params, match as normal.
- if (request.url === strippedRequestURL) {
- return cache.match(request, matchOptions);
- }
- // Otherwise, match by comparing keys
- const keysOptions = Object.assign(Object.assign({}, matchOptions), {
- ignoreSearch: true
- });
- const cacheKeys = await cache.keys(request, keysOptions);
- for (const cacheKey of cacheKeys) {
- const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams);
- if (strippedRequestURL === strippedCacheKeyURL) {
- return cache.match(cacheKey, matchOptions);
- }
- }
- return;
- }
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * The Deferred class composes Promises in a way that allows for them to be
- * resolved or rejected from outside the constructor. In most cases promises
- * should be used directly, but Deferreds can be necessary when the logic to
- * resolve a promise must be separate.
- *
- * @private
- */
- class Deferred {
- /**
- * Creates a promise and exposes its resolve and reject functions as methods.
- */
- constructor() {
- this.promise = new Promise((resolve, reject) => {
- this.resolve = resolve;
- this.reject = reject;
- });
- }
- }
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- // Callbacks to be executed whenever there's a quota error.
- // Can't change Function type right now.
- // eslint-disable-next-line @typescript-eslint/ban-types
- const quotaErrorCallbacks = new Set();
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * Runs all of the callback functions, one at a time sequentially, in the order
- * in which they were registered.
- *
- * @memberof workbox-core
- * @private
- */
- async function executeQuotaErrorCallbacks() {
- {
- logger.log(`About to run ${quotaErrorCallbacks.size} ` + `callbacks to clean up caches.`);
- }
- for (const callback of quotaErrorCallbacks) {
- await callback();
- {
- logger.log(callback, 'is complete.');
- }
- }
- {
- logger.log('Finished running callbacks.');
- }
- }
-
- /*
- Copyright 2019 Google LLC
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * Returns a promise that resolves and the passed number of milliseconds.
- * This utility is an async/await-friendly version of `setTimeout`.
- *
- * @param {number} ms
- * @return {Promise}
- * @private
- */
- function timeout(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
-
- // @ts-ignore
- try {
- self['workbox:strategies:7.0.0'] && _();
- } catch (e) {}
-
- /*
- Copyright 2020 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- function toRequest(input) {
- return typeof input === 'string' ? new Request(input) : input;
- }
- /**
- * A class created every time a Strategy instance instance calls
- * {@link workbox-strategies.Strategy~handle} or
- * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and
- * cache actions around plugin callbacks and keeps track of when the strategy
- * is "done" (i.e. all added `event.waitUntil()` promises have resolved).
- *
- * @memberof workbox-strategies
- */
- class StrategyHandler {
- /**
- * Creates a new instance associated with the passed strategy and event
- * that's handling the request.
- *
- * The constructor also initializes the state that will be passed to each of
- * the plugins handling this request.
- *
- * @param {workbox-strategies.Strategy} strategy
- * @param {Object} options
- * @param {Request|string} options.request A request to run this strategy for.
- * @param {ExtendableEvent} options.event The event associated with the
- * request.
- * @param {URL} [options.url]
- * @param {*} [options.params] The return value from the
- * {@link workbox-routing~matchCallback} (if applicable).
- */
- constructor(strategy, options) {
- this._cacheKeys = {};
- /**
- * The request the strategy is performing (passed to the strategy's
- * `handle()` or `handleAll()` method).
- * @name request
- * @instance
- * @type {Request}
- * @memberof workbox-strategies.StrategyHandler
- */
- /**
- * The event associated with this request.
- * @name event
- * @instance
- * @type {ExtendableEvent}
- * @memberof workbox-strategies.StrategyHandler
- */
- /**
- * A `URL` instance of `request.url` (if passed to the strategy's
- * `handle()` or `handleAll()` method).
- * Note: the `url` param will be present if the strategy was invoked
- * from a workbox `Route` object.
- * @name url
- * @instance
- * @type {URL|undefined}
- * @memberof workbox-strategies.StrategyHandler
- */
- /**
- * A `param` value (if passed to the strategy's
- * `handle()` or `handleAll()` method).
- * Note: the `param` param will be present if the strategy was invoked
- * from a workbox `Route` object and the
- * {@link workbox-routing~matchCallback} returned
- * a truthy value (it will be that value).
- * @name params
- * @instance
- * @type {*|undefined}
- * @memberof workbox-strategies.StrategyHandler
- */
- {
- finalAssertExports.isInstance(options.event, ExtendableEvent, {
- moduleName: 'workbox-strategies',
- className: 'StrategyHandler',
- funcName: 'constructor',
- paramName: 'options.event'
- });
- }
- Object.assign(this, options);
- this.event = options.event;
- this._strategy = strategy;
- this._handlerDeferred = new Deferred();
- this._extendLifetimePromises = [];
- // Copy the plugins list (since it's mutable on the strategy),
- // so any mutations don't affect this handler instance.
- this._plugins = [...strategy.plugins];
- this._pluginStateMap = new Map();
- for (const plugin of this._plugins) {
- this._pluginStateMap.set(plugin, {});
- }
- this.event.waitUntil(this._handlerDeferred.promise);
- }
- /**
- * Fetches a given request (and invokes any applicable plugin callback
- * methods) using the `fetchOptions` (for non-navigation requests) and
- * `plugins` defined on the `Strategy` object.
- *
- * The following plugin lifecycle methods are invoked when using this method:
- * - `requestWillFetch()`
- * - `fetchDidSucceed()`
- * - `fetchDidFail()`
- *
- * @param {Request|string} input The URL or request to fetch.
- * @return {Promise}
- */
- async fetch(input) {
- const {
- event
- } = this;
- let request = toRequest(input);
- if (request.mode === 'navigate' && event instanceof FetchEvent && event.preloadResponse) {
- const possiblePreloadResponse = await event.preloadResponse;
- if (possiblePreloadResponse) {
+ // Get route's catch handler, if it exists
+ const catchHandler = route && route.catchHandler;
+ if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) {
+ responsePromise = responsePromise.catch(async (err) => {
+ // If there's a route catch handler, process that first
+ if (catchHandler) {
{
- logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`);
+ // Still include URL here as it will be async from the console group
+ // and may not make sense without the URL
+ logger.groupCollapsed(
+ `Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`
+ );
+ logger.error(`Error thrown by:`, route);
+ logger.error(err);
+ logger.groupEnd();
+ }
+ try {
+ return await catchHandler.handle({
+ url,
+ request,
+ event,
+ params
+ });
+ } catch (catchErr) {
+ if (catchErr instanceof Error) {
+ err = catchErr;
+ }
}
- return possiblePreloadResponse;
}
- }
- // If there is a fetchDidFail plugin, we need to save a clone of the
- // original request before it's either modified by a requestWillFetch
- // plugin or before the original request's body is consumed via fetch().
- const originalRequest = this.hasCallback('fetchDidFail') ? request.clone() : null;
- try {
- for (const cb of this.iterateCallbacks('requestWillFetch')) {
- request = await cb({
- request: request.clone(),
+ if (this._catchHandler) {
+ {
+ // Still include URL here as it will be async from the console group
+ // and may not make sense without the URL
+ logger.groupCollapsed(
+ `Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to global Catch Handler.`
+ );
+ logger.error(`Error thrown by:`, route);
+ logger.error(err);
+ logger.groupEnd();
+ }
+ return this._catchHandler.handle({
+ url,
+ request,
event
});
}
- } catch (err) {
- if (err instanceof Error) {
- throw new WorkboxError('plugin-error-request-will-fetch', {
- thrownErrorMessage: err.message
- });
- }
- }
- // The request can be altered by plugins with `requestWillFetch` making
- // the original request (most likely from a `fetch` event) different
- // from the Request we make. Pass both to `fetchDidFail` to aid debugging.
- const pluginFilteredRequest = request.clone();
- try {
- let fetchResponse;
- // See https://github.com/GoogleChrome/workbox/issues/1796
- fetchResponse = await fetch(request, request.mode === 'navigate' ? undefined : this._strategy.fetchOptions);
- if ("development" !== 'production') {
- logger.debug(`Network request for ` + `'${getFriendlyURL(request.url)}' returned a response with ` + `status '${fetchResponse.status}'.`);
- }
- for (const callback of this.iterateCallbacks('fetchDidSucceed')) {
- fetchResponse = await callback({
- event,
- request: pluginFilteredRequest,
- response: fetchResponse
- });
- }
- return fetchResponse;
- } catch (error) {
- {
- logger.log(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error);
- }
- // `originalRequest` will only exist if a `fetchDidFail` callback
- // is being used (see above).
- if (originalRequest) {
- await this.runCallbacks('fetchDidFail', {
- error: error,
- event,
- originalRequest: originalRequest.clone(),
- request: pluginFilteredRequest.clone()
- });
- }
- throw error;
- }
- }
- /**
- * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on
- * the response generated by `this.fetch()`.
- *
- * The call to `this.cachePut()` automatically invokes `this.waitUntil()`,
- * so you do not have to manually call `waitUntil()` on the event.
- *
- * @param {Request|string} input The request or URL to fetch and cache.
- * @return {Promise}
- */
- async fetchAndCachePut(input) {
- const response = await this.fetch(input);
- const responseClone = response.clone();
- void this.waitUntil(this.cachePut(input, responseClone));
- return response;
- }
- /**
- * Matches a request from the cache (and invokes any applicable plugin
- * callback methods) using the `cacheName`, `matchOptions`, and `plugins`
- * defined on the strategy object.
- *
- * The following plugin lifecycle methods are invoked when using this method:
- * - cacheKeyWillByUsed()
- * - cachedResponseWillByUsed()
- *
- * @param {Request|string} key The Request or URL to use as the cache key.
- * @return {Promise} A matching response, if found.
- */
- async cacheMatch(key) {
- const request = toRequest(key);
- let cachedResponse;
- const {
- cacheName,
- matchOptions
- } = this._strategy;
- const effectiveRequest = await this.getCacheKey(request, 'read');
- const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), {
- cacheName
+ throw err;
});
- cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);
+ }
+ return responsePromise;
+ }
+ /**
+ * Checks a request and URL (and optionally an event) against the list of
+ * registered routes, and if there's a match, returns the corresponding
+ * route along with any params generated by the match.
+ *
+ * @param {Object} options
+ * @param {URL} options.url
+ * @param {boolean} options.sameOrigin The result of comparing `url.origin`
+ * against the current origin.
+ * @param {Request} options.request The request to match.
+ * @param {Event} options.event The corresponding event.
+ * @return {Object} An object with `route` and `params` properties.
+ * They are populated if a matching route was found or `undefined`
+ * otherwise.
+ */
+ findMatchingRoute({ url, sameOrigin, request, event }) {
+ const routes = this._routes.get(request.method) || [];
+ for (const route of routes) {
+ let params;
+ // route.match returns type any, not possible to change right now.
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const matchResult = route.match({
+ url,
+ sameOrigin,
+ request,
+ event
+ });
+ if (matchResult) {
+ {
+ // Warn developers that using an async matchCallback is almost always
+ // not the right thing to do.
+ if (matchResult instanceof Promise) {
+ logger.warn(
+ `While routing ${getFriendlyURL(url)}, an async ` +
+ `matchCallback function was used. Please convert the ` +
+ `following route to use a synchronous matchCallback function:`,
+ route
+ );
+ }
+ }
+ // See https://github.com/GoogleChrome/workbox/issues/2079
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ params = matchResult;
+ if (Array.isArray(params) && params.length === 0) {
+ // Instead of passing an empty array in as params, use undefined.
+ params = undefined;
+ } else if (
+ matchResult.constructor === Object &&
+ // eslint-disable-line
+ Object.keys(matchResult).length === 0
+ ) {
+ // Instead of passing an empty object in as params, use undefined.
+ params = undefined;
+ } else if (typeof matchResult === "boolean") {
+ // For the boolean value true (rather than just something truth-y),
+ // don't set params.
+ // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353
+ params = undefined;
+ }
+ // Return early if have a match.
+ return {
+ route,
+ params
+ };
+ }
+ }
+ // If no match was found above, return and empty object.
+ return {};
+ }
+ /**
+ * Define a default `handler` that's called when no routes explicitly
+ * match the incoming request.
+ *
+ * Each HTTP method ('GET', 'POST', etc.) gets its own default handler.
+ *
+ * Without a default handler, unmatched requests will go against the
+ * network as if there were no service worker present.
+ *
+ * @param {workbox-routing~handlerCallback} handler A callback
+ * function that returns a Promise resulting in a Response.
+ * @param {string} [method='GET'] The HTTP method to associate with this
+ * default handler. Each method has its own default.
+ */
+ setDefaultHandler(handler, method = defaultMethod) {
+ this._defaultHandlerMap.set(method, normalizeHandler(handler));
+ }
+ /**
+ * If a Route throws an error while handling a request, this `handler`
+ * will be called and given a chance to provide a response.
+ *
+ * @param {workbox-routing~handlerCallback} handler A callback
+ * function that returns a Promise resulting in a Response.
+ */
+ setCatchHandler(handler) {
+ this._catchHandler = normalizeHandler(handler);
+ }
+ /**
+ * Registers a route with the router.
+ *
+ * @param {workbox-routing.Route} route The route to register.
+ */
+ registerRoute(route) {
+ {
+ finalAssertExports.isType(route, "object", {
+ moduleName: "workbox-routing",
+ className: "Router",
+ funcName: "registerRoute",
+ paramName: "route"
+ });
+ finalAssertExports.hasMethod(route, "match", {
+ moduleName: "workbox-routing",
+ className: "Router",
+ funcName: "registerRoute",
+ paramName: "route"
+ });
+ finalAssertExports.isType(route.handler, "object", {
+ moduleName: "workbox-routing",
+ className: "Router",
+ funcName: "registerRoute",
+ paramName: "route"
+ });
+ finalAssertExports.hasMethod(route.handler, "handle", {
+ moduleName: "workbox-routing",
+ className: "Router",
+ funcName: "registerRoute",
+ paramName: "route.handler"
+ });
+ finalAssertExports.isType(route.method, "string", {
+ moduleName: "workbox-routing",
+ className: "Router",
+ funcName: "registerRoute",
+ paramName: "route.method"
+ });
+ }
+ if (!this._routes.has(route.method)) {
+ this._routes.set(route.method, []);
+ }
+ // Give precedence to all of the earlier routes by adding this additional
+ // route to the end of the array.
+ this._routes.get(route.method).push(route);
+ }
+ /**
+ * Unregisters a route with the router.
+ *
+ * @param {workbox-routing.Route} route The route to unregister.
+ */
+ unregisterRoute(route) {
+ if (!this._routes.has(route.method)) {
+ throw new WorkboxError("unregister-route-but-not-found-with-method", {
+ method: route.method
+ });
+ }
+ const routeIndex = this._routes.get(route.method).indexOf(route);
+ if (routeIndex > -1) {
+ this._routes.get(route.method).splice(routeIndex, 1);
+ } else {
+ throw new WorkboxError("unregister-route-route-not-registered");
+ }
+ }
+ }
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ let defaultRouter;
+ /**
+ * Creates a new, singleton Router instance if one does not exist. If one
+ * does already exist, that instance is returned.
+ *
+ * @private
+ * @return {Router}
+ */
+ const getOrCreateDefaultRouter = () => {
+ if (!defaultRouter) {
+ defaultRouter = new Router();
+ // The helpers that use the default Router assume these listeners exist.
+ defaultRouter.addFetchListener();
+ defaultRouter.addCacheListener();
+ }
+ return defaultRouter;
+ };
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Easily register a RegExp, string, or function with a caching
+ * strategy to a singleton Router instance.
+ *
+ * This method will generate a Route for you if needed and
+ * call {@link workbox-routing.Router#registerRoute}.
+ *
+ * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture
+ * If the capture param is a `Route`, all other arguments will be ignored.
+ * @param {workbox-routing~handlerCallback} [handler] A callback
+ * function that returns a Promise resulting in a Response. This parameter
+ * is required if `capture` is not a `Route` object.
+ * @param {string} [method='GET'] The HTTP method to match the Route
+ * against.
+ * @return {workbox-routing.Route} The generated `Route`.
+ *
+ * @memberof workbox-routing
+ */
+ function registerRoute(capture, handler, method) {
+ let route;
+ if (typeof capture === "string") {
+ const captureUrl = new URL(capture, location.href);
+ {
+ if (!(capture.startsWith("/") || capture.startsWith("http"))) {
+ throw new WorkboxError("invalid-string", {
+ moduleName: "workbox-routing",
+ funcName: "registerRoute",
+ paramName: "capture"
+ });
+ }
+ // We want to check if Express-style wildcards are in the pathname only.
+ // TODO: Remove this log message in v4.
+ const valueToCheck = capture.startsWith("http") ? captureUrl.pathname : capture;
+ // See https://github.com/pillarjs/path-to-regexp#parameters
+ const wildcards = "[*:?+]";
+ if (new RegExp(`${wildcards}`).exec(valueToCheck)) {
+ logger.debug(
+ `The '$capture' parameter contains an Express-style wildcard ` +
+ `character (${wildcards}). Strings are now always interpreted as ` +
+ `exact matches; use a RegExp for partial or wildcard matches.`
+ );
+ }
+ }
+ const matchCallback = ({ url }) => {
{
- if (cachedResponse) {
- logger.debug(`Found a cached response in '${cacheName}'.`);
- } else {
- logger.debug(`No cached response found in '${cacheName}'.`);
+ if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) {
+ logger.debug(
+ `${capture} only partially matches the cross-origin URL ` +
+ `${url.toString()}. This route will only handle cross-origin requests ` +
+ `if they match the entire URL.`
+ );
}
}
- for (const callback of this.iterateCallbacks('cachedResponseWillBeUsed')) {
- cachedResponse = (await callback({
+ return url.href === captureUrl.href;
+ };
+ // If `capture` is a string then `handler` and `method` must be present.
+ route = new Route(matchCallback, handler, method);
+ } else if (capture instanceof RegExp) {
+ // If `capture` is a `RegExp` then `handler` and `method` must be present.
+ route = new RegExpRoute(capture, handler, method);
+ } else if (typeof capture === "function") {
+ // If `capture` is a function then `handler` and `method` must be present.
+ route = new Route(capture, handler, method);
+ } else if (capture instanceof Route) {
+ route = capture;
+ } else {
+ throw new WorkboxError("unsupported-route-type", {
+ moduleName: "workbox-routing",
+ funcName: "registerRoute",
+ paramName: "capture"
+ });
+ }
+ const defaultRouter = getOrCreateDefaultRouter();
+ defaultRouter.registerRoute(route);
+ return route;
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const _cacheNameDetails = {
+ googleAnalytics: "googleAnalytics",
+ precache: "precache-v2",
+ prefix: "workbox",
+ runtime: "runtime",
+ suffix: typeof registration !== "undefined" ? registration.scope : ""
+ };
+ const _createCacheName = (cacheName) => {
+ return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix]
+ .filter((value) => value && value.length > 0)
+ .join("-");
+ };
+ const eachCacheNameDetail = (fn) => {
+ for (const key of Object.keys(_cacheNameDetails)) {
+ fn(key);
+ }
+ };
+ const cacheNames = {
+ updateDetails: (details) => {
+ eachCacheNameDetail((key) => {
+ if (typeof details[key] === "string") {
+ _cacheNameDetails[key] = details[key];
+ }
+ });
+ },
+ getGoogleAnalyticsName: (userCacheName) => {
+ return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics);
+ },
+ getPrecacheName: (userCacheName) => {
+ return userCacheName || _createCacheName(_cacheNameDetails.precache);
+ },
+ getPrefix: () => {
+ return _cacheNameDetails.prefix;
+ },
+ getRuntimeName: (userCacheName) => {
+ return userCacheName || _createCacheName(_cacheNameDetails.runtime);
+ },
+ getSuffix: () => {
+ return _cacheNameDetails.suffix;
+ }
+ };
+
+ /*
+ Copyright 2020 Google LLC
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * A utility method that makes it easier to use `event.waitUntil` with
+ * async functions and return the result.
+ *
+ * @param {ExtendableEvent} event
+ * @param {Function} asyncFn
+ * @return {Function}
+ * @private
+ */
+ function waitUntil(event, asyncFn) {
+ const returnPromise = asyncFn();
+ event.waitUntil(returnPromise);
+ return returnPromise;
+ }
+
+ // @ts-ignore
+ try {
+ self["workbox:precaching:7.0.0"] && _();
+ } catch (e) {}
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ // Name of the search parameter used to store revision info.
+ const REVISION_SEARCH_PARAM = "__WB_REVISION__";
+ /**
+ * Converts a manifest entry into a versioned URL suitable for precaching.
+ *
+ * @param {Object|string} entry
+ * @return {string} A URL with versioning info.
+ *
+ * @private
+ * @memberof workbox-precaching
+ */
+ function createCacheKey(entry) {
+ if (!entry) {
+ throw new WorkboxError("add-to-cache-list-unexpected-type", {
+ entry
+ });
+ }
+ // If a precache manifest entry is a string, it's assumed to be a versioned
+ // URL, like '/app.abcd1234.js'. Return as-is.
+ if (typeof entry === "string") {
+ const urlObject = new URL(entry, location.href);
+ return {
+ cacheKey: urlObject.href,
+ url: urlObject.href
+ };
+ }
+ const { revision, url } = entry;
+ if (!url) {
+ throw new WorkboxError("add-to-cache-list-unexpected-type", {
+ entry
+ });
+ }
+ // If there's just a URL and no revision, then it's also assumed to be a
+ // versioned URL.
+ if (!revision) {
+ const urlObject = new URL(url, location.href);
+ return {
+ cacheKey: urlObject.href,
+ url: urlObject.href
+ };
+ }
+ // Otherwise, construct a properly versioned URL using the custom Workbox
+ // search parameter along with the revision info.
+ const cacheKeyURL = new URL(url, location.href);
+ const originalURL = new URL(url, location.href);
+ cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision);
+ return {
+ cacheKey: cacheKeyURL.href,
+ url: originalURL.href
+ };
+ }
+
+ /*
+ Copyright 2020 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * A plugin, designed to be used with PrecacheController, to determine the
+ * of assets that were updated (or not updated) during the install event.
+ *
+ * @private
+ */
+ class PrecacheInstallReportPlugin {
+ constructor() {
+ this.updatedURLs = [];
+ this.notUpdatedURLs = [];
+ this.handlerWillStart = async ({ request, state }) => {
+ // TODO: `state` should never be undefined...
+ if (state) {
+ state.originalRequest = request;
+ }
+ };
+ this.cachedResponseWillBeUsed = async ({ event, state, cachedResponse }) => {
+ if (event.type === "install") {
+ if (state && state.originalRequest && state.originalRequest instanceof Request) {
+ // TODO: `state` should never be undefined...
+ const url = state.originalRequest.url;
+ if (cachedResponse) {
+ this.notUpdatedURLs.push(url);
+ } else {
+ this.updatedURLs.push(url);
+ }
+ }
+ }
+ return cachedResponse;
+ };
+ }
+ }
+
+ /*
+ Copyright 2020 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * A plugin, designed to be used with PrecacheController, to translate URLs into
+ * the corresponding cache key, based on the current revision info.
+ *
+ * @private
+ */
+ class PrecacheCacheKeyPlugin {
+ constructor({ precacheController }) {
+ this.cacheKeyWillBeUsed = async ({ request, params }) => {
+ // Params is type any, can't change right now.
+ /* eslint-disable */
+ const cacheKey =
+ (params === null || params === void 0 ? void 0 : params.cacheKey) ||
+ this._precacheController.getCacheKeyForURL(request.url);
+ /* eslint-enable */
+ return cacheKey
+ ? new Request(cacheKey, {
+ headers: request.headers
+ })
+ : request;
+ };
+ this._precacheController = precacheController;
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * @param {string} groupTitle
+ * @param {Array} deletedURLs
+ *
+ * @private
+ */
+ const logGroup = (groupTitle, deletedURLs) => {
+ logger.groupCollapsed(groupTitle);
+ for (const url of deletedURLs) {
+ logger.log(url);
+ }
+ logger.groupEnd();
+ };
+ /**
+ * @param {Array} deletedURLs
+ *
+ * @private
+ * @memberof workbox-precaching
+ */
+ function printCleanupDetails(deletedURLs) {
+ const deletionCount = deletedURLs.length;
+ if (deletionCount > 0) {
+ logger.groupCollapsed(
+ `During precaching cleanup, ` +
+ `${deletionCount} cached ` +
+ `request${deletionCount === 1 ? " was" : "s were"} deleted.`
+ );
+ logGroup("Deleted Cache Requests", deletedURLs);
+ logger.groupEnd();
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * @param {string} groupTitle
+ * @param {Array} urls
+ *
+ * @private
+ */
+ function _nestedGroup(groupTitle, urls) {
+ if (urls.length === 0) {
+ return;
+ }
+ logger.groupCollapsed(groupTitle);
+ for (const url of urls) {
+ logger.log(url);
+ }
+ logger.groupEnd();
+ }
+ /**
+ * @param {Array} urlsToPrecache
+ * @param {Array} urlsAlreadyPrecached
+ *
+ * @private
+ * @memberof workbox-precaching
+ */
+ function printInstallDetails(urlsToPrecache, urlsAlreadyPrecached) {
+ const precachedCount = urlsToPrecache.length;
+ const alreadyPrecachedCount = urlsAlreadyPrecached.length;
+ if (precachedCount || alreadyPrecachedCount) {
+ let message = `Precaching ${precachedCount} file${precachedCount === 1 ? "" : "s"}.`;
+ if (alreadyPrecachedCount > 0) {
+ message +=
+ ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? " is" : "s are"} already cached.`;
+ }
+ logger.groupCollapsed(message);
+ _nestedGroup(`View newly precached URLs.`, urlsToPrecache);
+ _nestedGroup(`View previously precached URLs.`, urlsAlreadyPrecached);
+ logger.groupEnd();
+ }
+ }
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ let supportStatus;
+ /**
+ * A utility function that determines whether the current browser supports
+ * constructing a new `Response` from a `response.body` stream.
+ *
+ * @return {boolean} `true`, if the current browser can successfully
+ * construct a `Response` from a `response.body` stream, `false` otherwise.
+ *
+ * @private
+ */
+ function canConstructResponseFromBodyStream() {
+ if (supportStatus === undefined) {
+ const testResponse = new Response("");
+ if ("body" in testResponse) {
+ try {
+ new Response(testResponse.body);
+ supportStatus = true;
+ } catch (error) {
+ supportStatus = false;
+ }
+ }
+ supportStatus = false;
+ }
+ return supportStatus;
+ }
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Allows developers to copy a response and modify its `headers`, `status`,
+ * or `statusText` values (the values settable via a
+ * [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax}
+ * object in the constructor).
+ * To modify these values, pass a function as the second argument. That
+ * function will be invoked with a single object with the response properties
+ * `{headers, status, statusText}`. The return value of this function will
+ * be used as the `ResponseInit` for the new `Response`. To change the values
+ * either modify the passed parameter(s) and return it, or return a totally
+ * new object.
+ *
+ * This method is intentionally limited to same-origin responses, regardless of
+ * whether CORS was used or not.
+ *
+ * @param {Response} response
+ * @param {Function} modifier
+ * @memberof workbox-core
+ */
+ async function copyResponse(response, modifier) {
+ let origin = null;
+ // If response.url isn't set, assume it's cross-origin and keep origin null.
+ if (response.url) {
+ const responseURL = new URL(response.url);
+ origin = responseURL.origin;
+ }
+ if (origin !== self.location.origin) {
+ throw new WorkboxError("cross-origin-copy-response", {
+ origin
+ });
+ }
+ const clonedResponse = response.clone();
+ // Create a fresh `ResponseInit` object by cloning the headers.
+ const responseInit = {
+ headers: new Headers(clonedResponse.headers),
+ status: clonedResponse.status,
+ statusText: clonedResponse.statusText
+ };
+ // Apply any user modifications.
+ const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit;
+ // Create the new response from the body stream and `ResponseInit`
+ // modifications. Note: not all browsers support the Response.body stream,
+ // so fall back to reading the entire body into memory as a blob.
+ const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob();
+ return new Response(body, modifiedResponseInit);
+ }
+
+ /*
+ Copyright 2020 Google LLC
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ function stripParams(fullURL, ignoreParams) {
+ const strippedURL = new URL(fullURL);
+ for (const param of ignoreParams) {
+ strippedURL.searchParams.delete(param);
+ }
+ return strippedURL.href;
+ }
+ /**
+ * Matches an item in the cache, ignoring specific URL params. This is similar
+ * to the `ignoreSearch` option, but it allows you to ignore just specific
+ * params (while continuing to match on the others).
+ *
+ * @private
+ * @param {Cache} cache
+ * @param {Request} request
+ * @param {Object} matchOptions
+ * @param {Array} ignoreParams
+ * @return {Promise}
+ */
+ async function cacheMatchIgnoreParams(cache, request, ignoreParams, matchOptions) {
+ const strippedRequestURL = stripParams(request.url, ignoreParams);
+ // If the request doesn't include any ignored params, match as normal.
+ if (request.url === strippedRequestURL) {
+ return cache.match(request, matchOptions);
+ }
+ // Otherwise, match by comparing keys
+ const keysOptions = Object.assign(Object.assign({}, matchOptions), {
+ ignoreSearch: true
+ });
+ const cacheKeys = await cache.keys(request, keysOptions);
+ for (const cacheKey of cacheKeys) {
+ const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams);
+ if (strippedRequestURL === strippedCacheKeyURL) {
+ return cache.match(cacheKey, matchOptions);
+ }
+ }
+ return;
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * The Deferred class composes Promises in a way that allows for them to be
+ * resolved or rejected from outside the constructor. In most cases promises
+ * should be used directly, but Deferreds can be necessary when the logic to
+ * resolve a promise must be separate.
+ *
+ * @private
+ */
+ class Deferred {
+ /**
+ * Creates a promise and exposes its resolve and reject functions as methods.
+ */
+ constructor() {
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ // Callbacks to be executed whenever there's a quota error.
+ // Can't change Function type right now.
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ const quotaErrorCallbacks = new Set();
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Runs all of the callback functions, one at a time sequentially, in the order
+ * in which they were registered.
+ *
+ * @memberof workbox-core
+ * @private
+ */
+ async function executeQuotaErrorCallbacks() {
+ {
+ logger.log(`About to run ${quotaErrorCallbacks.size} ` + `callbacks to clean up caches.`);
+ }
+ for (const callback of quotaErrorCallbacks) {
+ await callback();
+ {
+ logger.log(callback, "is complete.");
+ }
+ }
+ {
+ logger.log("Finished running callbacks.");
+ }
+ }
+
+ /*
+ Copyright 2019 Google LLC
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Returns a promise that resolves and the passed number of milliseconds.
+ * This utility is an async/await-friendly version of `setTimeout`.
+ *
+ * @param {number} ms
+ * @return {Promise}
+ * @private
+ */
+ function timeout(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ // @ts-ignore
+ try {
+ self["workbox:strategies:7.0.0"] && _();
+ } catch (e) {}
+
+ /*
+ Copyright 2020 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ function toRequest(input) {
+ return typeof input === "string" ? new Request(input) : input;
+ }
+ /**
+ * A class created every time a Strategy instance instance calls
+ * {@link workbox-strategies.Strategy~handle} or
+ * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and
+ * cache actions around plugin callbacks and keeps track of when the strategy
+ * is "done" (i.e. all added `event.waitUntil()` promises have resolved).
+ *
+ * @memberof workbox-strategies
+ */
+ class StrategyHandler {
+ /**
+ * Creates a new instance associated with the passed strategy and event
+ * that's handling the request.
+ *
+ * The constructor also initializes the state that will be passed to each of
+ * the plugins handling this request.
+ *
+ * @param {workbox-strategies.Strategy} strategy
+ * @param {Object} options
+ * @param {Request|string} options.request A request to run this strategy for.
+ * @param {ExtendableEvent} options.event The event associated with the
+ * request.
+ * @param {URL} [options.url]
+ * @param {*} [options.params] The return value from the
+ * {@link workbox-routing~matchCallback} (if applicable).
+ */
+ constructor(strategy, options) {
+ this._cacheKeys = {};
+ /**
+ * The request the strategy is performing (passed to the strategy's
+ * `handle()` or `handleAll()` method).
+ * @name request
+ * @instance
+ * @type {Request}
+ * @memberof workbox-strategies.StrategyHandler
+ */
+ /**
+ * The event associated with this request.
+ * @name event
+ * @instance
+ * @type {ExtendableEvent}
+ * @memberof workbox-strategies.StrategyHandler
+ */
+ /**
+ * A `URL` instance of `request.url` (if passed to the strategy's
+ * `handle()` or `handleAll()` method).
+ * Note: the `url` param will be present if the strategy was invoked
+ * from a workbox `Route` object.
+ * @name url
+ * @instance
+ * @type {URL|undefined}
+ * @memberof workbox-strategies.StrategyHandler
+ */
+ /**
+ * A `param` value (if passed to the strategy's
+ * `handle()` or `handleAll()` method).
+ * Note: the `param` param will be present if the strategy was invoked
+ * from a workbox `Route` object and the
+ * {@link workbox-routing~matchCallback} returned
+ * a truthy value (it will be that value).
+ * @name params
+ * @instance
+ * @type {*|undefined}
+ * @memberof workbox-strategies.StrategyHandler
+ */
+ {
+ finalAssertExports.isInstance(options.event, ExtendableEvent, {
+ moduleName: "workbox-strategies",
+ className: "StrategyHandler",
+ funcName: "constructor",
+ paramName: "options.event"
+ });
+ }
+ Object.assign(this, options);
+ this.event = options.event;
+ this._strategy = strategy;
+ this._handlerDeferred = new Deferred();
+ this._extendLifetimePromises = [];
+ // Copy the plugins list (since it's mutable on the strategy),
+ // so any mutations don't affect this handler instance.
+ this._plugins = [...strategy.plugins];
+ this._pluginStateMap = new Map();
+ for (const plugin of this._plugins) {
+ this._pluginStateMap.set(plugin, {});
+ }
+ this.event.waitUntil(this._handlerDeferred.promise);
+ }
+ /**
+ * Fetches a given request (and invokes any applicable plugin callback
+ * methods) using the `fetchOptions` (for non-navigation requests) and
+ * `plugins` defined on the `Strategy` object.
+ *
+ * The following plugin lifecycle methods are invoked when using this method:
+ * - `requestWillFetch()`
+ * - `fetchDidSucceed()`
+ * - `fetchDidFail()`
+ *
+ * @param {Request|string} input The URL or request to fetch.
+ * @return {Promise}
+ */
+ async fetch(input) {
+ const { event } = this;
+ let request = toRequest(input);
+ if (request.mode === "navigate" && event instanceof FetchEvent && event.preloadResponse) {
+ const possiblePreloadResponse = await event.preloadResponse;
+ if (possiblePreloadResponse) {
+ {
+ logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`);
+ }
+ return possiblePreloadResponse;
+ }
+ }
+ // If there is a fetchDidFail plugin, we need to save a clone of the
+ // original request before it's either modified by a requestWillFetch
+ // plugin or before the original request's body is consumed via fetch().
+ const originalRequest = this.hasCallback("fetchDidFail") ? request.clone() : null;
+ try {
+ for (const cb of this.iterateCallbacks("requestWillFetch")) {
+ request = await cb({
+ request: request.clone(),
+ event
+ });
+ }
+ } catch (err) {
+ if (err instanceof Error) {
+ throw new WorkboxError("plugin-error-request-will-fetch", {
+ thrownErrorMessage: err.message
+ });
+ }
+ }
+ // The request can be altered by plugins with `requestWillFetch` making
+ // the original request (most likely from a `fetch` event) different
+ // from the Request we make. Pass both to `fetchDidFail` to aid debugging.
+ const pluginFilteredRequest = request.clone();
+ try {
+ let fetchResponse;
+ // See https://github.com/GoogleChrome/workbox/issues/1796
+ fetchResponse = await fetch(request, request.mode === "navigate" ? undefined : this._strategy.fetchOptions);
+ if ("development" !== "production") {
+ logger.debug(
+ `Network request for ` +
+ `'${getFriendlyURL(request.url)}' returned a response with ` +
+ `status '${fetchResponse.status}'.`
+ );
+ }
+ for (const callback of this.iterateCallbacks("fetchDidSucceed")) {
+ fetchResponse = await callback({
+ event,
+ request: pluginFilteredRequest,
+ response: fetchResponse
+ });
+ }
+ return fetchResponse;
+ } catch (error) {
+ {
+ logger.log(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error);
+ }
+ // `originalRequest` will only exist if a `fetchDidFail` callback
+ // is being used (see above).
+ if (originalRequest) {
+ await this.runCallbacks("fetchDidFail", {
+ error: error,
+ event,
+ originalRequest: originalRequest.clone(),
+ request: pluginFilteredRequest.clone()
+ });
+ }
+ throw error;
+ }
+ }
+ /**
+ * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on
+ * the response generated by `this.fetch()`.
+ *
+ * The call to `this.cachePut()` automatically invokes `this.waitUntil()`,
+ * so you do not have to manually call `waitUntil()` on the event.
+ *
+ * @param {Request|string} input The request or URL to fetch and cache.
+ * @return {Promise}
+ */
+ async fetchAndCachePut(input) {
+ const response = await this.fetch(input);
+ const responseClone = response.clone();
+ void this.waitUntil(this.cachePut(input, responseClone));
+ return response;
+ }
+ /**
+ * Matches a request from the cache (and invokes any applicable plugin
+ * callback methods) using the `cacheName`, `matchOptions`, and `plugins`
+ * defined on the strategy object.
+ *
+ * The following plugin lifecycle methods are invoked when using this method:
+ * - cacheKeyWillByUsed()
+ * - cachedResponseWillByUsed()
+ *
+ * @param {Request|string} key The Request or URL to use as the cache key.
+ * @return {Promise} A matching response, if found.
+ */
+ async cacheMatch(key) {
+ const request = toRequest(key);
+ let cachedResponse;
+ const { cacheName, matchOptions } = this._strategy;
+ const effectiveRequest = await this.getCacheKey(request, "read");
+ const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), {
+ cacheName
+ });
+ cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);
+ {
+ if (cachedResponse) {
+ logger.debug(`Found a cached response in '${cacheName}'.`);
+ } else {
+ logger.debug(`No cached response found in '${cacheName}'.`);
+ }
+ }
+ for (const callback of this.iterateCallbacks("cachedResponseWillBeUsed")) {
+ cachedResponse =
+ (await callback({
cacheName,
matchOptions,
cachedResponse,
request: effectiveRequest,
event: this.event
})) || undefined;
- }
- return cachedResponse;
}
- /**
- * Puts a request/response pair in the cache (and invokes any applicable
- * plugin callback methods) using the `cacheName` and `plugins` defined on
- * the strategy object.
- *
- * The following plugin lifecycle methods are invoked when using this method:
- * - cacheKeyWillByUsed()
- * - cacheWillUpdate()
- * - cacheDidUpdate()
- *
- * @param {Request|string} key The request or URL to use as the cache key.
- * @param {Response} response The response to cache.
- * @return {Promise} `false` if a cacheWillUpdate caused the response
- * not be cached, and `true` otherwise.
- */
- async cachePut(key, response) {
- const request = toRequest(key);
- // Run in the next task to avoid blocking other cache reads.
- // https://github.com/w3c/ServiceWorker/issues/1397
- await timeout(0);
- const effectiveRequest = await this.getCacheKey(request, 'write');
- {
- if (effectiveRequest.method && effectiveRequest.method !== 'GET') {
- throw new WorkboxError('attempt-to-cache-non-get-request', {
- url: getFriendlyURL(effectiveRequest.url),
- method: effectiveRequest.method
- });
- }
- // See https://github.com/GoogleChrome/workbox/issues/2818
- const vary = response.headers.get('Vary');
- if (vary) {
- logger.debug(`The response for ${getFriendlyURL(effectiveRequest.url)} ` + `has a 'Vary: ${vary}' header. ` + `Consider setting the {ignoreVary: true} option on your strategy ` + `to ensure cache matching and deletion works as expected.`);
- }
- }
- if (!response) {
- {
- logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(effectiveRequest.url)}'.`);
- }
- throw new WorkboxError('cache-put-with-no-response', {
- url: getFriendlyURL(effectiveRequest.url)
+ return cachedResponse;
+ }
+ /**
+ * Puts a request/response pair in the cache (and invokes any applicable
+ * plugin callback methods) using the `cacheName` and `plugins` defined on
+ * the strategy object.
+ *
+ * The following plugin lifecycle methods are invoked when using this method:
+ * - cacheKeyWillByUsed()
+ * - cacheWillUpdate()
+ * - cacheDidUpdate()
+ *
+ * @param {Request|string} key The request or URL to use as the cache key.
+ * @param {Response} response The response to cache.
+ * @return {Promise} `false` if a cacheWillUpdate caused the response
+ * not be cached, and `true` otherwise.
+ */
+ async cachePut(key, response) {
+ const request = toRequest(key);
+ // Run in the next task to avoid blocking other cache reads.
+ // https://github.com/w3c/ServiceWorker/issues/1397
+ await timeout(0);
+ const effectiveRequest = await this.getCacheKey(request, "write");
+ {
+ if (effectiveRequest.method && effectiveRequest.method !== "GET") {
+ throw new WorkboxError("attempt-to-cache-non-get-request", {
+ url: getFriendlyURL(effectiveRequest.url),
+ method: effectiveRequest.method
});
}
- const responseToCache = await this._ensureResponseSafeToCache(response);
- if (!responseToCache) {
- {
- logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` + `will not be cached.`, responseToCache);
- }
- return false;
+ // See https://github.com/GoogleChrome/workbox/issues/2818
+ const vary = response.headers.get("Vary");
+ if (vary) {
+ logger.debug(
+ `The response for ${getFriendlyURL(effectiveRequest.url)} ` +
+ `has a 'Vary: ${vary}' header. ` +
+ `Consider setting the {ignoreVary: true} option on your strategy ` +
+ `to ensure cache matching and deletion works as expected.`
+ );
}
- const {
+ }
+ if (!response) {
+ {
+ logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(effectiveRequest.url)}'.`);
+ }
+ throw new WorkboxError("cache-put-with-no-response", {
+ url: getFriendlyURL(effectiveRequest.url)
+ });
+ }
+ const responseToCache = await this._ensureResponseSafeToCache(response);
+ if (!responseToCache) {
+ {
+ logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` + `will not be cached.`, responseToCache);
+ }
+ return false;
+ }
+ const { cacheName, matchOptions } = this._strategy;
+ const cache = await self.caches.open(cacheName);
+ const hasCacheUpdateCallback = this.hasCallback("cacheDidUpdate");
+ const oldResponse = hasCacheUpdateCallback
+ ? await cacheMatchIgnoreParams(
+ // TODO(philipwalton): the `__WB_REVISION__` param is a precaching
+ // feature. Consider into ways to only add this behavior if using
+ // precaching.
+ cache,
+ effectiveRequest.clone(),
+ ["__WB_REVISION__"],
+ matchOptions
+ )
+ : null;
+ {
+ logger.debug(
+ `Updating the '${cacheName}' cache with a new Response ` + `for ${getFriendlyURL(effectiveRequest.url)}.`
+ );
+ }
+ try {
+ await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);
+ } catch (error) {
+ if (error instanceof Error) {
+ // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError
+ if (error.name === "QuotaExceededError") {
+ await executeQuotaErrorCallbacks();
+ }
+ throw error;
+ }
+ }
+ for (const callback of this.iterateCallbacks("cacheDidUpdate")) {
+ await callback({
cacheName,
- matchOptions
- } = this._strategy;
- const cache = await self.caches.open(cacheName);
- const hasCacheUpdateCallback = this.hasCallback('cacheDidUpdate');
- const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams(
- // TODO(philipwalton): the `__WB_REVISION__` param is a precaching
- // feature. Consider into ways to only add this behavior if using
- // precaching.
- cache, effectiveRequest.clone(), ['__WB_REVISION__'], matchOptions) : null;
- {
- logger.debug(`Updating the '${cacheName}' cache with a new Response ` + `for ${getFriendlyURL(effectiveRequest.url)}.`);
- }
- try {
- await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);
- } catch (error) {
- if (error instanceof Error) {
- // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError
- if (error.name === 'QuotaExceededError') {
- await executeQuotaErrorCallbacks();
- }
- throw error;
- }
- }
- for (const callback of this.iterateCallbacks('cacheDidUpdate')) {
- await callback({
- cacheName,
- oldResponse,
- newResponse: responseToCache.clone(),
- request: effectiveRequest,
- event: this.event
- });
- }
- return true;
+ oldResponse,
+ newResponse: responseToCache.clone(),
+ request: effectiveRequest,
+ event: this.event
+ });
}
- /**
- * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and
- * executes any of those callbacks found in sequence. The final `Request`
- * object returned by the last plugin is treated as the cache key for cache
- * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have
- * been registered, the passed request is returned unmodified
- *
- * @param {Request} request
- * @param {string} mode
- * @return {Promise}
- */
- async getCacheKey(request, mode) {
- const key = `${request.url} | ${mode}`;
- if (!this._cacheKeys[key]) {
- let effectiveRequest = request;
- for (const callback of this.iterateCallbacks('cacheKeyWillBeUsed')) {
- effectiveRequest = toRequest(await callback({
+ return true;
+ }
+ /**
+ * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and
+ * executes any of those callbacks found in sequence. The final `Request`
+ * object returned by the last plugin is treated as the cache key for cache
+ * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have
+ * been registered, the passed request is returned unmodified
+ *
+ * @param {Request} request
+ * @param {string} mode
+ * @return {Promise}
+ */
+ async getCacheKey(request, mode) {
+ const key = `${request.url} | ${mode}`;
+ if (!this._cacheKeys[key]) {
+ let effectiveRequest = request;
+ for (const callback of this.iterateCallbacks("cacheKeyWillBeUsed")) {
+ effectiveRequest = toRequest(
+ await callback({
mode,
request: effectiveRequest,
event: this.event,
// params has a type any can't change right now.
params: this.params // eslint-disable-line
- }));
- }
- this._cacheKeys[key] = effectiveRequest;
+ })
+ );
}
- return this._cacheKeys[key];
+ this._cacheKeys[key] = effectiveRequest;
}
- /**
- * Returns true if the strategy has at least one plugin with the given
- * callback.
- *
- * @param {string} name The name of the callback to check for.
- * @return {boolean}
- */
- hasCallback(name) {
- for (const plugin of this._strategy.plugins) {
- if (name in plugin) {
- return true;
- }
- }
- return false;
- }
- /**
- * Runs all plugin callbacks matching the given name, in order, passing the
- * given param object (merged ith the current plugin state) as the only
- * argument.
- *
- * Note: since this method runs all plugins, it's not suitable for cases
- * where the return value of a callback needs to be applied prior to calling
- * the next callback. See
- * {@link workbox-strategies.StrategyHandler#iterateCallbacks}
- * below for how to handle that case.
- *
- * @param {string} name The name of the callback to run within each plugin.
- * @param {Object} param The object to pass as the first (and only) param
- * when executing each callback. This object will be merged with the
- * current plugin state prior to callback execution.
- */
- async runCallbacks(name, param) {
- for (const callback of this.iterateCallbacks(name)) {
- // TODO(philipwalton): not sure why `any` is needed. It seems like
- // this should work with `as WorkboxPluginCallbackParam[C]`.
- await callback(param);
+ return this._cacheKeys[key];
+ }
+ /**
+ * Returns true if the strategy has at least one plugin with the given
+ * callback.
+ *
+ * @param {string} name The name of the callback to check for.
+ * @return {boolean}
+ */
+ hasCallback(name) {
+ for (const plugin of this._strategy.plugins) {
+ if (name in plugin) {
+ return true;
}
}
- /**
- * Accepts a callback and returns an iterable of matching plugin callbacks,
- * where each callback is wrapped with the current handler state (i.e. when
- * you call each callback, whatever object parameter you pass it will
- * be merged with the plugin's current state).
- *
- * @param {string} name The name fo the callback to run
- * @return {Array}
- */
- *iterateCallbacks(name) {
- for (const plugin of this._strategy.plugins) {
- if (typeof plugin[name] === 'function') {
- const state = this._pluginStateMap.get(plugin);
- const statefulCallback = param => {
- const statefulParam = Object.assign(Object.assign({}, param), {
- state
- });
- // TODO(philipwalton): not sure why `any` is needed. It seems like
- // this should work with `as WorkboxPluginCallbackParam[C]`.
- return plugin[name](statefulParam);
- };
- yield statefulCallback;
- }
+ return false;
+ }
+ /**
+ * Runs all plugin callbacks matching the given name, in order, passing the
+ * given param object (merged ith the current plugin state) as the only
+ * argument.
+ *
+ * Note: since this method runs all plugins, it's not suitable for cases
+ * where the return value of a callback needs to be applied prior to calling
+ * the next callback. See
+ * {@link workbox-strategies.StrategyHandler#iterateCallbacks}
+ * below for how to handle that case.
+ *
+ * @param {string} name The name of the callback to run within each plugin.
+ * @param {Object} param The object to pass as the first (and only) param
+ * when executing each callback. This object will be merged with the
+ * current plugin state prior to callback execution.
+ */
+ async runCallbacks(name, param) {
+ for (const callback of this.iterateCallbacks(name)) {
+ // TODO(philipwalton): not sure why `any` is needed. It seems like
+ // this should work with `as WorkboxPluginCallbackParam[C]`.
+ await callback(param);
+ }
+ }
+ /**
+ * Accepts a callback and returns an iterable of matching plugin callbacks,
+ * where each callback is wrapped with the current handler state (i.e. when
+ * you call each callback, whatever object parameter you pass it will
+ * be merged with the plugin's current state).
+ *
+ * @param {string} name The name fo the callback to run
+ * @return {Array}
+ */
+ *iterateCallbacks(name) {
+ for (const plugin of this._strategy.plugins) {
+ if (typeof plugin[name] === "function") {
+ const state = this._pluginStateMap.get(plugin);
+ const statefulCallback = (param) => {
+ const statefulParam = Object.assign(Object.assign({}, param), {
+ state
+ });
+ // TODO(philipwalton): not sure why `any` is needed. It seems like
+ // this should work with `as WorkboxPluginCallbackParam[C]`.
+ return plugin[name](statefulParam);
+ };
+ yield statefulCallback;
}
}
- /**
- * Adds a promise to the
- * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises}
- * of the event event associated with the request being handled (usually a
- * `FetchEvent`).
- *
- * Note: you can await
- * {@link workbox-strategies.StrategyHandler~doneWaiting}
- * to know when all added promises have settled.
- *
- * @param {Promise} promise A promise to add to the extend lifetime promises
- * of the event that triggered the request.
- */
- waitUntil(promise) {
- this._extendLifetimePromises.push(promise);
- return promise;
+ }
+ /**
+ * Adds a promise to the
+ * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises}
+ * of the event event associated with the request being handled (usually a
+ * `FetchEvent`).
+ *
+ * Note: you can await
+ * {@link workbox-strategies.StrategyHandler~doneWaiting}
+ * to know when all added promises have settled.
+ *
+ * @param {Promise} promise A promise to add to the extend lifetime promises
+ * of the event that triggered the request.
+ */
+ waitUntil(promise) {
+ this._extendLifetimePromises.push(promise);
+ return promise;
+ }
+ /**
+ * Returns a promise that resolves once all promises passed to
+ * {@link workbox-strategies.StrategyHandler~waitUntil}
+ * have settled.
+ *
+ * Note: any work done after `doneWaiting()` settles should be manually
+ * passed to an event's `waitUntil()` method (not this handler's
+ * `waitUntil()` method), otherwise the service worker thread my be killed
+ * prior to your work completing.
+ */
+ async doneWaiting() {
+ let promise;
+ while ((promise = this._extendLifetimePromises.shift())) {
+ await promise;
}
- /**
- * Returns a promise that resolves once all promises passed to
- * {@link workbox-strategies.StrategyHandler~waitUntil}
- * have settled.
- *
- * Note: any work done after `doneWaiting()` settles should be manually
- * passed to an event's `waitUntil()` method (not this handler's
- * `waitUntil()` method), otherwise the service worker thread my be killed
- * prior to your work completing.
- */
- async doneWaiting() {
- let promise;
- while (promise = this._extendLifetimePromises.shift()) {
- await promise;
- }
- }
- /**
- * Stops running the strategy and immediately resolves any pending
- * `waitUntil()` promises.
- */
- destroy() {
- this._handlerDeferred.resolve(null);
- }
- /**
- * This method will call cacheWillUpdate on the available plugins (or use
- * status === 200) to determine if the Response is safe and valid to cache.
- *
- * @param {Request} options.request
- * @param {Response} options.response
- * @return {Promise}
- *
- * @private
- */
- async _ensureResponseSafeToCache(response) {
- let responseToCache = response;
- let pluginsUsed = false;
- for (const callback of this.iterateCallbacks('cacheWillUpdate')) {
- responseToCache = (await callback({
+ }
+ /**
+ * Stops running the strategy and immediately resolves any pending
+ * `waitUntil()` promises.
+ */
+ destroy() {
+ this._handlerDeferred.resolve(null);
+ }
+ /**
+ * This method will call cacheWillUpdate on the available plugins (or use
+ * status === 200) to determine if the Response is safe and valid to cache.
+ *
+ * @param {Request} options.request
+ * @param {Response} options.response
+ * @return {Promise}
+ *
+ * @private
+ */
+ async _ensureResponseSafeToCache(response) {
+ let responseToCache = response;
+ let pluginsUsed = false;
+ for (const callback of this.iterateCallbacks("cacheWillUpdate")) {
+ responseToCache =
+ (await callback({
request: this.request,
response: responseToCache,
event: this.event
})) || undefined;
- pluginsUsed = true;
- if (!responseToCache) {
- break;
- }
+ pluginsUsed = true;
+ if (!responseToCache) {
+ break;
}
- if (!pluginsUsed) {
- if (responseToCache && responseToCache.status !== 200) {
- responseToCache = undefined;
- }
- {
- if (responseToCache) {
- if (responseToCache.status !== 200) {
- if (responseToCache.status === 0) {
- logger.warn(`The response for '${this.request.url}' ` + `is an opaque response. The caching strategy that you're ` + `using will not cache opaque responses by default.`);
- } else {
- logger.debug(`The response for '${this.request.url}' ` + `returned a status code of '${response.status}' and won't ` + `be cached as a result.`);
- }
+ }
+ if (!pluginsUsed) {
+ if (responseToCache && responseToCache.status !== 200) {
+ responseToCache = undefined;
+ }
+ {
+ if (responseToCache) {
+ if (responseToCache.status !== 200) {
+ if (responseToCache.status === 0) {
+ logger.warn(
+ `The response for '${this.request.url}' ` +
+ `is an opaque response. The caching strategy that you're ` +
+ `using will not cache opaque responses by default.`
+ );
+ } else {
+ logger.debug(
+ `The response for '${this.request.url}' ` +
+ `returned a status code of '${response.status}' and won't ` +
+ `be cached as a result.`
+ );
}
}
}
}
- return responseToCache;
}
+ return responseToCache;
}
+ }
- /*
+ /*
Copyright 2020 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
+ /**
+ * An abstract base class that all other strategy classes must extend from:
+ *
+ * @memberof workbox-strategies
+ */
+ class Strategy {
/**
- * An abstract base class that all other strategy classes must extend from:
+ * Creates a new instance of the strategy and sets all documented option
+ * properties as public instance properties.
*
- * @memberof workbox-strategies
+ * Note: if a custom strategy class extends the base Strategy class and does
+ * not need more than these properties, it does not need to define its own
+ * constructor.
+ *
+ * @param {Object} [options]
+ * @param {string} [options.cacheName] Cache name to store and retrieve
+ * requests. Defaults to the cache names provided by
+ * {@link workbox-core.cacheNames}.
+ * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}
+ * to use in conjunction with this caching strategy.
+ * @param {Object} [options.fetchOptions] Values passed along to the
+ * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters)
+ * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796)
+ * `fetch()` requests made by this strategy.
+ * @param {Object} [options.matchOptions] The
+ * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions}
+ * for any `cache.match()` or `cache.put()` calls made by this strategy.
*/
- class Strategy {
+ constructor(options = {}) {
/**
- * Creates a new instance of the strategy and sets all documented option
- * properties as public instance properties.
- *
- * Note: if a custom strategy class extends the base Strategy class and does
- * not need more than these properties, it does not need to define its own
- * constructor.
- *
- * @param {Object} [options]
- * @param {string} [options.cacheName] Cache name to store and retrieve
+ * Cache name to store and retrieve
* requests. Defaults to the cache names provided by
* {@link workbox-core.cacheNames}.
- * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}
- * to use in conjunction with this caching strategy.
- * @param {Object} [options.fetchOptions] Values passed along to the
- * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters)
- * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796)
- * `fetch()` requests made by this strategy.
- * @param {Object} [options.matchOptions] The
+ *
+ * @type {string}
+ */
+ this.cacheName = cacheNames.getRuntimeName(options.cacheName);
+ /**
+ * The list
+ * [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}
+ * used by this strategy.
+ *
+ * @type {Array}
+ */
+ this.plugins = options.plugins || [];
+ /**
+ * Values passed along to the
+ * [`init`]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters}
+ * of all fetch() requests made by this strategy.
+ *
+ * @type {Object}
+ */
+ this.fetchOptions = options.fetchOptions;
+ /**
+ * The
* [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions}
* for any `cache.match()` or `cache.put()` calls made by this strategy.
+ *
+ * @type {Object}
*/
- constructor(options = {}) {
- /**
- * Cache name to store and retrieve
- * requests. Defaults to the cache names provided by
- * {@link workbox-core.cacheNames}.
- *
- * @type {string}
- */
- this.cacheName = cacheNames.getRuntimeName(options.cacheName);
- /**
- * The list
- * [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}
- * used by this strategy.
- *
- * @type {Array}
- */
- this.plugins = options.plugins || [];
- /**
- * Values passed along to the
- * [`init`]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters}
- * of all fetch() requests made by this strategy.
- *
- * @type {Object}
- */
- this.fetchOptions = options.fetchOptions;
- /**
- * The
- * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions}
- * for any `cache.match()` or `cache.put()` calls made by this strategy.
- *
- * @type {Object}
- */
- this.matchOptions = options.matchOptions;
- }
- /**
- * Perform a request strategy and returns a `Promise` that will resolve with
- * a `Response`, invoking all relevant plugin callbacks.
- *
- * When a strategy instance is registered with a Workbox
- * {@link workbox-routing.Route}, this method is automatically
- * called when the route matches.
- *
- * Alternatively, this method can be used in a standalone `FetchEvent`
- * listener by passing it to `event.respondWith()`.
- *
- * @param {FetchEvent|Object} options A `FetchEvent` or an object with the
- * properties listed below.
- * @param {Request|string} options.request A request to run this strategy for.
- * @param {ExtendableEvent} options.event The event associated with the
- * request.
- * @param {URL} [options.url]
- * @param {*} [options.params]
- */
- handle(options) {
- const [responseDone] = this.handleAll(options);
- return responseDone;
- }
- /**
- * Similar to {@link workbox-strategies.Strategy~handle}, but
- * instead of just returning a `Promise` that resolves to a `Response` it
- * it will return an tuple of `[response, done]` promises, where the former
- * (`response`) is equivalent to what `handle()` returns, and the latter is a
- * Promise that will resolve once any promises that were added to
- * `event.waitUntil()` as part of performing the strategy have completed.
- *
- * You can await the `done` promise to ensure any extra work performed by
- * the strategy (usually caching responses) completes successfully.
- *
- * @param {FetchEvent|Object} options A `FetchEvent` or an object with the
- * properties listed below.
- * @param {Request|string} options.request A request to run this strategy for.
- * @param {ExtendableEvent} options.event The event associated with the
- * request.
- * @param {URL} [options.url]
- * @param {*} [options.params]
- * @return {Array} A tuple of [response, done]
- * promises that can be used to determine when the response resolves as
- * well as when the handler has completed all its work.
- */
- handleAll(options) {
- // Allow for flexible options to be passed.
- if (options instanceof FetchEvent) {
- options = {
- event: options,
- request: options.request
- };
- }
- const event = options.event;
- const request = typeof options.request === 'string' ? new Request(options.request) : options.request;
- const params = 'params' in options ? options.params : undefined;
- const handler = new StrategyHandler(this, {
- event,
- request,
- params
- });
- const responseDone = this._getResponse(handler, request, event);
- const handlerDone = this._awaitComplete(responseDone, handler, request, event);
- // Return an array of promises, suitable for use with Promise.all().
- return [responseDone, handlerDone];
- }
- async _getResponse(handler, request, event) {
- await handler.runCallbacks('handlerWillStart', {
- event,
- request
- });
- let response = undefined;
- try {
- response = await this._handle(request, handler);
- // The "official" Strategy subclasses all throw this error automatically,
- // but in case a third-party Strategy doesn't, ensure that we have a
- // consistent failure when there's no response or an error response.
- if (!response || response.type === 'error') {
- throw new WorkboxError('no-response', {
- url: request.url
- });
- }
- } catch (error) {
- if (error instanceof Error) {
- for (const callback of handler.iterateCallbacks('handlerDidError')) {
- response = await callback({
- error,
- event,
- request
- });
- if (response) {
- break;
- }
- }
- }
- if (!response) {
- throw error;
- } else {
- logger.log(`While responding to '${getFriendlyURL(request.url)}', ` + `an ${error instanceof Error ? error.toString() : ''} error occurred. Using a fallback response provided by ` + `a handlerDidError plugin.`);
- }
- }
- for (const callback of handler.iterateCallbacks('handlerWillRespond')) {
- response = await callback({
- event,
- request,
- response
- });
- }
- return response;
- }
- async _awaitComplete(responseDone, handler, request, event) {
- let response;
- let error;
- try {
- response = await responseDone;
- } catch (error) {
- // Ignore errors, as response errors should be caught via the `response`
- // promise above. The `done` promise will only throw for errors in
- // promises passed to `handler.waitUntil()`.
- }
- try {
- await handler.runCallbacks('handlerDidRespond', {
- event,
- request,
- response
- });
- await handler.doneWaiting();
- } catch (waitUntilError) {
- if (waitUntilError instanceof Error) {
- error = waitUntilError;
- }
- }
- await handler.runCallbacks('handlerDidComplete', {
- event,
- request,
- response,
- error: error
- });
- handler.destroy();
- if (error) {
- throw error;
- }
- }
+ this.matchOptions = options.matchOptions;
}
/**
- * Classes extending the `Strategy` based class should implement this method,
- * and leverage the {@link workbox-strategies.StrategyHandler}
- * arg to perform all fetching and cache logic, which will ensure all relevant
- * cache, cache options, fetch options and plugins are used (per the current
- * strategy instance).
+ * Perform a request strategy and returns a `Promise` that will resolve with
+ * a `Response`, invoking all relevant plugin callbacks.
*
- * @name _handle
- * @instance
- * @abstract
- * @function
- * @param {Request} request
- * @param {workbox-strategies.StrategyHandler} handler
- * @return {Promise}
+ * When a strategy instance is registered with a Workbox
+ * {@link workbox-routing.Route}, this method is automatically
+ * called when the route matches.
*
- * @memberof workbox-strategies.Strategy
+ * Alternatively, this method can be used in a standalone `FetchEvent`
+ * listener by passing it to `event.respondWith()`.
+ *
+ * @param {FetchEvent|Object} options A `FetchEvent` or an object with the
+ * properties listed below.
+ * @param {Request|string} options.request A request to run this strategy for.
+ * @param {ExtendableEvent} options.event The event associated with the
+ * request.
+ * @param {URL} [options.url]
+ * @param {*} [options.params]
*/
-
- /*
- Copyright 2020 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
+ handle(options) {
+ const [responseDone] = this.handleAll(options);
+ return responseDone;
+ }
/**
- * A {@link workbox-strategies.Strategy} implementation
- * specifically designed to work with
- * {@link workbox-precaching.PrecacheController}
- * to both cache and fetch precached assets.
+ * Similar to {@link workbox-strategies.Strategy~handle}, but
+ * instead of just returning a `Promise` that resolves to a `Response` it
+ * it will return an tuple of `[response, done]` promises, where the former
+ * (`response`) is equivalent to what `handle()` returns, and the latter is a
+ * Promise that will resolve once any promises that were added to
+ * `event.waitUntil()` as part of performing the strategy have completed.
*
- * Note: an instance of this class is created automatically when creating a
- * `PrecacheController`; it's generally not necessary to create this yourself.
+ * You can await the `done` promise to ensure any extra work performed by
+ * the strategy (usually caching responses) completes successfully.
*
- * @extends workbox-strategies.Strategy
- * @memberof workbox-precaching
+ * @param {FetchEvent|Object} options A `FetchEvent` or an object with the
+ * properties listed below.
+ * @param {Request|string} options.request A request to run this strategy for.
+ * @param {ExtendableEvent} options.event The event associated with the
+ * request.
+ * @param {URL} [options.url]
+ * @param {*} [options.params]
+ * @return {Array} A tuple of [response, done]
+ * promises that can be used to determine when the response resolves as
+ * well as when the handler has completed all its work.
*/
- class PrecacheStrategy extends Strategy {
- /**
- *
- * @param {Object} [options]
- * @param {string} [options.cacheName] Cache name to store and retrieve
- * requests. Defaults to the cache names provided by
- * {@link workbox-core.cacheNames}.
- * @param {Array} [options.plugins] {@link https://developers.google.com/web/tools/workbox/guides/using-plugins|Plugins}
- * to use in conjunction with this caching strategy.
- * @param {Object} [options.fetchOptions] Values passed along to the
- * {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init}
- * of all fetch() requests made by this strategy.
- * @param {Object} [options.matchOptions] The
- * {@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions|CacheQueryOptions}
- * for any `cache.match()` or `cache.put()` calls made by this strategy.
- * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to
- * get the response from the network if there's a precache miss.
- */
- constructor(options = {}) {
- options.cacheName = cacheNames.getPrecacheName(options.cacheName);
- super(options);
- this._fallbackToNetwork = options.fallbackToNetwork === false ? false : true;
- // Redirected responses cannot be used to satisfy a navigation request, so
- // any redirected response must be "copied" rather than cloned, so the new
- // response doesn't contain the `redirected` flag. See:
- // https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1
- this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin);
+ handleAll(options) {
+ // Allow for flexible options to be passed.
+ if (options instanceof FetchEvent) {
+ options = {
+ event: options,
+ request: options.request
+ };
}
- /**
- * @private
- * @param {Request|string} request A request to run this strategy for.
- * @param {workbox-strategies.StrategyHandler} handler The event that
- * triggered the request.
- * @return {Promise}
- */
- async _handle(request, handler) {
- const response = await handler.cacheMatch(request);
- if (response) {
- return response;
- }
- // If this is an `install` event for an entry that isn't already cached,
- // then populate the cache.
- if (handler.event && handler.event.type === 'install') {
- return await this._handleInstall(request, handler);
- }
- // Getting here means something went wrong. An entry that should have been
- // precached wasn't found in the cache.
- return await this._handleFetch(request, handler);
- }
- async _handleFetch(request, handler) {
- let response;
- const params = handler.params || {};
- // Fall back to the network if we're configured to do so.
- if (this._fallbackToNetwork) {
- {
- logger.warn(`The precached response for ` + `${getFriendlyURL(request.url)} in ${this.cacheName} was not ` + `found. Falling back to the network.`);
- }
- const integrityInManifest = params.integrity;
- const integrityInRequest = request.integrity;
- const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest;
- // Do not add integrity if the original request is no-cors
- // See https://github.com/GoogleChrome/workbox/issues/3096
- response = await handler.fetch(new Request(request, {
- integrity: request.mode !== 'no-cors' ? integrityInRequest || integrityInManifest : undefined
- }));
- // It's only "safe" to repair the cache if we're using SRI to guarantee
- // that the response matches the precache manifest's expectations,
- // and there's either a) no integrity property in the incoming request
- // or b) there is an integrity, and it matches the precache manifest.
- // See https://github.com/GoogleChrome/workbox/issues/2858
- // Also if the original request users no-cors we don't use integrity.
- // See https://github.com/GoogleChrome/workbox/issues/3096
- if (integrityInManifest && noIntegrityConflict && request.mode !== 'no-cors') {
- this._useDefaultCacheabilityPluginIfNeeded();
- const wasCached = await handler.cachePut(request, response.clone());
- {
- if (wasCached) {
- logger.log(`A response for ${getFriendlyURL(request.url)} ` + `was used to "repair" the precache.`);
- }
- }
- }
- } else {
- // This shouldn't normally happen, but there are edge cases:
- // https://github.com/GoogleChrome/workbox/issues/1441
- throw new WorkboxError('missing-precache-entry', {
- cacheName: this.cacheName,
+ const event = options.event;
+ const request = typeof options.request === "string" ? new Request(options.request) : options.request;
+ const params = "params" in options ? options.params : undefined;
+ const handler = new StrategyHandler(this, {
+ event,
+ request,
+ params
+ });
+ const responseDone = this._getResponse(handler, request, event);
+ const handlerDone = this._awaitComplete(responseDone, handler, request, event);
+ // Return an array of promises, suitable for use with Promise.all().
+ return [responseDone, handlerDone];
+ }
+ async _getResponse(handler, request, event) {
+ await handler.runCallbacks("handlerWillStart", {
+ event,
+ request
+ });
+ let response = undefined;
+ try {
+ response = await this._handle(request, handler);
+ // The "official" Strategy subclasses all throw this error automatically,
+ // but in case a third-party Strategy doesn't, ensure that we have a
+ // consistent failure when there's no response or an error response.
+ if (!response || response.type === "error") {
+ throw new WorkboxError("no-response", {
url: request.url
});
}
- {
- const cacheKey = params.cacheKey || (await handler.getCacheKey(request, 'read'));
- // Workbox is going to handle the route.
- // print the routing details to the console.
- logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL(request.url));
- logger.log(`Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}`);
- logger.groupCollapsed(`View request details here.`);
- logger.log(request);
- logger.groupEnd();
- logger.groupCollapsed(`View response details here.`);
- logger.log(response);
- logger.groupEnd();
- logger.groupEnd();
- }
- return response;
- }
- async _handleInstall(request, handler) {
- this._useDefaultCacheabilityPluginIfNeeded();
- const response = await handler.fetch(request);
- // Make sure we defer cachePut() until after we know the response
- // should be cached; see https://github.com/GoogleChrome/workbox/issues/2737
- const wasCached = await handler.cachePut(request, response.clone());
- if (!wasCached) {
- // Throwing here will lead to the `install` handler failing, which
- // we want to do if *any* of the responses aren't safe to cache.
- throw new WorkboxError('bad-precaching-response', {
- url: request.url,
- status: response.status
- });
- }
- return response;
- }
- /**
- * This method is complex, as there a number of things to account for:
- *
- * The `plugins` array can be set at construction, and/or it might be added to
- * to at any time before the strategy is used.
- *
- * At the time the strategy is used (i.e. during an `install` event), there
- * needs to be at least one plugin that implements `cacheWillUpdate` in the
- * array, other than `copyRedirectedCacheableResponsesPlugin`.
- *
- * - If this method is called and there are no suitable `cacheWillUpdate`
- * plugins, we need to add `defaultPrecacheCacheabilityPlugin`.
- *
- * - If this method is called and there is exactly one `cacheWillUpdate`, then
- * we don't have to do anything (this might be a previously added
- * `defaultPrecacheCacheabilityPlugin`, or it might be a custom plugin).
- *
- * - If this method is called and there is more than one `cacheWillUpdate`,
- * then we need to check if one is `defaultPrecacheCacheabilityPlugin`. If so,
- * we need to remove it. (This situation is unlikely, but it could happen if
- * the strategy is used multiple times, the first without a `cacheWillUpdate`,
- * and then later on after manually adding a custom `cacheWillUpdate`.)
- *
- * See https://github.com/GoogleChrome/workbox/issues/2737 for more context.
- *
- * @private
- */
- _useDefaultCacheabilityPluginIfNeeded() {
- let defaultPluginIndex = null;
- let cacheWillUpdatePluginCount = 0;
- for (const [index, plugin] of this.plugins.entries()) {
- // Ignore the copy redirected plugin when determining what to do.
- if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) {
- continue;
- }
- // Save the default plugin's index, in case it needs to be removed.
- if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) {
- defaultPluginIndex = index;
- }
- if (plugin.cacheWillUpdate) {
- cacheWillUpdatePluginCount++;
- }
- }
- if (cacheWillUpdatePluginCount === 0) {
- this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin);
- } else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) {
- // Only remove the default plugin; multiple custom plugins are allowed.
- this.plugins.splice(defaultPluginIndex, 1);
- }
- // Nothing needs to be done if cacheWillUpdatePluginCount is 1
- }
- }
- PrecacheStrategy.defaultPrecacheCacheabilityPlugin = {
- async cacheWillUpdate({
- response
- }) {
- if (!response || response.status >= 400) {
- return null;
- }
- return response;
- }
- };
- PrecacheStrategy.copyRedirectedCacheableResponsesPlugin = {
- async cacheWillUpdate({
- response
- }) {
- return response.redirected ? await copyResponse(response) : response;
- }
- };
-
- /*
- Copyright 2019 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * Performs efficient precaching of assets.
- *
- * @memberof workbox-precaching
- */
- class PrecacheController {
- /**
- * Create a new PrecacheController.
- *
- * @param {Object} [options]
- * @param {string} [options.cacheName] The cache to use for precaching.
- * @param {string} [options.plugins] Plugins to use when precaching as well
- * as responding to fetch events for precached assets.
- * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to
- * get the response from the network if there's a precache miss.
- */
- constructor({
- cacheName,
- plugins = [],
- fallbackToNetwork = true
- } = {}) {
- this._urlsToCacheKeys = new Map();
- this._urlsToCacheModes = new Map();
- this._cacheKeysToIntegrities = new Map();
- this._strategy = new PrecacheStrategy({
- cacheName: cacheNames.getPrecacheName(cacheName),
- plugins: [...plugins, new PrecacheCacheKeyPlugin({
- precacheController: this
- })],
- fallbackToNetwork
- });
- // Bind the install and activate methods to the instance.
- this.install = this.install.bind(this);
- this.activate = this.activate.bind(this);
- }
- /**
- * @type {workbox-precaching.PrecacheStrategy} The strategy created by this controller and
- * used to cache assets and respond to fetch events.
- */
- get strategy() {
- return this._strategy;
- }
- /**
- * Adds items to the precache list, removing any duplicates and
- * stores the files in the
- * {@link workbox-core.cacheNames|"precache cache"} when the service
- * worker installs.
- *
- * This method can be called multiple times.
- *
- * @param {Array} [entries=[]] Array of entries to precache.
- */
- precache(entries) {
- this.addToCacheList(entries);
- if (!this._installAndActiveListenersAdded) {
- self.addEventListener('install', this.install);
- self.addEventListener('activate', this.activate);
- this._installAndActiveListenersAdded = true;
- }
- }
- /**
- * This method will add items to the precache list, removing duplicates
- * and ensuring the information is valid.
- *
- * @param {Array} entries
- * Array of entries to precache.
- */
- addToCacheList(entries) {
- {
- finalAssertExports.isArray(entries, {
- moduleName: 'workbox-precaching',
- className: 'PrecacheController',
- funcName: 'addToCacheList',
- paramName: 'entries'
- });
- }
- const urlsToWarnAbout = [];
- for (const entry of entries) {
- // See https://github.com/GoogleChrome/workbox/issues/2259
- if (typeof entry === 'string') {
- urlsToWarnAbout.push(entry);
- } else if (entry && entry.revision === undefined) {
- urlsToWarnAbout.push(entry.url);
- }
- const {
- cacheKey,
- url
- } = createCacheKey(entry);
- const cacheMode = typeof entry !== 'string' && entry.revision ? 'reload' : 'default';
- if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) {
- throw new WorkboxError('add-to-cache-list-conflicting-entries', {
- firstEntry: this._urlsToCacheKeys.get(url),
- secondEntry: cacheKey
+ } catch (error) {
+ if (error instanceof Error) {
+ for (const callback of handler.iterateCallbacks("handlerDidError")) {
+ response = await callback({
+ error,
+ event,
+ request
});
- }
- if (typeof entry !== 'string' && entry.integrity) {
- if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) {
- throw new WorkboxError('add-to-cache-list-conflicting-integrities', {
- url
- });
- }
- this._cacheKeysToIntegrities.set(cacheKey, entry.integrity);
- }
- this._urlsToCacheKeys.set(url, cacheKey);
- this._urlsToCacheModes.set(url, cacheMode);
- if (urlsToWarnAbout.length > 0) {
- const warningMessage = `Workbox is precaching URLs without revision ` + `info: ${urlsToWarnAbout.join(', ')}\nThis is generally NOT safe. ` + `Learn more at https://bit.ly/wb-precache`;
- {
- logger.warn(warningMessage);
+ if (response) {
+ break;
}
}
}
+ if (!response) {
+ throw error;
+ } else {
+ logger.log(
+ `While responding to '${getFriendlyURL(request.url)}', ` +
+ `an ${error instanceof Error ? error.toString() : ""} error occurred. Using a fallback response provided by ` +
+ `a handlerDidError plugin.`
+ );
+ }
}
- /**
- * Precaches new and updated assets. Call this method from the service worker
- * install event.
- *
- * Note: this method calls `event.waitUntil()` for you, so you do not need
- * to call it yourself in your event handlers.
- *
- * @param {ExtendableEvent} event
- * @return {Promise}
- */
- install(event) {
- // waitUntil returns Promise
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return waitUntil(event, async () => {
- const installReportPlugin = new PrecacheInstallReportPlugin();
- this.strategy.plugins.push(installReportPlugin);
- // Cache entries one at a time.
- // See https://github.com/GoogleChrome/workbox/issues/2528
- for (const [url, cacheKey] of this._urlsToCacheKeys) {
- const integrity = this._cacheKeysToIntegrities.get(cacheKey);
- const cacheMode = this._urlsToCacheModes.get(url);
- const request = new Request(url, {
- integrity,
- cache: cacheMode,
- credentials: 'same-origin'
- });
- await Promise.all(this.strategy.handleAll({
- params: {
- cacheKey
- },
- request,
- event
- }));
- }
- const {
- updatedURLs,
- notUpdatedURLs
- } = installReportPlugin;
- {
- printInstallDetails(updatedURLs, notUpdatedURLs);
- }
- return {
- updatedURLs,
- notUpdatedURLs
- };
+ for (const callback of handler.iterateCallbacks("handlerWillRespond")) {
+ response = await callback({
+ event,
+ request,
+ response
});
}
- /**
- * Deletes assets that are no longer present in the current precache manifest.
- * Call this method from the service worker activate event.
- *
- * Note: this method calls `event.waitUntil()` for you, so you do not need
- * to call it yourself in your event handlers.
- *
- * @param {ExtendableEvent} event
- * @return {Promise}
- */
- activate(event) {
- // waitUntil returns Promise
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return waitUntil(event, async () => {
- const cache = await self.caches.open(this.strategy.cacheName);
- const currentlyCachedRequests = await cache.keys();
- const expectedCacheKeys = new Set(this._urlsToCacheKeys.values());
- const deletedURLs = [];
- for (const request of currentlyCachedRequests) {
- if (!expectedCacheKeys.has(request.url)) {
- await cache.delete(request);
- deletedURLs.push(request.url);
- }
- }
- {
- printCleanupDetails(deletedURLs);
- }
- return {
- deletedURLs
- };
+ return response;
+ }
+ async _awaitComplete(responseDone, handler, request, event) {
+ let response;
+ let error;
+ try {
+ response = await responseDone;
+ } catch (error) {
+ // Ignore errors, as response errors should be caught via the `response`
+ // promise above. The `done` promise will only throw for errors in
+ // promises passed to `handler.waitUntil()`.
+ }
+ try {
+ await handler.runCallbacks("handlerDidRespond", {
+ event,
+ request,
+ response
});
- }
- /**
- * Returns a mapping of a precached URL to the corresponding cache key, taking
- * into account the revision information for the URL.
- *
- * @return {Map} A URL to cache key mapping.
- */
- getURLsToCacheKeys() {
- return this._urlsToCacheKeys;
- }
- /**
- * Returns a list of all the URLs that have been precached by the current
- * service worker.
- *
- * @return {Array} The precached URLs.
- */
- getCachedURLs() {
- return [...this._urlsToCacheKeys.keys()];
- }
- /**
- * Returns the cache key used for storing a given URL. If that URL is
- * unversioned, like `/index.html', then the cache key will be the original
- * URL with a search parameter appended to it.
- *
- * @param {string} url A URL whose cache key you want to look up.
- * @return {string} The versioned URL that corresponds to a cache key
- * for the original URL, or undefined if that URL isn't precached.
- */
- getCacheKeyForURL(url) {
- const urlObject = new URL(url, location.href);
- return this._urlsToCacheKeys.get(urlObject.href);
- }
- /**
- * @param {string} url A cache key whose SRI you want to look up.
- * @return {string} The subresource integrity associated with the cache key,
- * or undefined if it's not set.
- */
- getIntegrityForCacheKey(cacheKey) {
- return this._cacheKeysToIntegrities.get(cacheKey);
- }
- /**
- * This acts as a drop-in replacement for
- * [`cache.match()`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match)
- * with the following differences:
- *
- * - It knows what the name of the precache is, and only checks in that cache.
- * - It allows you to pass in an "original" URL without versioning parameters,
- * and it will automatically look up the correct cache key for the currently
- * active revision of that URL.
- *
- * E.g., `matchPrecache('index.html')` will find the correct precached
- * response for the currently active service worker, even if the actual cache
- * key is `'/index.html?__WB_REVISION__=1234abcd'`.
- *
- * @param {string|Request} request The key (without revisioning parameters)
- * to look up in the precache.
- * @return {Promise}
- */
- async matchPrecache(request) {
- const url = request instanceof Request ? request.url : request;
- const cacheKey = this.getCacheKeyForURL(url);
- if (cacheKey) {
- const cache = await self.caches.open(this.strategy.cacheName);
- return cache.match(cacheKey);
+ await handler.doneWaiting();
+ } catch (waitUntilError) {
+ if (waitUntilError instanceof Error) {
+ error = waitUntilError;
}
- return undefined;
}
- /**
- * Returns a function that looks up `url` in the precache (taking into
- * account revision information), and returns the corresponding `Response`.
- *
- * @param {string} url The precached URL which will be used to lookup the
- * `Response`.
- * @return {workbox-routing~handlerCallback}
- */
- createHandlerBoundToURL(url) {
- const cacheKey = this.getCacheKeyForURL(url);
- if (!cacheKey) {
- throw new WorkboxError('non-precached-url', {
- url
- });
- }
- return options => {
- options.request = new Request(url);
- options.params = Object.assign({
- cacheKey
- }, options.params);
- return this.strategy.handle(options);
- };
+ await handler.runCallbacks("handlerDidComplete", {
+ event,
+ request,
+ response,
+ error: error
+ });
+ handler.destroy();
+ if (error) {
+ throw error;
}
}
+ }
+ /**
+ * Classes extending the `Strategy` based class should implement this method,
+ * and leverage the {@link workbox-strategies.StrategyHandler}
+ * arg to perform all fetching and cache logic, which will ensure all relevant
+ * cache, cache options, fetch options and plugins are used (per the current
+ * strategy instance).
+ *
+ * @name _handle
+ * @instance
+ * @abstract
+ * @function
+ * @param {Request} request
+ * @param {workbox-strategies.StrategyHandler} handler
+ * @return {Promise}
+ *
+ * @memberof workbox-strategies.Strategy
+ */
- /*
- Copyright 2019 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- let precacheController;
- /**
- * @return {PrecacheController}
- * @private
- */
- const getOrCreatePrecacheController = () => {
- if (!precacheController) {
- precacheController = new PrecacheController();
- }
- return precacheController;
- };
-
- /*
- Copyright 2018 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * Removes any URL search parameters that should be ignored.
- *
- * @param {URL} urlObject The original URL.
- * @param {Array} ignoreURLParametersMatching RegExps to test against
- * each search parameter name. Matches mean that the search parameter should be
- * ignored.
- * @return {URL} The URL with any ignored search parameters removed.
- *
- * @private
- * @memberof workbox-precaching
- */
- function removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching = []) {
- // Convert the iterable into an array at the start of the loop to make sure
- // deletion doesn't mess up iteration.
- for (const paramName of [...urlObject.searchParams.keys()]) {
- if (ignoreURLParametersMatching.some(regExp => regExp.test(paramName))) {
- urlObject.searchParams.delete(paramName);
- }
- }
- return urlObject;
- }
-
- /*
- Copyright 2019 Google LLC
-
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
- /**
- * Generator function that yields possible variations on the original URL to
- * check, one at a time.
- *
- * @param {string} url
- * @param {Object} options
- *
- * @private
- * @memberof workbox-precaching
- */
- function* generateURLVariations(url, {
- ignoreURLParametersMatching = [/^utm_/, /^fbclid$/],
- directoryIndex = 'index.html',
- cleanURLs = true,
- urlManipulation
- } = {}) {
- const urlObject = new URL(url, location.href);
- urlObject.hash = '';
- yield urlObject.href;
- const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching);
- yield urlWithoutIgnoredParams.href;
- if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith('/')) {
- const directoryURL = new URL(urlWithoutIgnoredParams.href);
- directoryURL.pathname += directoryIndex;
- yield directoryURL.href;
- }
- if (cleanURLs) {
- const cleanURL = new URL(urlWithoutIgnoredParams.href);
- cleanURL.pathname += '.html';
- yield cleanURL.href;
- }
- if (urlManipulation) {
- const additionalURLs = urlManipulation({
- url: urlObject
- });
- for (const urlToAttempt of additionalURLs) {
- yield urlToAttempt.href;
- }
- }
- }
-
- /*
+ /*
Copyright 2020 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
+ /**
+ * A {@link workbox-strategies.Strategy} implementation
+ * specifically designed to work with
+ * {@link workbox-precaching.PrecacheController}
+ * to both cache and fetch precached assets.
+ *
+ * Note: an instance of this class is created automatically when creating a
+ * `PrecacheController`; it's generally not necessary to create this yourself.
+ *
+ * @extends workbox-strategies.Strategy
+ * @memberof workbox-precaching
+ */
+ class PrecacheStrategy extends Strategy {
/**
- * A subclass of {@link workbox-routing.Route} that takes a
- * {@link workbox-precaching.PrecacheController}
- * instance and uses it to match incoming requests and handle fetching
- * responses from the precache.
*
- * @memberof workbox-precaching
- * @extends workbox-routing.Route
+ * @param {Object} [options]
+ * @param {string} [options.cacheName] Cache name to store and retrieve
+ * requests. Defaults to the cache names provided by
+ * {@link workbox-core.cacheNames}.
+ * @param {Array} [options.plugins] {@link https://developers.google.com/web/tools/workbox/guides/using-plugins|Plugins}
+ * to use in conjunction with this caching strategy.
+ * @param {Object} [options.fetchOptions] Values passed along to the
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init}
+ * of all fetch() requests made by this strategy.
+ * @param {Object} [options.matchOptions] The
+ * {@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions|CacheQueryOptions}
+ * for any `cache.match()` or `cache.put()` calls made by this strategy.
+ * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to
+ * get the response from the network if there's a precache miss.
*/
- class PrecacheRoute extends Route {
- /**
- * @param {PrecacheController} precacheController A `PrecacheController`
- * instance used to both match requests and respond to fetch events.
- * @param {Object} [options] Options to control how requests are matched
- * against the list of precached URLs.
- * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will
- * check cache entries for a URLs ending with '/' to see if there is a hit when
- * appending the `directoryIndex` value.
- * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/, /^fbclid$/]] An
- * array of regex's to remove search params when looking for a cache match.
- * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will
- * check the cache for the URL with a `.html` added to the end of the end.
- * @param {workbox-precaching~urlManipulation} [options.urlManipulation]
- * This is a function that should take a URL and return an array of
- * alternative URLs that should be checked for precache matches.
- */
- constructor(precacheController, options) {
- const match = ({
- request
- }) => {
- const urlsToCacheKeys = precacheController.getURLsToCacheKeys();
- for (const possibleURL of generateURLVariations(request.url, options)) {
- const cacheKey = urlsToCacheKeys.get(possibleURL);
- if (cacheKey) {
- const integrity = precacheController.getIntegrityForCacheKey(cacheKey);
- return {
- cacheKey,
- integrity
- };
+ constructor(options = {}) {
+ options.cacheName = cacheNames.getPrecacheName(options.cacheName);
+ super(options);
+ this._fallbackToNetwork = options.fallbackToNetwork === false ? false : true;
+ // Redirected responses cannot be used to satisfy a navigation request, so
+ // any redirected response must be "copied" rather than cloned, so the new
+ // response doesn't contain the `redirected` flag. See:
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1
+ this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin);
+ }
+ /**
+ * @private
+ * @param {Request|string} request A request to run this strategy for.
+ * @param {workbox-strategies.StrategyHandler} handler The event that
+ * triggered the request.
+ * @return {Promise}
+ */
+ async _handle(request, handler) {
+ const response = await handler.cacheMatch(request);
+ if (response) {
+ return response;
+ }
+ // If this is an `install` event for an entry that isn't already cached,
+ // then populate the cache.
+ if (handler.event && handler.event.type === "install") {
+ return await this._handleInstall(request, handler);
+ }
+ // Getting here means something went wrong. An entry that should have been
+ // precached wasn't found in the cache.
+ return await this._handleFetch(request, handler);
+ }
+ async _handleFetch(request, handler) {
+ let response;
+ const params = handler.params || {};
+ // Fall back to the network if we're configured to do so.
+ if (this._fallbackToNetwork) {
+ {
+ logger.warn(
+ `The precached response for ` +
+ `${getFriendlyURL(request.url)} in ${this.cacheName} was not ` +
+ `found. Falling back to the network.`
+ );
+ }
+ const integrityInManifest = params.integrity;
+ const integrityInRequest = request.integrity;
+ const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest;
+ // Do not add integrity if the original request is no-cors
+ // See https://github.com/GoogleChrome/workbox/issues/3096
+ response = await handler.fetch(
+ new Request(request, {
+ integrity: request.mode !== "no-cors" ? integrityInRequest || integrityInManifest : undefined
+ })
+ );
+ // It's only "safe" to repair the cache if we're using SRI to guarantee
+ // that the response matches the precache manifest's expectations,
+ // and there's either a) no integrity property in the incoming request
+ // or b) there is an integrity, and it matches the precache manifest.
+ // See https://github.com/GoogleChrome/workbox/issues/2858
+ // Also if the original request users no-cors we don't use integrity.
+ // See https://github.com/GoogleChrome/workbox/issues/3096
+ if (integrityInManifest && noIntegrityConflict && request.mode !== "no-cors") {
+ this._useDefaultCacheabilityPluginIfNeeded();
+ const wasCached = await handler.cachePut(request, response.clone());
+ {
+ if (wasCached) {
+ logger.log(`A response for ${getFriendlyURL(request.url)} ` + `was used to "repair" the precache.`);
}
}
- {
- logger.debug(`Precaching did not find a match for ` + getFriendlyURL(request.url));
- }
- return;
- };
- super(match, precacheController.strategy);
+ }
+ } else {
+ // This shouldn't normally happen, but there are edge cases:
+ // https://github.com/GoogleChrome/workbox/issues/1441
+ throw new WorkboxError("missing-precache-entry", {
+ cacheName: this.cacheName,
+ url: request.url
+ });
}
+ {
+ const cacheKey = params.cacheKey || (await handler.getCacheKey(request, "read"));
+ // Workbox is going to handle the route.
+ // print the routing details to the console.
+ logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL(request.url));
+ logger.log(
+ `Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}`
+ );
+ logger.groupCollapsed(`View request details here.`);
+ logger.log(request);
+ logger.groupEnd();
+ logger.groupCollapsed(`View response details here.`);
+ logger.log(response);
+ logger.groupEnd();
+ logger.groupEnd();
+ }
+ return response;
+ }
+ async _handleInstall(request, handler) {
+ this._useDefaultCacheabilityPluginIfNeeded();
+ const response = await handler.fetch(request);
+ // Make sure we defer cachePut() until after we know the response
+ // should be cached; see https://github.com/GoogleChrome/workbox/issues/2737
+ const wasCached = await handler.cachePut(request, response.clone());
+ if (!wasCached) {
+ // Throwing here will lead to the `install` handler failing, which
+ // we want to do if *any* of the responses aren't safe to cache.
+ throw new WorkboxError("bad-precaching-response", {
+ url: request.url,
+ status: response.status
+ });
+ }
+ return response;
}
-
- /*
- Copyright 2019 Google LLC
- Use of this source code is governed by an MIT-style
- license that can be found in the LICENSE file or at
- https://opensource.org/licenses/MIT.
- */
/**
- * Add a `fetch` listener to the service worker that will
- * respond to
- * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests}
- * with precached assets.
+ * This method is complex, as there a number of things to account for:
*
- * Requests for assets that aren't precached, the `FetchEvent` will not be
- * responded to, allowing the event to fall through to other `fetch` event
- * listeners.
+ * The `plugins` array can be set at construction, and/or it might be added to
+ * to at any time before the strategy is used.
*
- * @param {Object} [options] See the {@link workbox-precaching.PrecacheRoute}
- * options.
+ * At the time the strategy is used (i.e. during an `install` event), there
+ * needs to be at least one plugin that implements `cacheWillUpdate` in the
+ * array, other than `copyRedirectedCacheableResponsesPlugin`.
*
- * @memberof workbox-precaching
+ * - If this method is called and there are no suitable `cacheWillUpdate`
+ * plugins, we need to add `defaultPrecacheCacheabilityPlugin`.
+ *
+ * - If this method is called and there is exactly one `cacheWillUpdate`, then
+ * we don't have to do anything (this might be a previously added
+ * `defaultPrecacheCacheabilityPlugin`, or it might be a custom plugin).
+ *
+ * - If this method is called and there is more than one `cacheWillUpdate`,
+ * then we need to check if one is `defaultPrecacheCacheabilityPlugin`. If so,
+ * we need to remove it. (This situation is unlikely, but it could happen if
+ * the strategy is used multiple times, the first without a `cacheWillUpdate`,
+ * and then later on after manually adding a custom `cacheWillUpdate`.)
+ *
+ * See https://github.com/GoogleChrome/workbox/issues/2737 for more context.
+ *
+ * @private
*/
- function addRoute(options) {
- const precacheController = getOrCreatePrecacheController();
- const precacheRoute = new PrecacheRoute(precacheController, options);
- registerRoute(precacheRoute);
+ _useDefaultCacheabilityPluginIfNeeded() {
+ let defaultPluginIndex = null;
+ let cacheWillUpdatePluginCount = 0;
+ for (const [index, plugin] of this.plugins.entries()) {
+ // Ignore the copy redirected plugin when determining what to do.
+ if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) {
+ continue;
+ }
+ // Save the default plugin's index, in case it needs to be removed.
+ if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) {
+ defaultPluginIndex = index;
+ }
+ if (plugin.cacheWillUpdate) {
+ cacheWillUpdatePluginCount++;
+ }
+ }
+ if (cacheWillUpdatePluginCount === 0) {
+ this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin);
+ } else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) {
+ // Only remove the default plugin; multiple custom plugins are allowed.
+ this.plugins.splice(defaultPluginIndex, 1);
+ }
+ // Nothing needs to be done if cacheWillUpdatePluginCount is 1
}
+ }
+ PrecacheStrategy.defaultPrecacheCacheabilityPlugin = {
+ async cacheWillUpdate({ response }) {
+ if (!response || response.status >= 400) {
+ return null;
+ }
+ return response;
+ }
+ };
+ PrecacheStrategy.copyRedirectedCacheableResponsesPlugin = {
+ async cacheWillUpdate({ response }) {
+ return response.redirected ? await copyResponse(response) : response;
+ }
+ };
- /*
+ /*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
+ /**
+ * Performs efficient precaching of assets.
+ *
+ * @memberof workbox-precaching
+ */
+ class PrecacheController {
+ /**
+ * Create a new PrecacheController.
+ *
+ * @param {Object} [options]
+ * @param {string} [options.cacheName] The cache to use for precaching.
+ * @param {string} [options.plugins] Plugins to use when precaching as well
+ * as responding to fetch events for precached assets.
+ * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to
+ * get the response from the network if there's a precache miss.
+ */
+ constructor({ cacheName, plugins = [], fallbackToNetwork = true } = {}) {
+ this._urlsToCacheKeys = new Map();
+ this._urlsToCacheModes = new Map();
+ this._cacheKeysToIntegrities = new Map();
+ this._strategy = new PrecacheStrategy({
+ cacheName: cacheNames.getPrecacheName(cacheName),
+ plugins: [
+ ...plugins,
+ new PrecacheCacheKeyPlugin({
+ precacheController: this
+ })
+ ],
+ fallbackToNetwork
+ });
+ // Bind the install and activate methods to the instance.
+ this.install = this.install.bind(this);
+ this.activate = this.activate.bind(this);
+ }
+ /**
+ * @type {workbox-precaching.PrecacheStrategy} The strategy created by this controller and
+ * used to cache assets and respond to fetch events.
+ */
+ get strategy() {
+ return this._strategy;
+ }
/**
* Adds items to the precache list, removing any duplicates and
* stores the files in the
@@ -3140,252 +2742,694 @@ define(['exports'], (function (exports) { 'use strict';
*
* This method can be called multiple times.
*
- * Please note: This method **will not** serve any of the cached files for you.
- * It only precaches files. To respond to a network request you call
- * {@link workbox-precaching.addRoute}.
- *
- * If you have a single array of files to precache, you can just call
- * {@link workbox-precaching.precacheAndRoute}.
- *
* @param {Array} [entries=[]] Array of entries to precache.
- *
- * @memberof workbox-precaching
*/
- function precache(entries) {
- const precacheController = getOrCreatePrecacheController();
- precacheController.precache(entries);
+ precache(entries) {
+ this.addToCacheList(entries);
+ if (!this._installAndActiveListenersAdded) {
+ self.addEventListener("install", this.install);
+ self.addEventListener("activate", this.activate);
+ this._installAndActiveListenersAdded = true;
+ }
}
+ /**
+ * This method will add items to the precache list, removing duplicates
+ * and ensuring the information is valid.
+ *
+ * @param {Array} entries
+ * Array of entries to precache.
+ */
+ addToCacheList(entries) {
+ {
+ finalAssertExports.isArray(entries, {
+ moduleName: "workbox-precaching",
+ className: "PrecacheController",
+ funcName: "addToCacheList",
+ paramName: "entries"
+ });
+ }
+ const urlsToWarnAbout = [];
+ for (const entry of entries) {
+ // See https://github.com/GoogleChrome/workbox/issues/2259
+ if (typeof entry === "string") {
+ urlsToWarnAbout.push(entry);
+ } else if (entry && entry.revision === undefined) {
+ urlsToWarnAbout.push(entry.url);
+ }
+ const { cacheKey, url } = createCacheKey(entry);
+ const cacheMode = typeof entry !== "string" && entry.revision ? "reload" : "default";
+ if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) {
+ throw new WorkboxError("add-to-cache-list-conflicting-entries", {
+ firstEntry: this._urlsToCacheKeys.get(url),
+ secondEntry: cacheKey
+ });
+ }
+ if (typeof entry !== "string" && entry.integrity) {
+ if (
+ this._cacheKeysToIntegrities.has(cacheKey) &&
+ this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity
+ ) {
+ throw new WorkboxError("add-to-cache-list-conflicting-integrities", {
+ url
+ });
+ }
+ this._cacheKeysToIntegrities.set(cacheKey, entry.integrity);
+ }
+ this._urlsToCacheKeys.set(url, cacheKey);
+ this._urlsToCacheModes.set(url, cacheMode);
+ if (urlsToWarnAbout.length > 0) {
+ const warningMessage =
+ `Workbox is precaching URLs without revision ` +
+ `info: ${urlsToWarnAbout.join(", ")}\nThis is generally NOT safe. ` +
+ `Learn more at https://bit.ly/wb-precache`;
+ {
+ logger.warn(warningMessage);
+ }
+ }
+ }
+ }
+ /**
+ * Precaches new and updated assets. Call this method from the service worker
+ * install event.
+ *
+ * Note: this method calls `event.waitUntil()` for you, so you do not need
+ * to call it yourself in your event handlers.
+ *
+ * @param {ExtendableEvent} event
+ * @return {Promise}
+ */
+ install(event) {
+ // waitUntil returns Promise
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return waitUntil(event, async () => {
+ const installReportPlugin = new PrecacheInstallReportPlugin();
+ this.strategy.plugins.push(installReportPlugin);
+ // Cache entries one at a time.
+ // See https://github.com/GoogleChrome/workbox/issues/2528
+ for (const [url, cacheKey] of this._urlsToCacheKeys) {
+ const integrity = this._cacheKeysToIntegrities.get(cacheKey);
+ const cacheMode = this._urlsToCacheModes.get(url);
+ const request = new Request(url, {
+ integrity,
+ cache: cacheMode,
+ credentials: "same-origin"
+ });
+ await Promise.all(
+ this.strategy.handleAll({
+ params: {
+ cacheKey
+ },
+ request,
+ event
+ })
+ );
+ }
+ const { updatedURLs, notUpdatedURLs } = installReportPlugin;
+ {
+ printInstallDetails(updatedURLs, notUpdatedURLs);
+ }
+ return {
+ updatedURLs,
+ notUpdatedURLs
+ };
+ });
+ }
+ /**
+ * Deletes assets that are no longer present in the current precache manifest.
+ * Call this method from the service worker activate event.
+ *
+ * Note: this method calls `event.waitUntil()` for you, so you do not need
+ * to call it yourself in your event handlers.
+ *
+ * @param {ExtendableEvent} event
+ * @return {Promise}
+ */
+ activate(event) {
+ // waitUntil returns Promise
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return waitUntil(event, async () => {
+ const cache = await self.caches.open(this.strategy.cacheName);
+ const currentlyCachedRequests = await cache.keys();
+ const expectedCacheKeys = new Set(this._urlsToCacheKeys.values());
+ const deletedURLs = [];
+ for (const request of currentlyCachedRequests) {
+ if (!expectedCacheKeys.has(request.url)) {
+ await cache.delete(request);
+ deletedURLs.push(request.url);
+ }
+ }
+ {
+ printCleanupDetails(deletedURLs);
+ }
+ return {
+ deletedURLs
+ };
+ });
+ }
+ /**
+ * Returns a mapping of a precached URL to the corresponding cache key, taking
+ * into account the revision information for the URL.
+ *
+ * @return {Map} A URL to cache key mapping.
+ */
+ getURLsToCacheKeys() {
+ return this._urlsToCacheKeys;
+ }
+ /**
+ * Returns a list of all the URLs that have been precached by the current
+ * service worker.
+ *
+ * @return {Array} The precached URLs.
+ */
+ getCachedURLs() {
+ return [...this._urlsToCacheKeys.keys()];
+ }
+ /**
+ * Returns the cache key used for storing a given URL. If that URL is
+ * unversioned, like `/index.html', then the cache key will be the original
+ * URL with a search parameter appended to it.
+ *
+ * @param {string} url A URL whose cache key you want to look up.
+ * @return {string} The versioned URL that corresponds to a cache key
+ * for the original URL, or undefined if that URL isn't precached.
+ */
+ getCacheKeyForURL(url) {
+ const urlObject = new URL(url, location.href);
+ return this._urlsToCacheKeys.get(urlObject.href);
+ }
+ /**
+ * @param {string} url A cache key whose SRI you want to look up.
+ * @return {string} The subresource integrity associated with the cache key,
+ * or undefined if it's not set.
+ */
+ getIntegrityForCacheKey(cacheKey) {
+ return this._cacheKeysToIntegrities.get(cacheKey);
+ }
+ /**
+ * This acts as a drop-in replacement for
+ * [`cache.match()`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match)
+ * with the following differences:
+ *
+ * - It knows what the name of the precache is, and only checks in that cache.
+ * - It allows you to pass in an "original" URL without versioning parameters,
+ * and it will automatically look up the correct cache key for the currently
+ * active revision of that URL.
+ *
+ * E.g., `matchPrecache('index.html')` will find the correct precached
+ * response for the currently active service worker, even if the actual cache
+ * key is `'/index.html?__WB_REVISION__=1234abcd'`.
+ *
+ * @param {string|Request} request The key (without revisioning parameters)
+ * to look up in the precache.
+ * @return {Promise}
+ */
+ async matchPrecache(request) {
+ const url = request instanceof Request ? request.url : request;
+ const cacheKey = this.getCacheKeyForURL(url);
+ if (cacheKey) {
+ const cache = await self.caches.open(this.strategy.cacheName);
+ return cache.match(cacheKey);
+ }
+ return undefined;
+ }
+ /**
+ * Returns a function that looks up `url` in the precache (taking into
+ * account revision information), and returns the corresponding `Response`.
+ *
+ * @param {string} url The precached URL which will be used to lookup the
+ * `Response`.
+ * @return {workbox-routing~handlerCallback}
+ */
+ createHandlerBoundToURL(url) {
+ const cacheKey = this.getCacheKeyForURL(url);
+ if (!cacheKey) {
+ throw new WorkboxError("non-precached-url", {
+ url
+ });
+ }
+ return (options) => {
+ options.request = new Request(url);
+ options.params = Object.assign(
+ {
+ cacheKey
+ },
+ options.params
+ );
+ return this.strategy.handle(options);
+ };
+ }
+ }
- /*
+ /*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
- /**
- * This method will add entries to the precache list and add a route to
- * respond to fetch events.
- *
- * This is a convenience method that will call
- * {@link workbox-precaching.precache} and
- * {@link workbox-precaching.addRoute} in a single call.
- *
- * @param {Array} entries Array of entries to precache.
- * @param {Object} [options] See the
- * {@link workbox-precaching.PrecacheRoute} options.
- *
- * @memberof workbox-precaching
- */
- function precacheAndRoute(entries, options) {
- precache(entries);
- addRoute(options);
+ let precacheController;
+ /**
+ * @return {PrecacheController}
+ * @private
+ */
+ const getOrCreatePrecacheController = () => {
+ if (!precacheController) {
+ precacheController = new PrecacheController();
}
+ return precacheController;
+ };
- /*
+ /*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
- const SUBSTRING_TO_FIND = '-precache-';
- /**
- * Cleans up incompatible precaches that were created by older versions of
- * Workbox, by a service worker registered under the current scope.
- *
- * This is meant to be called as part of the `activate` event.
- *
- * This should be safe to use as long as you don't include `substringToFind`
- * (defaulting to `-precache-`) in your non-precache cache names.
- *
- * @param {string} currentPrecacheName The cache name currently in use for
- * precaching. This cache won't be deleted.
- * @param {string} [substringToFind='-precache-'] Cache names which include this
- * substring will be deleted (excluding `currentPrecacheName`).
- * @return {Array} A list of all the cache names that were deleted.
- *
- * @private
- * @memberof workbox-precaching
- */
- const deleteOutdatedCaches = async (currentPrecacheName, substringToFind = SUBSTRING_TO_FIND) => {
- const cacheNames = await self.caches.keys();
- const cacheNamesToDelete = cacheNames.filter(cacheName => {
- return cacheName.includes(substringToFind) && cacheName.includes(self.registration.scope) && cacheName !== currentPrecacheName;
- });
- await Promise.all(cacheNamesToDelete.map(cacheName => self.caches.delete(cacheName)));
- return cacheNamesToDelete;
- };
+ /**
+ * Removes any URL search parameters that should be ignored.
+ *
+ * @param {URL} urlObject The original URL.
+ * @param {Array} ignoreURLParametersMatching RegExps to test against
+ * each search parameter name. Matches mean that the search parameter should be
+ * ignored.
+ * @return {URL} The URL with any ignored search parameters removed.
+ *
+ * @private
+ * @memberof workbox-precaching
+ */
+ function removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching = []) {
+ // Convert the iterable into an array at the start of the loop to make sure
+ // deletion doesn't mess up iteration.
+ for (const paramName of [...urlObject.searchParams.keys()]) {
+ if (ignoreURLParametersMatching.some((regExp) => regExp.test(paramName))) {
+ urlObject.searchParams.delete(paramName);
+ }
+ }
+ return urlObject;
+ }
- /*
+ /*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
+ /**
+ * Generator function that yields possible variations on the original URL to
+ * check, one at a time.
+ *
+ * @param {string} url
+ * @param {Object} options
+ *
+ * @private
+ * @memberof workbox-precaching
+ */
+ function* generateURLVariations(
+ url,
+ {
+ ignoreURLParametersMatching = [/^utm_/, /^fbclid$/],
+ directoryIndex = "index.html",
+ cleanURLs = true,
+ urlManipulation
+ } = {}
+ ) {
+ const urlObject = new URL(url, location.href);
+ urlObject.hash = "";
+ yield urlObject.href;
+ const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching);
+ yield urlWithoutIgnoredParams.href;
+ if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith("/")) {
+ const directoryURL = new URL(urlWithoutIgnoredParams.href);
+ directoryURL.pathname += directoryIndex;
+ yield directoryURL.href;
+ }
+ if (cleanURLs) {
+ const cleanURL = new URL(urlWithoutIgnoredParams.href);
+ cleanURL.pathname += ".html";
+ yield cleanURL.href;
+ }
+ if (urlManipulation) {
+ const additionalURLs = urlManipulation({
+ url: urlObject
+ });
+ for (const urlToAttempt of additionalURLs) {
+ yield urlToAttempt.href;
+ }
+ }
+ }
+
+ /*
+ Copyright 2020 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * A subclass of {@link workbox-routing.Route} that takes a
+ * {@link workbox-precaching.PrecacheController}
+ * instance and uses it to match incoming requests and handle fetching
+ * responses from the precache.
+ *
+ * @memberof workbox-precaching
+ * @extends workbox-routing.Route
+ */
+ class PrecacheRoute extends Route {
/**
- * Adds an `activate` event listener which will clean up incompatible
- * precaches that were created by older versions of Workbox.
- *
- * @memberof workbox-precaching
+ * @param {PrecacheController} precacheController A `PrecacheController`
+ * instance used to both match requests and respond to fetch events.
+ * @param {Object} [options] Options to control how requests are matched
+ * against the list of precached URLs.
+ * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will
+ * check cache entries for a URLs ending with '/' to see if there is a hit when
+ * appending the `directoryIndex` value.
+ * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/, /^fbclid$/]] An
+ * array of regex's to remove search params when looking for a cache match.
+ * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will
+ * check the cache for the URL with a `.html` added to the end of the end.
+ * @param {workbox-precaching~urlManipulation} [options.urlManipulation]
+ * This is a function that should take a URL and return an array of
+ * alternative URLs that should be checked for precache matches.
*/
- function cleanupOutdatedCaches() {
- // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
- self.addEventListener('activate', event => {
- const cacheName = cacheNames.getPrecacheName();
- event.waitUntil(deleteOutdatedCaches(cacheName).then(cachesDeleted => {
+ constructor(precacheController, options) {
+ const match = ({ request }) => {
+ const urlsToCacheKeys = precacheController.getURLsToCacheKeys();
+ for (const possibleURL of generateURLVariations(request.url, options)) {
+ const cacheKey = urlsToCacheKeys.get(possibleURL);
+ if (cacheKey) {
+ const integrity = precacheController.getIntegrityForCacheKey(cacheKey);
+ return {
+ cacheKey,
+ integrity
+ };
+ }
+ }
+ {
+ logger.debug(`Precaching did not find a match for ` + getFriendlyURL(request.url));
+ }
+ return;
+ };
+ super(match, precacheController.strategy);
+ }
+ }
+
+ /*
+ Copyright 2019 Google LLC
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Add a `fetch` listener to the service worker that will
+ * respond to
+ * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests}
+ * with precached assets.
+ *
+ * Requests for assets that aren't precached, the `FetchEvent` will not be
+ * responded to, allowing the event to fall through to other `fetch` event
+ * listeners.
+ *
+ * @param {Object} [options] See the {@link workbox-precaching.PrecacheRoute}
+ * options.
+ *
+ * @memberof workbox-precaching
+ */
+ function addRoute(options) {
+ const precacheController = getOrCreatePrecacheController();
+ const precacheRoute = new PrecacheRoute(precacheController, options);
+ registerRoute(precacheRoute);
+ }
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Adds items to the precache list, removing any duplicates and
+ * stores the files in the
+ * {@link workbox-core.cacheNames|"precache cache"} when the service
+ * worker installs.
+ *
+ * This method can be called multiple times.
+ *
+ * Please note: This method **will not** serve any of the cached files for you.
+ * It only precaches files. To respond to a network request you call
+ * {@link workbox-precaching.addRoute}.
+ *
+ * If you have a single array of files to precache, you can just call
+ * {@link workbox-precaching.precacheAndRoute}.
+ *
+ * @param {Array} [entries=[]] Array of entries to precache.
+ *
+ * @memberof workbox-precaching
+ */
+ function precache(entries) {
+ const precacheController = getOrCreatePrecacheController();
+ precacheController.precache(entries);
+ }
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * This method will add entries to the precache list and add a route to
+ * respond to fetch events.
+ *
+ * This is a convenience method that will call
+ * {@link workbox-precaching.precache} and
+ * {@link workbox-precaching.addRoute} in a single call.
+ *
+ * @param {Array} entries Array of entries to precache.
+ * @param {Object} [options] See the
+ * {@link workbox-precaching.PrecacheRoute} options.
+ *
+ * @memberof workbox-precaching
+ */
+ function precacheAndRoute(entries, options) {
+ precache(entries);
+ addRoute(options);
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const SUBSTRING_TO_FIND = "-precache-";
+ /**
+ * Cleans up incompatible precaches that were created by older versions of
+ * Workbox, by a service worker registered under the current scope.
+ *
+ * This is meant to be called as part of the `activate` event.
+ *
+ * This should be safe to use as long as you don't include `substringToFind`
+ * (defaulting to `-precache-`) in your non-precache cache names.
+ *
+ * @param {string} currentPrecacheName The cache name currently in use for
+ * precaching. This cache won't be deleted.
+ * @param {string} [substringToFind='-precache-'] Cache names which include this
+ * substring will be deleted (excluding `currentPrecacheName`).
+ * @return {Array} A list of all the cache names that were deleted.
+ *
+ * @private
+ * @memberof workbox-precaching
+ */
+ const deleteOutdatedCaches = async (currentPrecacheName, substringToFind = SUBSTRING_TO_FIND) => {
+ const cacheNames = await self.caches.keys();
+ const cacheNamesToDelete = cacheNames.filter((cacheName) => {
+ return (
+ cacheName.includes(substringToFind) &&
+ cacheName.includes(self.registration.scope) &&
+ cacheName !== currentPrecacheName
+ );
+ });
+ await Promise.all(cacheNamesToDelete.map((cacheName) => self.caches.delete(cacheName)));
+ return cacheNamesToDelete;
+ };
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Adds an `activate` event listener which will clean up incompatible
+ * precaches that were created by older versions of Workbox.
+ *
+ * @memberof workbox-precaching
+ */
+ function cleanupOutdatedCaches() {
+ // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
+ self.addEventListener("activate", (event) => {
+ const cacheName = cacheNames.getPrecacheName();
+ event.waitUntil(
+ deleteOutdatedCaches(cacheName).then((cachesDeleted) => {
{
if (cachesDeleted.length > 0) {
logger.log(`The following out-of-date precaches were cleaned up ` + `automatically:`, cachesDeleted);
}
}
- }));
- });
- }
+ })
+ );
+ });
+ }
- /*
+ /*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
+ /**
+ * NavigationRoute makes it easy to create a
+ * {@link workbox-routing.Route} that matches for browser
+ * [navigation requests]{@link https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests}.
+ *
+ * It will only match incoming Requests whose
+ * {@link https://fetch.spec.whatwg.org/#concept-request-mode|mode}
+ * is set to `navigate`.
+ *
+ * You can optionally only apply this route to a subset of navigation requests
+ * by using one or both of the `denylist` and `allowlist` parameters.
+ *
+ * @memberof workbox-routing
+ * @extends workbox-routing.Route
+ */
+ class NavigationRoute extends Route {
/**
- * NavigationRoute makes it easy to create a
- * {@link workbox-routing.Route} that matches for browser
- * [navigation requests]{@link https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests}.
+ * If both `denylist` and `allowlist` are provided, the `denylist` will
+ * take precedence and the request will not match this route.
*
- * It will only match incoming Requests whose
- * {@link https://fetch.spec.whatwg.org/#concept-request-mode|mode}
- * is set to `navigate`.
+ * The regular expressions in `allowlist` and `denylist`
+ * are matched against the concatenated
+ * [`pathname`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname}
+ * and [`search`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search}
+ * portions of the requested URL.
*
- * You can optionally only apply this route to a subset of navigation requests
- * by using one or both of the `denylist` and `allowlist` parameters.
+ * *Note*: These RegExps may be evaluated against every destination URL during
+ * a navigation. Avoid using
+ * [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077),
+ * or else your users may see delays when navigating your site.
*
- * @memberof workbox-routing
- * @extends workbox-routing.Route
+ * @param {workbox-routing~handlerCallback} handler A callback
+ * function that returns a Promise resulting in a Response.
+ * @param {Object} options
+ * @param {Array} [options.denylist] If any of these patterns match,
+ * the route will not handle the request (even if a allowlist RegExp matches).
+ * @param {Array} [options.allowlist=[/./]] If any of these patterns
+ * match the URL's pathname and search parameter, the route will handle the
+ * request (assuming the denylist doesn't match).
*/
- class NavigationRoute extends Route {
- /**
- * If both `denylist` and `allowlist` are provided, the `denylist` will
- * take precedence and the request will not match this route.
- *
- * The regular expressions in `allowlist` and `denylist`
- * are matched against the concatenated
- * [`pathname`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname}
- * and [`search`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search}
- * portions of the requested URL.
- *
- * *Note*: These RegExps may be evaluated against every destination URL during
- * a navigation. Avoid using
- * [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077),
- * or else your users may see delays when navigating your site.
- *
- * @param {workbox-routing~handlerCallback} handler A callback
- * function that returns a Promise resulting in a Response.
- * @param {Object} options
- * @param {Array} [options.denylist] If any of these patterns match,
- * the route will not handle the request (even if a allowlist RegExp matches).
- * @param {Array} [options.allowlist=[/./]] If any of these patterns
- * match the URL's pathname and search parameter, the route will handle the
- * request (assuming the denylist doesn't match).
- */
- constructor(handler, {
- allowlist = [/./],
- denylist = []
- } = {}) {
- {
- finalAssertExports.isArrayOfClass(allowlist, RegExp, {
- moduleName: 'workbox-routing',
- className: 'NavigationRoute',
- funcName: 'constructor',
- paramName: 'options.allowlist'
- });
- finalAssertExports.isArrayOfClass(denylist, RegExp, {
- moduleName: 'workbox-routing',
- className: 'NavigationRoute',
- funcName: 'constructor',
- paramName: 'options.denylist'
- });
- }
- super(options => this._match(options), handler);
- this._allowlist = allowlist;
- this._denylist = denylist;
+ constructor(handler, { allowlist = [/./], denylist = [] } = {}) {
+ {
+ finalAssertExports.isArrayOfClass(allowlist, RegExp, {
+ moduleName: "workbox-routing",
+ className: "NavigationRoute",
+ funcName: "constructor",
+ paramName: "options.allowlist"
+ });
+ finalAssertExports.isArrayOfClass(denylist, RegExp, {
+ moduleName: "workbox-routing",
+ className: "NavigationRoute",
+ funcName: "constructor",
+ paramName: "options.denylist"
+ });
}
- /**
- * Routes match handler.
- *
- * @param {Object} options
- * @param {URL} options.url
- * @param {Request} options.request
- * @return {boolean}
- *
- * @private
- */
- _match({
- url,
- request
- }) {
- if (request && request.mode !== 'navigate') {
- return false;
- }
- const pathnameAndSearch = url.pathname + url.search;
- for (const regExp of this._denylist) {
- if (regExp.test(pathnameAndSearch)) {
- {
- logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL matches this denylist pattern: ` + `${regExp.toString()}`);
- }
- return false;
- }
- }
- if (this._allowlist.some(regExp => regExp.test(pathnameAndSearch))) {
- {
- logger.debug(`The navigation route ${pathnameAndSearch} ` + `is being used.`);
- }
- return true;
- }
- {
- logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL being navigated to doesn't ` + `match the allowlist.`);
- }
+ super((options) => this._match(options), handler);
+ this._allowlist = allowlist;
+ this._denylist = denylist;
+ }
+ /**
+ * Routes match handler.
+ *
+ * @param {Object} options
+ * @param {URL} options.url
+ * @param {Request} options.request
+ * @return {boolean}
+ *
+ * @private
+ */
+ _match({ url, request }) {
+ if (request && request.mode !== "navigate") {
return false;
}
+ const pathnameAndSearch = url.pathname + url.search;
+ for (const regExp of this._denylist) {
+ if (regExp.test(pathnameAndSearch)) {
+ {
+ logger.log(
+ `The navigation route ${pathnameAndSearch} is not ` +
+ `being used, since the URL matches this denylist pattern: ` +
+ `${regExp.toString()}`
+ );
+ }
+ return false;
+ }
+ }
+ if (this._allowlist.some((regExp) => regExp.test(pathnameAndSearch))) {
+ {
+ logger.debug(`The navigation route ${pathnameAndSearch} ` + `is being used.`);
+ }
+ return true;
+ }
+ {
+ logger.log(
+ `The navigation route ${pathnameAndSearch} is not ` +
+ `being used, since the URL being navigated to doesn't ` +
+ `match the allowlist.`
+ );
+ }
+ return false;
}
+ }
- /*
+ /*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
- /**
- * Helper function that calls
- * {@link PrecacheController#createHandlerBoundToURL} on the default
- * {@link PrecacheController} instance.
- *
- * If you are creating your own {@link PrecacheController}, then call the
- * {@link PrecacheController#createHandlerBoundToURL} on that instance,
- * instead of using this function.
- *
- * @param {string} url The precached URL which will be used to lookup the
- * `Response`.
- * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the
- * response from the network if there's a precache miss.
- * @return {workbox-routing~handlerCallback}
- *
- * @memberof workbox-precaching
- */
- function createHandlerBoundToURL(url) {
- const precacheController = getOrCreatePrecacheController();
- return precacheController.createHandlerBoundToURL(url);
- }
+ /**
+ * Helper function that calls
+ * {@link PrecacheController#createHandlerBoundToURL} on the default
+ * {@link PrecacheController} instance.
+ *
+ * If you are creating your own {@link PrecacheController}, then call the
+ * {@link PrecacheController#createHandlerBoundToURL} on that instance,
+ * instead of using this function.
+ *
+ * @param {string} url The precached URL which will be used to lookup the
+ * `Response`.
+ * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the
+ * response from the network if there's a precache miss.
+ * @return {workbox-routing~handlerCallback}
+ *
+ * @memberof workbox-precaching
+ */
+ function createHandlerBoundToURL(url) {
+ const precacheController = getOrCreatePrecacheController();
+ return precacheController.createHandlerBoundToURL(url);
+ }
- exports.NavigationRoute = NavigationRoute;
- exports.cleanupOutdatedCaches = cleanupOutdatedCaches;
- exports.clientsClaim = clientsClaim;
- exports.createHandlerBoundToURL = createHandlerBoundToURL;
- exports.precacheAndRoute = precacheAndRoute;
- exports.registerRoute = registerRoute;
-
-}));
+ exports.NavigationRoute = NavigationRoute;
+ exports.cleanupOutdatedCaches = cleanupOutdatedCaches;
+ exports.clientsClaim = clientsClaim;
+ exports.createHandlerBoundToURL = createHandlerBoundToURL;
+ exports.precacheAndRoute = precacheAndRoute;
+ exports.registerRoute = registerRoute;
+});
diff --git a/client/src/App/App.container.jsx b/client/src/App/App.container.jsx
index 13c751c58..e4df37c61 100644
--- a/client/src/App/App.container.jsx
+++ b/client/src/App/App.container.jsx
@@ -1,59 +1,58 @@
-import {ApolloProvider} from "@apollo/client";
-import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react';
-import {ConfigProvider} from "antd";
+import { ApolloProvider } from "@apollo/client";
+import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
+import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day";
-import 'dayjs/locale/en';
+import "dayjs/locale/en";
import React from "react";
-import {useTranslation} from "react-i18next";
+import { useTranslation } from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
import themeProvider from "./themeProvider";
-import { Userpilot } from 'userpilot'
+import { Userpilot } from "userpilot";
// Initialize Userpilot
-if(import.meta.env.DEV){
- Userpilot.initialize('NX-69145f08');
+if (import.meta.env.DEV) {
+ Userpilot.initialize("NX-69145f08");
}
dayjs.locale("en");
const config = {
- core: {
- authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
- key: "anon",
- },
+ core: {
+ authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
+ key: "anon"
+ }
};
export const factory = SplitSdk(config);
-
function AppContainer() {
- const {t} = useTranslation();
+ const { t } = useTranslation();
- return (
-
-
-
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+
+
+
+ );
}
export default Sentry.withProfiler(AppContainer);
diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx
index 2f4583c67..50bde585a 100644
--- a/client/src/App/App.jsx
+++ b/client/src/App/App.jsx
@@ -17,245 +17,225 @@ import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
-import {
- selectBodyshop,
- selectCurrentEula,
- selectCurrentUser,
-} from "../redux/user/user.selectors";
+import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr";
-import { ProductFruits } from 'react-product-fruits';
+import { ProductFruits } from "react-product-fruits";
-const ResetPassword = lazy(() =>
- import("../pages/reset-password/reset-password.component")
-);
+const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
-const MobilePaymentContainer = lazy(() =>
- import("../pages/mobile-payment/mobile-payment.container")
-);
+const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
const mapStateToProps = createStructuredSelector({
- currentUser: selectCurrentUser,
- online: selectOnline,
- bodyshop: selectBodyshop,
- currentEula: selectCurrentEula,
+ currentUser: selectCurrentUser,
+ online: selectOnline,
+ bodyshop: selectBodyshop,
+ currentEula: selectCurrentEula
});
const mapDispatchToProps = (dispatch) => ({
- checkUserSession: () => dispatch(checkUserSession()),
- setOnline: (isOnline) => dispatch(setOnline(isOnline)),
+ checkUserSession: () => dispatch(checkUserSession()),
+ setOnline: (isOnline) => dispatch(setOnline(isOnline))
});
-export function App({
- bodyshop,
- checkUserSession,
- currentUser,
- online,
- setOnline,
- currentEula,
-}) {
- const client = useSplitClient().client;
- const [listenersAdded, setListenersAdded] = useState(false);
- const { t } = useTranslation();
+export function App({ bodyshop, checkUserSession, currentUser, online, setOnline, currentEula }) {
+ const client = useSplitClient().client;
+ const [listenersAdded, setListenersAdded] = useState(false);
+ const { t } = useTranslation();
- useEffect(() => {
- if (!navigator.onLine) {
- setOnline(false);
- }
-
- checkUserSession();
- }, [checkUserSession, setOnline]);
-
- //const b = Grid.useBreakpoint();
- // console.log("Breakpoints:", b);
-
- // Associate event listeners, memoize to prevent multiple listeners being added
- useEffect(() => {
- const offlineListener = (e) => {
- setOnline(false);
- };
-
- const onlineListener = (e) => {
- setOnline(true);
- };
-
- if (!listenersAdded) {
- console.log("Added events for offline and online");
- window.addEventListener("offline", offlineListener);
- window.addEventListener("online", onlineListener);
- setListenersAdded(true);
- }
-
- return () => {
- window.removeEventListener("offline", offlineListener);
- window.removeEventListener("online", onlineListener);
- };
- }, [setOnline, listenersAdded]);
-
- useEffect(() => {
- if (currentUser.authorized && bodyshop) {
- client.setAttribute("imexshopid", bodyshop.imexshopid);
-
- if (
- client.getTreatment("LogRocket_Tracking") === "on" ||
- window.location.hostname ===
- InstanceRenderMgr({
- imex: "beta.imex.online",
- rome: "beta.romeonline.io",
- })
- ) {
- console.log("LR Start");
- LogRocket.init(
- InstanceRenderMgr({
- imex: "gvfvfw/bodyshopapp",
- rome: "rome-online/rome-online",
- promanager: "", //TODO:AIO Add in log rocket for promanager instances.
- })
- );
- }
- }
- }, [bodyshop, client, currentUser.authorized]);
-
- if (currentUser.authorized === null) {
- return ;
+ useEffect(() => {
+ if (!navigator.onLine) {
+ setOnline(false);
}
- handleBeta();
+ checkUserSession();
+ }, [checkUserSession, setOnline]);
- if (!online)
- return (
- {
- window.location.reload();
- }}
- >
- {t("general.actions.refresh")}
-
- }
- />
+ //const b = Grid.useBreakpoint();
+ // console.log("Breakpoints:", b);
+
+ // Associate event listeners, memoize to prevent multiple listeners being added
+ useEffect(() => {
+ const offlineListener = (e) => {
+ setOnline(false);
+ };
+
+ const onlineListener = (e) => {
+ setOnline(true);
+ };
+
+ if (!listenersAdded) {
+ console.log("Added events for offline and online");
+ window.addEventListener("offline", offlineListener);
+ window.addEventListener("online", onlineListener);
+ setListenersAdded(true);
+ }
+
+ return () => {
+ window.removeEventListener("offline", offlineListener);
+ window.removeEventListener("online", onlineListener);
+ };
+ }, [setOnline, listenersAdded]);
+
+ useEffect(() => {
+ if (currentUser.authorized && bodyshop) {
+ client.setAttribute("imexshopid", bodyshop.imexshopid);
+
+ if (
+ client.getTreatment("LogRocket_Tracking") === "on" ||
+ window.location.hostname ===
+ InstanceRenderMgr({
+ imex: "beta.imex.online",
+ rome: "beta.romeonline.io"
+ })
+ ) {
+ console.log("LR Start");
+ LogRocket.init(
+ InstanceRenderMgr({
+ imex: "gvfvfw/bodyshopapp",
+ rome: "rome-online/rome-online",
+ promanager: "" //TODO:AIO Add in log rocket for promanager instances.
+ })
);
-
- if (currentEula && !currentUser.eulaIsAccepted) {
- return ;
+ }
}
+ }, [bodyshop, client, currentUser.authorized]);
- // Any route that is not assigned and matched will default to the Landing Page component
+ if (currentUser.authorized === null) {
+ return ;
+ }
+
+ handleBeta();
+
+ if (!online)
return (
-
-
- }
- >
-
-
-
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- >
- } />
-
-
-
-
- }
- >
- } />
-
-
- }
- >
- } />
-
-
-
+ {
+ window.location.reload();
+ }}
+ >
+ {t("general.actions.refresh")}
+
+ }
+ />
);
+
+ if (currentEula && !currentUser.eulaIsAccepted) {
+ return ;
+ }
+
+ // Any route that is not assigned and matched will default to the Landing Page component
+ return (
+
+ }
+ >
+
+
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ >
+ } />
+
+
+
+
+ }
+ >
+ } />
+
+ }>
+ } />
+
+
+
+ );
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss
index 047cf2541..dd79912c1 100644
--- a/client/src/App/App.styles.scss
+++ b/client/src/App/App.styles.scss
@@ -154,7 +154,6 @@
font-style: italic;
}
-
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4;
}
diff --git a/client/src/App/themeProvider.js b/client/src/App/themeProvider.js
index f8947ccc0..18bcaaa1c 100644
--- a/client/src/App/themeProvider.js
+++ b/client/src/App/themeProvider.js
@@ -1,8 +1,8 @@
-import {defaultsDeep} from "lodash";
-import {theme} from "antd";
-import InstanceRenderMgr from '../utils/instanceRenderMgr'
+import { defaultsDeep } from "lodash";
+import { theme } from "antd";
+import InstanceRenderMgr from "../utils/instanceRenderMgr";
-const {defaultAlgorithm, darkAlgorithm} = theme;
+const { defaultAlgorithm, darkAlgorithm } = theme;
let isDarkMode = false;
@@ -13,28 +13,28 @@ let isDarkMode = false;
const defaultTheme = {
components: {
Table: {
- rowHoverBg: '#e7f3ff',
- rowSelectedBg: '#e6f7ff',
- headerSortHoverBg: 'transparent',
+ rowHoverBg: "#e7f3ff",
+ rowSelectedBg: "#e6f7ff",
+ headerSortHoverBg: "transparent"
},
Menu: {
- darkItemHoverBg: '#1890ff',
- itemHoverBg: '#1890ff',
- horizontalItemHoverBg: '#1890ff',
- },
+ darkItemHoverBg: "#1890ff",
+ itemHoverBg: "#1890ff",
+ horizontalItemHoverBg: "#1890ff"
+ }
},
token: {
- colorPrimary: InstanceRenderMgr({
- imex: '#1890ff',
- rome: '#326ade',
- promanager:"#1d69a6"
+ colorPrimary: InstanceRenderMgr({
+ imex: "#1890ff",
+ rome: "#326ade",
+ promanager: "#1d69a6"
}),
colorInfo: InstanceRenderMgr({
- imex: '#1890ff',
- rome: '#326ade',
- promanager:"#1d69a6"
- }),
- },
+ imex: "#1890ff",
+ rome: "#326ade",
+ promanager: "#1d69a6"
+ })
+ }
};
/**
@@ -42,16 +42,16 @@ const defaultTheme = {
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/
const devTheme = {
- components: {
- Menu: {
- darkItemHoverBg: '#a51d1d',
- itemHoverBg: '#a51d1d',
- horizontalItemHoverBg: '#a51d1d',
- }
- },
- token: {
- colorPrimary: '#a51d1d'
+ components: {
+ Menu: {
+ darkItemHoverBg: "#a51d1d",
+ itemHoverBg: "#a51d1d",
+ horizontalItemHoverBg: "#a51d1d"
}
+ },
+ token: {
+ colorPrimary: "#a51d1d"
+ }
};
/**
@@ -60,11 +60,10 @@ const devTheme = {
*/
const prodTheme = {};
-const currentTheme = import.meta.env.DEV ? devTheme
- : prodTheme;
+const currentTheme = import.meta.env.DEV ? devTheme : prodTheme;
const finaltheme = {
- algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
- ...defaultsDeep(currentTheme, defaultTheme)
-}
+ algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
+ ...defaultsDeep(currentTheme, defaultTheme)
+};
export default finaltheme;
diff --git a/client/src/assets/promanager/ios/Contents.json b/client/src/assets/promanager/ios/Contents.json
index bd04914ae..0767e12cf 100644
--- a/client/src/assets/promanager/ios/Contents.json
+++ b/client/src/assets/promanager/ios/Contents.json
@@ -131,4 +131,4 @@
"author": "iconkitchen",
"version": 1
}
-}
\ No newline at end of file
+}
diff --git a/client/src/components/PrivateRoute.jsx b/client/src/components/PrivateRoute.jsx
index ebb7ece93..9286ae2d6 100644
--- a/client/src/components/PrivateRoute.jsx
+++ b/client/src/components/PrivateRoute.jsx
@@ -1,17 +1,17 @@
-import React, {useEffect} from "react";
-import {Outlet, useLocation, useNavigate} from "react-router-dom";
+import React, { useEffect } from "react";
+import { Outlet, useLocation, useNavigate } from "react-router-dom";
-function PrivateRoute({component: Component, isAuthorized, ...rest}) {
- const location = useLocation();
- const navigate = useNavigate();
+function PrivateRoute({ component: Component, isAuthorized, ...rest }) {
+ const location = useLocation();
+ const navigate = useNavigate();
- useEffect(() => {
- if (!isAuthorized) {
- navigate(`/signin?redirect=${location.pathname}`);
- }
- }, [isAuthorized, navigate, location]);
+ useEffect(() => {
+ if (!isAuthorized) {
+ navigate(`/signin?redirect=${location.pathname}`);
+ }
+ }, [isAuthorized, navigate, location]);
- return ;
+ return ;
}
export default PrivateRoute;
diff --git a/client/src/components/_test/test.page.jsx b/client/src/components/_test/test.page.jsx
index df1d9b41b..10af3b10b 100644
--- a/client/src/components/_test/test.page.jsx
+++ b/client/src/components/_test/test.page.jsx
@@ -1,31 +1,30 @@
-import {Button} from "antd";
+import { Button } from "antd";
import React from "react";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {setModalContext} from "../../redux/modals/modals.actions";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { setModalContext } from "../../redux/modals/modals.actions";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
- setRefundPaymentContext: (context) =>
- dispatch(setModalContext({context: context, modal: "refund_payment"})),
+ setRefundPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "refund_payment" }))
});
-function Test({setRefundPaymentContext, refundPaymentModal}) {
- console.log("refundPaymentModal", refundPaymentModal);
- return (
-
-
- setRefundPaymentContext({
- context: {},
- })
- }
- >
- Open Modal
-
-
- );
+function Test({ setRefundPaymentContext, refundPaymentModal }) {
+ console.log("refundPaymentModal", refundPaymentModal);
+ return (
+
+
+ setRefundPaymentContext({
+ context: {}
+ })
+ }
+ >
+ Open Modal
+
+
+ );
}
export default connect(mapStateToProps, mapDispatchToProps)(Test);
diff --git a/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx b/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx
index bb3201fcc..f501b3dbb 100644
--- a/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx
+++ b/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx
@@ -1,15 +1,16 @@
-import {Card, Checkbox, Input, Space, Table} from "antd";import queryString from "query-string";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect } from "react-redux";
-import {Link} from "react-router-dom";
+import { Card, Checkbox, Input, Space, Table } from "antd";
+import queryString from "query-string";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
-import {logImEXEvent} from "../../firebase/firebase.utils";
+import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
-import {DateFormatter} from "../../utils/DateFormatter";
+import { DateFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
-import {alphaSort, dateSort} from "../../utils/sorters";
+import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import PayableExportButton from "../payable-export-button/payable-export-button.component";
@@ -17,216 +18,179 @@ import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(AccountingPayablesTableComponent);
+export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
-export function AccountingPayablesTableComponent({
- bodyshop,
- loading,
- bills,
- refetch,
- }) {
- const {t} = useTranslation();
- const [selectedBills, setSelectedBills] = useState([]);
- const [transInProgress, setTransInProgress] = useState(false);
- const [state, setState] = useState({
- sortedInfo: {},
- search: "",
- });
+export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
+ const { t } = useTranslation();
+ const [selectedBills, setSelectedBills] = useState([]);
+ const [transInProgress, setTransInProgress] = useState(false);
+ const [state, setState] = useState({
+ sortedInfo: {},
+ search: ""
+ });
- const handleTableChange = (pagination, filters, sorter) => {
- setState({...state, filteredInfo: filters, sortedInfo: sorter});
- };
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
- const columns = [
- {
- title: t("bills.fields.vendorname"),
- dataIndex: "vendorname",
- key: "vendorname",
- sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
- sortOrder:
- state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
- render: (text, record) => (
-
- {record.vendor.name}
-
- ),
- },
- {
- title: t("bills.fields.invoice_number"),
- dataIndex: "invoice_number",
- key: "invoice_number",
- sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
- sortOrder:
- state.sortedInfo.columnKey === "invoice_number" &&
- state.sortedInfo.order,
- render: (text, record) => (
-
- {record.invoice_number}
-
- ),
- },
- {
- title: t("jobs.fields.ro_number"),
- dataIndex: "ro_number",
- key: "ro_number",
- sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
- sortOrder:
- state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
- render: (text, record) => (
- {record.job.ro_number}
- ),
- },
- {
- title: t("bills.fields.date"),
- dataIndex: "date",
- key: "date",
-
- sorter: (a, b) => dateSort(a.date, b.date),
- sortOrder:
- state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
- render: (text, record) => {record.date} ,
- },
- {
- title: t("bills.fields.total"),
- dataIndex: "total",
- key: "total",
-
- sorter: (a, b) => a.total - b.total,
- sortOrder:
- state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
- render: (text, record) => (
- {record.total}
- ),
- },
- {
- title: t("bills.fields.is_credit_memo"),
- dataIndex: "is_credit_memo",
- key: "is_credit_memo",
- sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
- sortOrder:
- state.sortedInfo.columnKey === "is_credit_memo" &&
- state.sortedInfo.order,
- render: (text, record) => (
-
- ),
- },
- {
- title: t("exportlogs.labels.attempts"),
- dataIndex: "attempts",
- key: "attempts",
-
- render: (text, record) => (
-
- ),
- },
- {
- title: t("general.labels.actions"),
- dataIndex: "actions",
- key: "actions",
-
-
- render: (text, record) => (
-
- ),
- },
- ];
-
- const handleSearch = (e) => {
- setState({...state, search: e.target.value});
- logImEXEvent("accounting_payables_table_search");
- };
-
- const dataSource = state.search
- ? bills.filter(
- (v) =>
- (v.vendor.name || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (v.invoice_number || "")
- .toLowerCase()
- .includes(state.search.toLowerCase())
- )
- : bills;
-
- return (
-
-
-
- {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
-
- )}
-
-
- }
+ const columns = [
+ {
+ title: t("bills.fields.vendorname"),
+ dataIndex: "vendorname",
+ key: "vendorname",
+ sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
+ sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
+ render: (text, record) => (
+
-
- setSelectedBills(selectedRows.map((i) => i.id)),
- onSelect: (record, selected, selectedRows, nativeEvent) => {
- setSelectedBills(selectedRows.map((i) => i.id));
- },
- getCheckboxProps: (record) => ({
- disabled: record.exported,
- }),
- selectedRowKeys: selectedBills,
- type: "checkbox",
- }}
- />
-
- );
+ {record.vendor.name}
+
+ )
+ },
+ {
+ title: t("bills.fields.invoice_number"),
+ dataIndex: "invoice_number",
+ key: "invoice_number",
+ sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
+ sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order,
+ render: (text, record) => (
+
+ {record.invoice_number}
+
+ )
+ },
+ {
+ title: t("jobs.fields.ro_number"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
+ sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+ render: (text, record) => {record.job.ro_number}
+ },
+ {
+ title: t("bills.fields.date"),
+ dataIndex: "date",
+ key: "date",
+
+ sorter: (a, b) => dateSort(a.date, b.date),
+ sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
+ render: (text, record) => {record.date}
+ },
+ {
+ title: t("bills.fields.total"),
+ dataIndex: "total",
+ key: "total",
+
+ sorter: (a, b) => a.total - b.total,
+ sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
+ render: (text, record) => {record.total}
+ },
+ {
+ title: t("bills.fields.is_credit_memo"),
+ dataIndex: "is_credit_memo",
+ key: "is_credit_memo",
+ sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
+ sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
+ render: (text, record) =>
+ },
+ {
+ title: t("exportlogs.labels.attempts"),
+ dataIndex: "attempts",
+ key: "attempts",
+
+ render: (text, record) =>
+ },
+ {
+ title: t("general.labels.actions"),
+ dataIndex: "actions",
+ key: "actions",
+
+ render: (text, record) => (
+
+ )
+ }
+ ];
+
+ const handleSearch = (e) => {
+ setState({ ...state, search: e.target.value });
+ logImEXEvent("accounting_payables_table_search");
+ };
+
+ const dataSource = state.search
+ ? bills.filter(
+ (v) =>
+ (v.vendor.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (v.invoice_number || "").toLowerCase().includes(state.search.toLowerCase())
+ )
+ : bills;
+
+ return (
+
+
+
+ {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && }
+
+
+ }
+ >
+ setSelectedBills(selectedRows.map((i) => i.id)),
+ onSelect: (record, selected, selectedRows, nativeEvent) => {
+ setSelectedBills(selectedRows.map((i) => i.id));
+ },
+ getCheckboxProps: (record) => ({
+ disabled: record.exported
+ }),
+ selectedRowKeys: selectedBills,
+ type: "checkbox"
+ }}
+ />
+
+ );
}
diff --git a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx
index 9f1c37651..f6040fab2 100644
--- a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx
+++ b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx
@@ -1,242 +1,198 @@
-import {Card, Input, Space, Table} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {Link} from "react-router-dom";
-import {createStructuredSelector} from "reselect";
-import {logImEXEvent} from "../../firebase/firebase.utils";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { Card, Input, Space, Table } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { Link } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { logImEXEvent } from "../../firebase/firebase.utils";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
-import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter";
-import {pageLimit } from "../../utils/config";
-import {alphaSort, dateSort} from "../../utils/sorters";
+import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
+import { pageLimit } from "../../utils/config";
+import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
-import OwnerNameDisplay, {
- OwnerNameDisplayFunction,
-} from "../owner-name-display/owner-name-display.component";
+import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(AccountingPayablesTableComponent);
+export default connect(mapStateToProps, mapDispatchToProps)(AccountingPayablesTableComponent);
-export function AccountingPayablesTableComponent({
- bodyshop,
- loading,
- payments,
- refetch,
- }) {
- const {t} = useTranslation();
- const [selectedPayments, setSelectedPayments] = useState([]);
- const [transInProgress, setTransInProgress] = useState(false);
- const [state, setState] = useState({
- sortedInfo: {},
- search: "",
- });
+export function AccountingPayablesTableComponent({ bodyshop, loading, payments, refetch }) {
+ const { t } = useTranslation();
+ const [selectedPayments, setSelectedPayments] = useState([]);
+ const [transInProgress, setTransInProgress] = useState(false);
+ const [state, setState] = useState({
+ sortedInfo: {},
+ search: ""
+ });
- const handleTableChange = (pagination, filters, sorter) => {
- setState({...state, filteredInfo: filters, sortedInfo: sorter});
- };
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
- const columns = [
- {
- title: t("jobs.fields.ro_number"),
- dataIndex: "ro_number",
- key: "ro_number",
- sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
- sortOrder:
- state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
- render: (text, record) => (
- {record.job.ro_number}
- ),
- },
- {
- title: t("payments.fields.date"),
- dataIndex: "date",
- key: "date",
- sorter: (a, b) => dateSort(a.date, b.date),
- sortOrder:
- state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
- render: (text, record) => {record.date} ,
- },
+ const columns = [
+ {
+ title: t("jobs.fields.ro_number"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
+ sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+ render: (text, record) => {record.job.ro_number}
+ },
+ {
+ title: t("payments.fields.date"),
+ dataIndex: "date",
+ key: "date",
+ sorter: (a, b) => dateSort(a.date, b.date),
+ sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
+ render: (text, record) => {record.date}
+ },
- {
- title: t("jobs.fields.owner"),
- dataIndex: "owner",
- key: "owner",
- ellipsis: true,
- sorter: (a, b) =>
- alphaSort(
- OwnerNameDisplayFunction(a.job),
- OwnerNameDisplayFunction(b.job)
- ),
- sortOrder:
- state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
- render: (text, record) => {
- return record.job.owner ? (
-
-
-
- ) : (
-
-
+ {
+ title: t("jobs.fields.owner"),
+ dataIndex: "owner",
+ key: "owner",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a.job), OwnerNameDisplayFunction(b.job)),
+ sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.job.owner ? (
+
+
+
+ ) : (
+
+
- );
- },
- },
- {
- title: t("payments.fields.amount"),
- dataIndex: "amount",
- key: "amount",
- sorter: (a, b) => a.amount - b.amount,
- sortOrder:
- state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,render: (text, record) => (
- {record.amount}
- ),
- },
- {
- title: t("payments.fields.memo"),
- dataIndex: "memo",
- key: "memo",
- },
- {
- title: t("payments.fields.transactionid"),
- dataIndex: "transactionid",
- key: "transactionid",
- },
- {
- title: t("payments.fields.created_at"),
- dataIndex: "created_at",
- key: "created_at",sorter: (a, b) => dateSort(a.created_at, b.created_at),
- sortOrder:
- state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
- render: (text, record) => (
- {record.created_at}
- ),
- },
- //{
- // title: t("payments.fields.exportedat"),
- // dataIndex: "exportedat",
- // key: "exportedat",
- // render: (text, record) => (
- // {record.exportedat}
- // ),
- //},
- {
- title: t("exportlogs.labels.attempts"),
- dataIndex: "attempts",
- key: "attempts",
+ );
+ }
+ },
+ {
+ title: t("payments.fields.amount"),
+ dataIndex: "amount",
+ key: "amount",
+ sorter: (a, b) => a.amount - b.amount,
+ sortOrder: state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
+ render: (text, record) => {record.amount}
+ },
+ {
+ title: t("payments.fields.memo"),
+ dataIndex: "memo",
+ key: "memo"
+ },
+ {
+ title: t("payments.fields.transactionid"),
+ dataIndex: "transactionid",
+ key: "transactionid"
+ },
+ {
+ title: t("payments.fields.created_at"),
+ dataIndex: "created_at",
+ key: "created_at",
+ sorter: (a, b) => dateSort(a.created_at, b.created_at),
+ sortOrder: state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
+ render: (text, record) => {record.created_at}
+ },
+ //{
+ // title: t("payments.fields.exportedat"),
+ // dataIndex: "exportedat",
+ // key: "exportedat",
+ // render: (text, record) => (
+ // {record.exportedat}
+ // ),
+ //},
+ {
+ title: t("exportlogs.labels.attempts"),
+ dataIndex: "attempts",
+ key: "attempts",
- render: (text, record) => (
-
- ),
- },
- {
- title: t("general.labels.actions"),
- dataIndex: "actions",
- key: "actions",
+ render: (text, record) =>
+ },
+ {
+ title: t("general.labels.actions"),
+ dataIndex: "actions",
+ key: "actions",
+ render: (text, record) => (
+
+ )
+ }
+ ];
- render: (text, record) => (
-
- ),
- },
- ];
+ const handleSearch = (e) => {
+ setState({ ...state, search: e.target.value });
+ logImEXEvent("account_payments_table_search");
+ };
- const handleSearch = (e) => {
- setState({...state, search: e.target.value});
- logImEXEvent("account_payments_table_search");
- };
+ const dataSource = state.search
+ ? payments.filter(
+ (v) =>
+ (v.paymentnum || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ ((v.job && v.job.ro_number) || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ ((v.job && v.job.ownr_fn) || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ ((v.job && v.job.ownr_ln) || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ ((v.job && v.job.ownr_co_nm) || "").toLowerCase().includes(state.search.toLowerCase())
+ )
+ : payments;
- const dataSource = state.search
- ? payments.filter(
- (v) =>
- (v.paymentnum || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- ((v.job && v.job.ro_number) || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- ((v.job && v.job.ownr_fn) || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- ((v.job && v.job.ownr_ln) || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- ((v.job && v.job.ownr_co_nm) || "")
- .toLowerCase()
- .includes(state.search.toLowerCase())
- )
- : payments;
-
- return (
-
-
-
- {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
-
- )}
-
-
- }
- >
-
- setSelectedPayments(selectedRows.map((i) => i.id)),
- onSelect: (record, selected, selectedRows, nativeEvent) => {
- setSelectedPayments(selectedRows.map((i) => i.id));
- },
- getCheckboxProps: (record) => ({
- disabled: record.exported,
- }),
- selectedRowKeys: selectedPayments,
- type: "checkbox",
- }}
- />
-
- );
+ return (
+
+
+
+ {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && }
+
+
+ }
+ >
+ setSelectedPayments(selectedRows.map((i) => i.id)),
+ onSelect: (record, selected, selectedRows, nativeEvent) => {
+ setSelectedPayments(selectedRows.map((i) => i.id));
+ },
+ getCheckboxProps: (record) => ({
+ disabled: record.exported
+ }),
+ selectedRowKeys: selectedPayments,
+ type: "checkbox"
+ }}
+ />
+
+ );
}
diff --git a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx
index 1fe3cbf4d..83b139827 100644
--- a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx
+++ b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx
@@ -1,258 +1,212 @@
-import {Button, Card, Input, Space, Table} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {Link} from "react-router-dom";
-import {logImEXEvent} from "../../firebase/firebase.utils";
+import { Button, Card, Input, Space, Table } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router-dom";
+import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
-import {alphaSort, dateSort, statusSort } from "../../utils/sorters";
+import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectBodyshop} from "../../redux/user/user.selectors";
-import {DateFormatter} from "../../utils/DateFormatter";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { DateFormatter } from "../../utils/DateFormatter";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
-import OwnerNameDisplay, {
- OwnerNameDisplayFunction,
-} from "../owner-name-display/owner-name-display.component";
+import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(AccountingReceivablesTableComponent);
+export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
-export function AccountingReceivablesTableComponent({
- bodyshop,
- loading,
- jobs,
- refetch,
- }) {
- const {t} = useTranslation();
- const [selectedJobs, setSelectedJobs] = useState([]);
- const [transInProgress, setTransInProgress] = useState(false);
+export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, refetch }) {
+ const { t } = useTranslation();
+ const [selectedJobs, setSelectedJobs] = useState([]);
+ const [transInProgress, setTransInProgress] = useState(false);
- const [state, setState] = useState({
- sortedInfo: {},
- search: "",
- });
+ const [state, setState] = useState({
+ sortedInfo: {},
+ search: ""
+ });
- const handleTableChange = (pagination, filters, sorter) => {
- setState({...state, filteredInfo: filters, sortedInfo: sorter});
- };
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
- const columns = [
- {
- title: t("jobs.fields.ro_number"),
- dataIndex: "ro_number",
- key: "ro_number",
- sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
- sortOrder:
- state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
- render: (text, record) => (
- {record.ro_number}
- ),
- },
+ const columns = [
+ {
+ title: t("jobs.fields.ro_number"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
+ sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+ render: (text, record) => {record.ro_number}
+ },
- {
- title: t("jobs.fields.status"),
- dataIndex: "status",
- key: "status",
- sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
- sortOrder:
- state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
- },
- {
- title: t("jobs.fields.date_invoiced"),
- dataIndex: "date_invoiced",
- key: "date_invoiced",
- sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
- sortOrder:
- state.sortedInfo.columnKey === "date_invoiced" &&
- state.sortedInfo.order,
- render: (text, record) => (
- {record.date_invoiced}
- ),
- },
- {
- title: t("jobs.fields.owner"),
- dataIndex: "owner",
- key: "owner",
- sorter: (a, b) =>
- alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
- sortOrder:
- state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
- render: (text, record) => {
- return record.owner ? (
-
-
-
- ) : (
-
-
+ {
+ title: t("jobs.fields.status"),
+ dataIndex: "status",
+ key: "status",
+ sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
+ sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order
+ },
+ {
+ title: t("jobs.fields.date_invoiced"),
+ dataIndex: "date_invoiced",
+ key: "date_invoiced",
+ sorter: (a, b) => dateSort(a.date_invoiced, b.date_invoiced),
+ sortOrder: state.sortedInfo.columnKey === "date_invoiced" && state.sortedInfo.order,
+ render: (text, record) => {record.date_invoiced}
+ },
+ {
+ title: t("jobs.fields.owner"),
+ dataIndex: "owner",
+ key: "owner",
+ sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
+ sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.owner ? (
+
+
+
+ ) : (
+
+
- );
- },
- },
- {
- title: t("jobs.fields.vehicle"),
- dataIndex: "vehicle",
- key: "vehicle",
- ellipsis: true,
- sorter: (a, b) =>
+ );
+ }
+ },
+ {
+ title: t("jobs.fields.vehicle"),
+ dataIndex: "vehicle",
+ key: "vehicle",
+ ellipsis: true,
+ sorter: (a, b) =>
alphaSort(
- `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
- a.v_model_desc || ""
- }`,
+ `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${a.v_model_desc || ""}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
- sortOrder:
- state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,render: (text, record) => {
- return record.vehicleid ? (
-
- {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
- record.v_model_desc || ""
- }`}
-
- ) : (
- {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
- record.v_model_desc || ""
- }`}
- );
- },
- },
- {
- title: t("jobs.fields.clm_no"),
- dataIndex: "clm_no",
- key: "clm_no",
- ellipsis: true,
- sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
- sortOrder:
- state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
- },
- {
- title: t("jobs.fields.clm_total"),
- dataIndex: "clm_total",
- key: "clm_total",
- sorter: (a, b) => a.clm_total - b.clm_total,
- sortOrder:
- state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
- render: (text, record) => {
- return {record.clm_total} ;
- },
- },
- {
- title: t("exportlogs.labels.attempts"),
- dataIndex: "attempts",
- key: "attempts",
- render: (text, record) => (
-
- ),
- },
- {
- title: t("general.labels.actions"),
- dataIndex: "actions",
- key: "actions",
+ sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.vehicleid ? (
+
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
+
+ ) : (
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
+ );
+ }
+ },
+ {
+ title: t("jobs.fields.clm_no"),
+ dataIndex: "clm_no",
+ key: "clm_no",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
+ sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order
+ },
+ {
+ title: t("jobs.fields.clm_total"),
+ dataIndex: "clm_total",
+ key: "clm_total",
+ sorter: (a, b) => a.clm_total - b.clm_total,
+ sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
+ render: (text, record) => {
+ return {record.clm_total} ;
+ }
+ },
+ {
+ title: t("exportlogs.labels.attempts"),
+ dataIndex: "attempts",
+ key: "attempts",
+ render: (text, record) =>
+ },
+ {
+ title: t("general.labels.actions"),
+ dataIndex: "actions",
+ key: "actions",
- render: (text, record) => (
-
-
-
- {t("jobs.labels.viewallocations")}
-
-
- ),
- },
- ];
+ render: (text, record) => (
+
+
+
+ {t("jobs.labels.viewallocations")}
+
+
+ )
+ }
+ ];
- const handleSearch = (e) => {
- setState({...state, search: e.target.value});
- logImEXEvent("accounting_receivables_search");
- };
+ const handleSearch = (e) => {
+ setState({ ...state, search: e.target.value });
+ logImEXEvent("accounting_receivables_search");
+ };
- const dataSource = state.search
- ? jobs.filter(
- (v) =>
- (v.ro_number || "")
- .toString()
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (v.ownr_fn || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (v.ownr_ln || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (v.ownr_co_nm || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (v.v_model_desc || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (v.v_make_desc || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
- )
- : jobs;
+ const dataSource = state.search
+ ? jobs.filter(
+ (v) =>
+ (v.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) ||
+ (v.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (v.ownr_ln || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (v.ownr_co_nm || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (v.v_model_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (v.v_make_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (v.clm_no || "").toLowerCase().includes(state.search.toLowerCase())
+ )
+ : jobs;
- return (
-
- {!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
-
- )}
- {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
-
- )}
-
-
- }
- >
-
- setSelectedJobs(selectedRows.map((i) => i.id)),
- onSelect: (record, selected, selectedRows, nativeEvent) => {
- setSelectedJobs(selectedRows.map((i) => i.id));
- },
- getCheckboxProps: (record) => ({
- disabled: record.exported,
- }),
- selectedRowKeys: selectedJobs,
- type: "checkbox",
- }}
+ return (
+
+ {!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
+
-
- );
+ )}
+ {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && }
+
+
+ }
+ >
+ setSelectedJobs(selectedRows.map((i) => i.id)),
+ onSelect: (record, selected, selectedRows, nativeEvent) => {
+ setSelectedJobs(selectedRows.map((i) => i.id));
+ },
+ getCheckboxProps: (record) => ({
+ disabled: record.exported
+ }),
+ selectedRowKeys: selectedJobs,
+ type: "checkbox"
+ }}
+ />
+
+ );
}
diff --git a/client/src/components/alert/alert.component.jsx b/client/src/components/alert/alert.component.jsx
index 5af5fce31..f02edd614 100644
--- a/client/src/components/alert/alert.component.jsx
+++ b/client/src/components/alert/alert.component.jsx
@@ -1,6 +1,6 @@
-import {Alert} from "antd";
+import { Alert } from "antd";
import React from "react";
export default function AlertComponent(props) {
- return ;
+ return ;
}
diff --git a/client/src/components/alert/alert.component.test.js b/client/src/components/alert/alert.component.test.js
index 32114c305..b7c14b48a 100644
--- a/client/src/components/alert/alert.component.test.js
+++ b/client/src/components/alert/alert.component.test.js
@@ -1,19 +1,19 @@
-import {shallow} from "enzyme";
+import { shallow } from "enzyme";
import React from "react";
import Alert from "./alert.component";
describe("Alert component", () => {
- let wrapper;
- beforeEach(() => {
- const mockProps = {
- type: "error",
- message: "Test error message.",
- };
+ let wrapper;
+ beforeEach(() => {
+ const mockProps = {
+ type: "error",
+ message: "Test error message."
+ };
- wrapper = shallow( );
- });
+ wrapper = shallow( );
+ });
- it("should render Alert component", () => {
- expect(wrapper).toMatchSnapshot();
- });
+ it("should render Alert component", () => {
+ expect(wrapper).toMatchSnapshot();
+ });
});
diff --git a/client/src/components/allocations-assignment/allocations-assignment.component.jsx b/client/src/components/allocations-assignment/allocations-assignment.component.jsx
index 1d1804341..929d9eb75 100644
--- a/client/src/components/allocations-assignment/allocations-assignment.component.jsx
+++ b/client/src/components/allocations-assignment/allocations-assignment.component.jsx
@@ -1,72 +1,67 @@
-import {Button, InputNumber, Popover, Select} from "antd";
+import { Button, InputNumber, Popover, Select } from "antd";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop
+ bodyshop: selectBodyshop
});
export function AllocationsAssignmentComponent({
- bodyshop,
- handleAssignment,
- assignment,
- setAssignment,
- visibilityState,
- maxHours
- }) {
- const {t} = useTranslation();
+ bodyshop,
+ handleAssignment,
+ assignment,
+ setAssignment,
+ visibilityState,
+ maxHours
+}) {
+ const { t } = useTranslation();
- const onChange = e => {
- setAssignment({...assignment, employeeid: e});
- };
+ const onChange = (e) => {
+ setAssignment({ ...assignment, employeeid: e });
+ };
- const [visibility, setVisibility] = visibilityState;
+ const [visibility, setVisibility] = visibilityState;
- const popContent = (
-
-
- option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
- }>
- {bodyshop.employees.map(emp => (
-
- {`${emp.first_name} ${emp.last_name}`}
-
- ))}
-
-
setAssignment({...assignment, hours: e})}
- />
+ const popContent = (
+
+ option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
+ >
+ {bodyshop.employees.map((emp) => (
+
+ {`${emp.first_name} ${emp.last_name}`}
+
+ ))}
+
+ setAssignment({ ...assignment, hours: e })}
+ />
-
- Assign
-
- setVisibility(false)}>Close
-
- );
+
+ Assign
+
+ setVisibility(false)}>Close
+
+ );
- return (
-
- setVisibility(true)}>
- {t("allocations.actions.assign")}
-
-
- );
+ return (
+
+ setVisibility(true)}>{t("allocations.actions.assign")}
+
+ );
}
export default connect(mapStateToProps, null)(AllocationsAssignmentComponent);
diff --git a/client/src/components/allocations-assignment/allocations-assignment.component.test.js b/client/src/components/allocations-assignment/allocations-assignment.component.test.js
index 8aad7efe6..fe9c3628b 100644
--- a/client/src/components/allocations-assignment/allocations-assignment.component.test.js
+++ b/client/src/components/allocations-assignment/allocations-assignment.component.test.js
@@ -1,35 +1,35 @@
-import {mount} from "enzyme";
+import { mount } from "enzyme";
import React from "react";
-import {MockBodyshop} from "../../utils/TestingHelpers";
-import {AllocationsAssignmentComponent} from "./allocations-assignment.component";
+import { MockBodyshop } from "../../utils/TestingHelpers";
+import { AllocationsAssignmentComponent } from "./allocations-assignment.component";
describe("AllocationsAssignmentComponent component", () => {
- let wrapper;
+ let wrapper;
- beforeEach(() => {
- const mockProps = {
- bodyshop: MockBodyshop,
- handleAssignment: jest.fn(),
- assignment: {},
- setAssignment: jest.fn(),
- visibilityState: [false, jest.fn()],
- maxHours: 4,
- };
+ beforeEach(() => {
+ const mockProps = {
+ bodyshop: MockBodyshop,
+ handleAssignment: jest.fn(),
+ assignment: {},
+ setAssignment: jest.fn(),
+ visibilityState: [false, jest.fn()],
+ maxHours: 4
+ };
- wrapper = mount( );
- });
+ wrapper = mount( );
+ });
- it("should render AllocationsAssignmentComponent component", () => {
- expect(wrapper).toMatchSnapshot();
- });
+ it("should render AllocationsAssignmentComponent component", () => {
+ expect(wrapper).toMatchSnapshot();
+ });
- it("should render a list of employees", () => {
- const empList = wrapper.find("#employeeSelector");
- expect(empList.children()).to.have.lengthOf(2);
- });
+ it("should render a list of employees", () => {
+ const empList = wrapper.find("#employeeSelector");
+ expect(empList.children()).to.have.lengthOf(2);
+ });
- it("should create an allocation on save", () => {
- wrapper.find("Button").simulate("click");
- expect(wrapper).toMatchSnapshot();
- });
+ it("should create an allocation on save", () => {
+ wrapper.find("Button").simulate("click");
+ expect(wrapper).toMatchSnapshot();
+ });
});
diff --git a/client/src/components/allocations-assignment/allocations-assignment.container.jsx b/client/src/components/allocations-assignment/allocations-assignment.container.jsx
index 5153066cb..43d4b306c 100644
--- a/client/src/components/allocations-assignment/allocations-assignment.container.jsx
+++ b/client/src/components/allocations-assignment/allocations-assignment.container.jsx
@@ -1,47 +1,43 @@
-import React, {useState} from "react";
+import React, { useState } from "react";
import AllocationsAssignmentComponent from "./allocations-assignment.component";
-import {useMutation} from "@apollo/client";
-import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
-import {useTranslation} from "react-i18next";
-import {notification} from "antd";
+import { useMutation } from "@apollo/client";
+import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
+import { useTranslation } from "react-i18next";
+import { notification } from "antd";
-export default function AllocationsAssignmentContainer({
- jobLineId,
- hours,
- refetch,
- }) {
- const visibilityState = useState(false);
- const {t} = useTranslation();
- const [assignment, setAssignment] = useState({
- joblineid: jobLineId,
- hours: parseFloat(hours),
- employeeid: null,
- });
- const [insertAllocation] = useMutation(INSERT_ALLOCATION);
+export default function AllocationsAssignmentContainer({ jobLineId, hours, refetch }) {
+ const visibilityState = useState(false);
+ const { t } = useTranslation();
+ const [assignment, setAssignment] = useState({
+ joblineid: jobLineId,
+ hours: parseFloat(hours),
+ employeeid: null
+ });
+ const [insertAllocation] = useMutation(INSERT_ALLOCATION);
- const handleAssignment = () => {
- insertAllocation({variables: {alloc: {...assignment}}})
- .then((r) => {
- notification["success"]({
- message: t("allocations.successes.save"),
- });
- visibilityState[1](false);
- if (refetch) refetch();
- })
- .catch((error) => {
- notification["error"]({
- message: t("employees.errors.saving", {message: error.message}),
- });
- });
- };
+ const handleAssignment = () => {
+ insertAllocation({ variables: { alloc: { ...assignment } } })
+ .then((r) => {
+ notification["success"]({
+ message: t("allocations.successes.save")
+ });
+ visibilityState[1](false);
+ if (refetch) refetch();
+ })
+ .catch((error) => {
+ notification["error"]({
+ message: t("employees.errors.saving", { message: error.message })
+ });
+ });
+ };
- return (
-
- );
+ return (
+
+ );
}
diff --git a/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.component.jsx b/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.component.jsx
index 2af0d75d0..301887fb7 100644
--- a/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.component.jsx
+++ b/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.component.jsx
@@ -1,68 +1,62 @@
-import {Button, Popover, Select} from "antd";
+import { Button, Popover, Select } from "antd";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
export default connect(
- mapStateToProps,
- null
+ mapStateToProps,
+ null
)(function AllocationsBulkAssignmentComponent({
- disabled,
- bodyshop,
- handleAssignment,
- assignment,
- setAssignment,
- visibilityState,
- }) {
- const {t} = useTranslation();
+ disabled,
+ bodyshop,
+ handleAssignment,
+ assignment,
+ setAssignment,
+ visibilityState
+}) {
+ const { t } = useTranslation();
- const onChange = (e) => {
- setAssignment({...assignment, employeeid: e});
- };
+ const onChange = (e) => {
+ setAssignment({ ...assignment, employeeid: e });
+ };
- const [visibility, setVisibility] = visibilityState;
+ const [visibility, setVisibility] = visibilityState;
- const popContent = (
-
-
- option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
- }
- >
- {bodyshop.employees.map((emp) => (
-
- {`${emp.first_name} ${emp.last_name}`}
-
- ))}
-
+ const popContent = (
+
+ option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
+ >
+ {bodyshop.employees.map((emp) => (
+
+ {`${emp.first_name} ${emp.last_name}`}
+
+ ))}
+
-
- Assign
-
- setVisibility(false)}>Close
-
- );
+
+ Assign
+
+
setVisibility(false)}>Close
+
+ );
- return (
-
- setVisibility(true)}>
- {t("allocations.actions.assign")}
-
-
- );
+ return (
+
+ setVisibility(true)}>
+ {t("allocations.actions.assign")}
+
+
+ );
});
diff --git a/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.container.jsx b/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.container.jsx
index aad3ea8d2..ea68dd129 100644
--- a/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.container.jsx
+++ b/client/src/components/allocations-bulk-assignment/allocations-bulk-assignment.container.jsx
@@ -1,47 +1,44 @@
-import React, {useState} from "react";
+import React, { useState } from "react";
import AllocationsBulkAssignment from "./allocations-bulk-assignment.component";
-import {useMutation} from "@apollo/client";
-import {INSERT_ALLOCATION} from "../../graphql/allocations.queries";
-import {useTranslation} from "react-i18next";
-import {notification} from "antd";
+import { useMutation } from "@apollo/client";
+import { INSERT_ALLOCATION } from "../../graphql/allocations.queries";
+import { useTranslation } from "react-i18next";
+import { notification } from "antd";
-export default function AllocationsBulkAssignmentContainer({
- jobLines,
- refetch,
- }) {
- const visibilityState = useState(false);
- const {t} = useTranslation();
- const [assignment, setAssignment] = useState({
- employeeid: null,
+export default function AllocationsBulkAssignmentContainer({ jobLines, refetch }) {
+ const visibilityState = useState(false);
+ const { t } = useTranslation();
+ const [assignment, setAssignment] = useState({
+ employeeid: null
+ });
+ const [insertAllocation] = useMutation(INSERT_ALLOCATION);
+
+ const handleAssignment = () => {
+ const allocs = jobLines.reduce((acc, value) => {
+ acc.push({
+ joblineid: value.id,
+ hours: parseFloat(value.mod_lb_hrs) || 0,
+ employeeid: assignment.employeeid
+ });
+ return acc;
+ }, []);
+
+ insertAllocation({ variables: { alloc: allocs } }).then((r) => {
+ notification["success"]({
+ message: t("employees.successes.save")
+ });
+ visibilityState[1](false);
+ if (refetch) refetch();
});
- const [insertAllocation] = useMutation(INSERT_ALLOCATION);
+ };
- const handleAssignment = () => {
- const allocs = jobLines.reduce((acc, value) => {
- acc.push({
- joblineid: value.id,
- hours: parseFloat(value.mod_lb_hrs) || 0,
- employeeid: assignment.employeeid,
- });
- return acc;
- }, []);
-
- insertAllocation({variables: {alloc: allocs}}).then((r) => {
- notification["success"]({
- message: t("employees.successes.save"),
- });
- visibilityState[1](false);
- if (refetch) refetch();
- });
- };
-
- return (
- 0 ? false : true}
- handleAssignment={handleAssignment}
- assignment={assignment}
- setAssignment={setAssignment}
- visibilityState={visibilityState}
- />
- );
+ return (
+ 0 ? false : true}
+ handleAssignment={handleAssignment}
+ assignment={assignment}
+ setAssignment={setAssignment}
+ visibilityState={visibilityState}
+ />
+ );
}
diff --git a/client/src/components/allocations-employee-label/allocations-employee-label.component.jsx b/client/src/components/allocations-employee-label/allocations-employee-label.component.jsx
index 9f1447348..0adc01964 100644
--- a/client/src/components/allocations-employee-label/allocations-employee-label.component.jsx
+++ b/client/src/components/allocations-employee-label/allocations-employee-label.component.jsx
@@ -1,20 +1,14 @@
import Icon from "@ant-design/icons";
import React from "react";
-import {MdRemoveCircleOutline} from "react-icons/md";
+import { MdRemoveCircleOutline } from "react-icons/md";
-export default function AllocationsLabelComponent({allocation, handleClick}) {
- return (
-
+export default function AllocationsLabelComponent({ allocation, handleClick }) {
+ return (
+
- {`${allocation.employee.first_name || ""} ${
- allocation.employee.last_name || ""
- } (${allocation.hours || ""})`}
+ {`${allocation.employee.first_name || ""} ${allocation.employee.last_name || ""} (${allocation.hours || ""})`}
-
-
- );
+
+
+ );
}
diff --git a/client/src/components/allocations-employee-label/allocations-employee-label.container.jsx b/client/src/components/allocations-employee-label/allocations-employee-label.container.jsx
index e573e806b..44fe5d43e 100644
--- a/client/src/components/allocations-employee-label/allocations-employee-label.container.jsx
+++ b/client/src/components/allocations-employee-label/allocations-employee-label.container.jsx
@@ -1,32 +1,27 @@
import React from "react";
-import {useMutation} from "@apollo/client";
-import {DELETE_ALLOCATION} from "../../graphql/allocations.queries";
+import { useMutation } from "@apollo/client";
+import { DELETE_ALLOCATION } from "../../graphql/allocations.queries";
import AllocationsLabelComponent from "./allocations-employee-label.component";
-import {notification} from "antd";
-import {useTranslation} from "react-i18next";
+import { notification } from "antd";
+import { useTranslation } from "react-i18next";
-export default function AllocationsLabelContainer({allocation, refetch}) {
- const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
- const {t} = useTranslation();
+export default function AllocationsLabelContainer({ allocation, refetch }) {
+ const [deleteAllocation] = useMutation(DELETE_ALLOCATION);
+ const { t } = useTranslation();
- const handleClick = (e) => {
- e.preventDefault();
- deleteAllocation({variables: {id: allocation.id}})
- .then((r) => {
- notification["success"]({
- message: t("allocations.successes.deleted"),
- });
- if (refetch) refetch();
- })
- .catch((error) => {
- notification["error"]({message: t("allocations.errors.deleting")});
- });
- };
+ const handleClick = (e) => {
+ e.preventDefault();
+ deleteAllocation({ variables: { id: allocation.id } })
+ .then((r) => {
+ notification["success"]({
+ message: t("allocations.successes.deleted")
+ });
+ if (refetch) refetch();
+ })
+ .catch((error) => {
+ notification["error"]({ message: t("allocations.errors.deleting") });
+ });
+ };
- return (
-
- );
+ return ;
}
diff --git a/client/src/components/audit-trail-list/audit-trail-list.component.jsx b/client/src/components/audit-trail-list/audit-trail-list.component.jsx
index 3f6decd80..9722005d6 100644
--- a/client/src/components/audit-trail-list/audit-trail-list.component.jsx
+++ b/client/src/components/audit-trail-list/audit-trail-list.component.jsx
@@ -1,85 +1,75 @@
-import React, {useState} from "react";
-import {Table} from "antd";
-import {alphaSort} from "../../utils/sorters";
-import {DateTimeFormatter} from "../../utils/DateFormatter";
-import {useTranslation} from "react-i18next";
+import React, { useState } from "react";
+import { Table } from "antd";
+import { alphaSort } from "../../utils/sorters";
+import { DateTimeFormatter } from "../../utils/DateFormatter";
+import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
-import {pageLimit} from "../../utils/config";
+import { pageLimit } from "../../utils/config";
-export default function AuditTrailListComponent({loading, data}) {
- const [state, setState] = useState({
- sortedInfo: {},
- filteredInfo: {},
- });
- const {t} = useTranslation();
- const columns = [
- {
- title: t("audit.fields.created"),
- dataIndex: " created",
- key: " created",
- width: "10%",
- render: (text, record) => (
- {record.created}
- ),
- sorter: (a, b) => a.created - b.created,
- sortOrder:
- state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
- },
- {
- title: t("audit.fields.operation"),
- dataIndex: "operation",
- key: "operation",
- width: "10%",
- sorter: (a, b) => alphaSort(a.operation, b.operation),
- sortOrder:
- state.sortedInfo.columnKey === "operation" && state.sortedInfo.order,
- },
- {
- title: t("audit.fields.values"),
- dataIndex: " old_val",
- key: " old_val",
- width: "10%",
- render: (text, record) => (
-
- ),
- },
- {
- title: t("audit.fields.useremail"),
- dataIndex: "useremail",
- key: "useremail",
- width: "10%",
- sorter: (a, b) => alphaSort(a.useremail, b.useremail),
- sortOrder:
- state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
- },
- ];
+export default function AuditTrailListComponent({ loading, data }) {
+ const [state, setState] = useState({
+ sortedInfo: {},
+ filteredInfo: {}
+ });
+ const { t } = useTranslation();
+ const columns = [
+ {
+ title: t("audit.fields.created"),
+ dataIndex: " created",
+ key: " created",
+ width: "10%",
+ render: (text, record) => {record.created} ,
+ sorter: (a, b) => a.created - b.created,
+ sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
+ },
+ {
+ title: t("audit.fields.operation"),
+ dataIndex: "operation",
+ key: "operation",
+ width: "10%",
+ sorter: (a, b) => alphaSort(a.operation, b.operation),
+ sortOrder: state.sortedInfo.columnKey === "operation" && state.sortedInfo.order
+ },
+ {
+ title: t("audit.fields.values"),
+ dataIndex: " old_val",
+ key: " old_val",
+ width: "10%",
+ render: (text, record) =>
+ },
+ {
+ title: t("audit.fields.useremail"),
+ dataIndex: "useremail",
+ key: "useremail",
+ width: "10%",
+ sorter: (a, b) => alphaSort(a.useremail, b.useremail),
+ sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
+ }
+ ];
- const formItemLayout = {
- labelCol: {
- xs: {span: 12},
- sm: {span: 5},
- },
- wrapperCol: {
- xs: {span: 24},
- sm: {span: 12},
- },
- };
- const handleTableChange = (pagination, filters, sorter) => {
- setState({...state, filteredInfo: filters, sortedInfo: sorter});
- };
+ const formItemLayout = {
+ labelCol: {
+ xs: { span: 12 },
+ sm: { span: 5 }
+ },
+ wrapperCol: {
+ xs: { span: 24 },
+ sm: { span: 12 }
+ }
+ };
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
- return (
-
- );
+ return (
+
+ );
}
diff --git a/client/src/components/audit-trail-list/audit-trail-list.container.jsx b/client/src/components/audit-trail-list/audit-trail-list.container.jsx
index cd5672943..6361bc240 100644
--- a/client/src/components/audit-trail-list/audit-trail-list.container.jsx
+++ b/client/src/components/audit-trail-list/audit-trail-list.container.jsx
@@ -1,40 +1,34 @@
import React from "react";
import AuditTrailListComponent from "./audit-trail-list.component";
-import {useQuery} from "@apollo/client";
-import {QUERY_AUDIT_TRAIL} from "../../graphql/audit_trail.queries";
+import { useQuery } from "@apollo/client";
+import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import AlertComponent from "../alert/alert.component";
-import {logImEXEvent} from "../../firebase/firebase.utils";
+import { logImEXEvent } from "../../firebase/firebase.utils";
import EmailAuditTrailListComponent from "./email-audit-trail-list.component";
-import {Card, Row} from "antd";
+import { Card, Row } from "antd";
-export default function AuditTrailListContainer({recordId}) {
- const {loading, error, data} = useQuery(QUERY_AUDIT_TRAIL, {
- variables: {id: recordId},
- fetchPolicy: "network-only",
- nextFetchPolicy: "network-only",
- });
+export default function AuditTrailListContainer({ recordId }) {
+ const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
+ variables: { id: recordId },
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
- logImEXEvent("audittrail_view", {recordId});
- return (
-
- {error ? (
-
- ) : (
-
-
-
-
-
-
-
-
- )}
-
- );
+ logImEXEvent("audittrail_view", { recordId });
+ return (
+
+ {error ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+ )}
+
+ );
}
diff --git a/client/src/components/audit-trail-list/email-audit-trail-list.component.jsx b/client/src/components/audit-trail-list/email-audit-trail-list.component.jsx
index 015b0d962..5f462d500 100644
--- a/client/src/components/audit-trail-list/email-audit-trail-list.component.jsx
+++ b/client/src/components/audit-trail-list/email-audit-trail-list.component.jsx
@@ -1,64 +1,60 @@
-import {Table} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {DateTimeFormatter} from "../../utils/DateFormatter";
-import {alphaSort} from "../../utils/sorters";
-import {pageLimit} from "../../utils/config";
+import { Table } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { DateTimeFormatter } from "../../utils/DateFormatter";
+import { alphaSort } from "../../utils/sorters";
+import { pageLimit } from "../../utils/config";
-export default function EmailAuditTrailListComponent({loading, data}) {
- const [state, setState] = useState({
- sortedInfo: {},
- filteredInfo: {},
- });
- const {t} = useTranslation();
- const columns = [
- {
- title: t("audit.fields.created"),
- dataIndex: " created",
- key: " created",
- width: "10%",
- render: (text, record) => (
- {record.created}
- ),
- sorter: (a, b) => a.created - b.created,
- sortOrder:
- state.sortedInfo.columnKey === "created" && state.sortedInfo.order,
- },
+export default function EmailAuditTrailListComponent({ loading, data }) {
+ const [state, setState] = useState({
+ sortedInfo: {},
+ filteredInfo: {}
+ });
+ const { t } = useTranslation();
+ const columns = [
+ {
+ title: t("audit.fields.created"),
+ dataIndex: " created",
+ key: " created",
+ width: "10%",
+ render: (text, record) => {record.created} ,
+ sorter: (a, b) => a.created - b.created,
+ sortOrder: state.sortedInfo.columnKey === "created" && state.sortedInfo.order
+ },
- {
- title: t("audit.fields.useremail"),
- dataIndex: "useremail",
- key: "useremail",
- width: "10%",
- sorter: (a, b) => alphaSort(a.useremail, b.useremail),
- sortOrder:
- state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order,
- },
- ];
+ {
+ title: t("audit.fields.useremail"),
+ dataIndex: "useremail",
+ key: "useremail",
+ width: "10%",
+ sorter: (a, b) => alphaSort(a.useremail, b.useremail),
+ sortOrder: state.sortedInfo.columnKey === "useremail" && state.sortedInfo.order
+ }
+ ];
- const formItemLayout = {
- labelCol: {
- xs: {span: 12},
- sm: {span: 5},
- },
- wrapperCol: {
- xs: {span: 24},
- sm: {span: 12},
- },
- };
- const handleTableChange = (pagination, filters, sorter) => {
- setState({...state, filteredInfo: filters, sortedInfo: sorter});
- };
+ const formItemLayout = {
+ labelCol: {
+ xs: { span: 12 },
+ sm: { span: 5 }
+ },
+ wrapperCol: {
+ xs: { span: 24 },
+ sm: { span: 12 }
+ }
+ };
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
- return (
-
- );
+ return (
+
+ );
}
diff --git a/client/src/components/audit-trail-values/audit-trail-values.component.jsx b/client/src/components/audit-trail-values/audit-trail-values.component.jsx
index 3838a60dd..b3c531927 100644
--- a/client/src/components/audit-trail-values/audit-trail-values.component.jsx
+++ b/client/src/components/audit-trail-values/audit-trail-values.component.jsx
@@ -1,30 +1,30 @@
import React from "react";
-import {List} from "antd";
+import { List } from "antd";
import Icon from "@ant-design/icons";
-import {FaArrowRight} from "react-icons/fa";
+import { FaArrowRight } from "react-icons/fa";
-export default function AuditTrailValuesComponent({oldV, newV}) {
- if (!oldV && !newV) return
;
-
- if (!oldV && newV)
- return (
-
- {Object.keys(newV).map((key, idx) => (
-
- {key}: {JSON.stringify(newV[key])}
-
- ))}
-
- );
+export default function AuditTrailValuesComponent({ oldV, newV }) {
+ if (!oldV && !newV) return
;
+ if (!oldV && newV)
return (
-
- {Object.keys(oldV).map((key, idx) => (
-
- {key}: {oldV[key]}
- {JSON.stringify(newV[key])}
-
- ))}
-
+
+ {Object.keys(newV).map((key, idx) => (
+
+ {key}: {JSON.stringify(newV[key])}
+
+ ))}
+
);
+
+ return (
+
+ {Object.keys(oldV).map((key, idx) => (
+
+ {key}: {oldV[key]}
+ {JSON.stringify(newV[key])}
+
+ ))}
+
+ );
}
diff --git a/client/src/components/barcode-popup/barcode-popup.component.jsx b/client/src/components/barcode-popup/barcode-popup.component.jsx
index 457de3b49..c6dbb4ee3 100644
--- a/client/src/components/barcode-popup/barcode-popup.component.jsx
+++ b/client/src/components/barcode-popup/barcode-popup.component.jsx
@@ -1,23 +1,15 @@
-import {Popover, Tag} from "antd";
+import { Popover, Tag } from "antd";
import React from "react";
import Barcode from "react-barcode";
-import {useTranslation} from "react-i18next";
+import { useTranslation } from "react-i18next";
-export default function BarcodePopupComponent({value, children}) {
- const {t} = useTranslation();
- return (
-
-
- }
- >
- {children ? children :
{t("general.labels.barcode")} }
-
-
- );
+export default function BarcodePopupComponent({ value, children }) {
+ const { t } = useTranslation();
+ return (
+
+
}>
+ {children ? children :
{t("general.labels.barcode")} }
+
+
+ );
}
diff --git a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx
index 7e2e43dd2..5eb8bb4fc 100644
--- a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx
+++ b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx
@@ -1,136 +1,128 @@
-import {Checkbox, Form, Skeleton, Typography} from "antd";
-import React, {useEffect} from "react";
-import {useTranslation} from "react-i18next";
+import { Checkbox, Form, Skeleton, Typography } from "antd";
+import React, { useEffect } from "react";
+import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-cm-returns-table.styles.scss";
-export default function BillCmdReturnsTableComponent({
- form,
- returnLoading,
- returnData,
- }) {
- const {t} = useTranslation();
+export default function BillCmdReturnsTableComponent({ form, returnLoading, returnData }) {
+ const { t } = useTranslation();
- useEffect(() => {
- if (returnData) {
- form.setFieldsValue({
- outstanding_returns: returnData.parts_order_lines,
- });
+ useEffect(() => {
+ if (returnData) {
+ form.setFieldsValue({
+ outstanding_returns: returnData.parts_order_lines
+ });
+ }
+ }, [returnData, form]);
+
+ return (
+
+ prev.jobid !== cur.jobid || prev.is_credit_memo !== cur.is_credit_memo || prev.vendorid !== cur.vendorid
+ }
+ noStyle
+ >
+ {() => {
+ const isReturn = form.getFieldValue("is_credit_memo");
+
+ if (!isReturn) {
+ return null;
}
- }, [returnData, form]);
- return (
-
- prev.jobid !== cur.jobid ||
- prev.is_credit_memo !== cur.is_credit_memo ||
- prev.vendorid !== cur.vendorid
- }
- noStyle
- >
- {() => {
- const isReturn = form.getFieldValue("is_credit_memo");
+ if (returnLoading) return ;
- if (!isReturn) {
- return null;
- }
+ return (
+
+ {(fields, { add, remove, move }) => {
+ return (
+ <>
+ {t("bills.labels.creditsnotreceived")}
+
+
+
+ {t("parts_orders.fields.line_desc")}
+ {t("parts_orders.fields.part_type")}
+ {t("parts_orders.fields.quantity")}
+ {t("parts_orders.fields.act_price")}
+ {t("parts_orders.fields.cost")}
+ {t("parts_orders.labels.mark_as_received")}
+
+
+
+ {fields.map((field, index) => (
+
+
+
+
+
+
- if (returnLoading) return ;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- return (
-
- {(fields, {add, remove, move}) => {
- return (
- <>
-
- {t("bills.labels.creditsnotreceived")}
-
-
-
-
- {t("parts_orders.fields.line_desc")}
- {t("parts_orders.fields.part_type")}
- {t("parts_orders.fields.quantity")}
- {t("parts_orders.fields.act_price")}
- {t("parts_orders.fields.cost")}
- {t("parts_orders.labels.mark_as_received")}
-
-
-
- {fields.map((field, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
- >
- );
- }}
-
- );
+
+
+
+
+
+
+ ))}
+
+
+ >
+ );
}}
-
- );
+
+ );
+ }}
+
+ );
}
diff --git a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss
index 2b1a11fc7..743ef7ac0 100644
--- a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss
+++ b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss
@@ -16,4 +16,4 @@
tr:hover {
background-color: #f5f5f5;
}
-}
\ No newline at end of file
+}
diff --git a/client/src/components/bill-delete-button/bill-delete-button.component.jsx b/client/src/components/bill-delete-button/bill-delete-button.component.jsx
index 13eb94527..55c9a9072 100644
--- a/client/src/components/bill-delete-button/bill-delete-button.component.jsx
+++ b/client/src/components/bill-delete-button/bill-delete-button.component.jsx
@@ -1,97 +1,88 @@
-import {DeleteFilled} from "@ant-design/icons";
-import {useMutation} from "@apollo/client";
-import {Button, notification, Popconfirm} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {DELETE_BILL} from "../../graphql/bills.queries";
+import { DeleteFilled } from "@ant-design/icons";
+import { useMutation } from "@apollo/client";
+import { Button, notification, Popconfirm } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { DELETE_BILL } from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
-import {insertAuditTrail} from "../../redux/application/application.actions";
+import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
- insertAuditTrail: ({ jobid, operation, type }) =>
- dispatch(insertAuditTrail({ jobid, operation, type })),
+ insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
- const [loading, setLoading] = useState(false);
- const { t } = useTranslation();
- const [deleteBill] = useMutation(DELETE_BILL);
+ const [loading, setLoading] = useState(false);
+ const { t } = useTranslation();
+ const [deleteBill] = useMutation(DELETE_BILL);
- const handleDelete = async () => {
- setLoading(true);
- const result = await deleteBill({
- variables: {billId: bill.id},
- update(cache, {errors}) {
- if (errors) return;
- cache.modify({
- fields: {
- bills(existingBills, {readField}) {
- return existingBills.filter(
- (billref) => bill.id !== readField("id", billref)
- );
- },
- search_bills(existingBills, {readField}) {
- return existingBills.filter(
- (billref) => bill.id !== readField("id", billref)
- );
- },
- },
- });
+ const handleDelete = async () => {
+ setLoading(true);
+ const result = await deleteBill({
+ variables: { billId: bill.id },
+ update(cache, { errors }) {
+ if (errors) return;
+ cache.modify({
+ fields: {
+ bills(existingBills, { readField }) {
+ return existingBills.filter((billref) => bill.id !== readField("id", billref));
},
+ search_bills(existingBills, { readField }) {
+ return existingBills.filter((billref) => bill.id !== readField("id", billref));
+ }
+ }
});
+ }
+ });
- if (!!!result.errors) {
- notification["success"]({ message: t("bills.successes.deleted") });
- insertAuditTrail({
- jobid: jobid,
- operation: AuditTrailMapping.billdeleted(bill.invoice_number),
- type: "billdeleted",
+ if (!!!result.errors) {
+ notification["success"]({ message: t("bills.successes.deleted") });
+ insertAuditTrail({
+ jobid: jobid,
+ operation: AuditTrailMapping.billdeleted(bill.invoice_number),
+ type: "billdeleted"
});
- if (callback && typeof callback === "function") callback(bill.id);
- } else {
- //Check if it's an fkey violation.
- const error = JSON.stringify(result.errors);
+ if (callback && typeof callback === "function") callback(bill.id);
+ } else {
+ //Check if it's an fkey violation.
+ const error = JSON.stringify(result.errors);
- if (error.toLowerCase().includes("inventory_billid_fkey")) {
- notification["error"]({
- message: t("bills.errors.deleting", {
- error: t("bills.errors.existinginventoryline"),
- }),
- });
- } else {
- notification["error"]({
- message: t("bills.errors.deleting", {
- error: JSON.stringify(result.errors),
- }),
- });
- }
- }
+ if (error.toLowerCase().includes("inventory_billid_fkey")) {
+ notification["error"]({
+ message: t("bills.errors.deleting", {
+ error: t("bills.errors.existinginventoryline")
+ })
+ });
+ } else {
+ notification["error"]({
+ message: t("bills.errors.deleting", {
+ error: JSON.stringify(result.errors)
+ })
+ });
+ }
+ }
- setLoading(false);
- };
+ setLoading(false);
+ };
- return (
- >}>
-
-
-
-
-
-
- );
+ return (
+ >}>
+
+
+
+
+
+
+ );
}
diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx
index bc763be77..f5ce3d3d4 100644
--- a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx
+++ b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx
@@ -1,17 +1,17 @@
-import {useMutation, useQuery} from "@apollo/client";
-import {Button, Divider, Form, Popconfirm, Space} from "antd";
+import { useMutation, useQuery } from "@apollo/client";
+import { Button, Divider, Form, Popconfirm, Space } from "antd";
import dayjs from "../../utils/day";
import queryString from "query-string";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {useLocation} from "react-router-dom";
-import {createStructuredSelector} from "reselect";
-import {DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE} from "../../graphql/bill-lines.queries";
-import {QUERY_BILL_BY_PK, UPDATE_BILL} from "../../graphql/bills.queries";
-import {insertAuditTrail} from "../../redux/application/application.actions";
-import {setModalContext} from "../../redux/modals/modals.actions";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { useLocation } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, UPDATE_BILL_LINE } from "../../graphql/bill-lines.queries";
+import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
+import { insertAuditTrail } from "../../redux/application/application.actions";
+import { setModalContext } from "../../redux/modals/modals.actions";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import AlertComponent from "../alert/alert.component";
import BillFormContainer from "../bill-form/bill-form.container";
@@ -22,227 +22,212 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component";
-import {PageHeader} from "@ant-design/pro-layout";
+import { PageHeader } from "@ant-design/pro-layout";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- setPartsOrderContext: (context) =>
- dispatch(setModalContext({context: context, modal: "partsOrder"})),
- insertAuditTrail: ({jobid, operation, type}) =>
- dispatch(insertAuditTrail({jobid, operation, type })),
+ setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
+ insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(BillDetailEditcontainer);
+export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditcontainer);
-export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
- const search = queryString.parse(useLocation().search);
+export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail, bodyshop }) {
+ const search = queryString.parse(useLocation().search);
- const {t} = useTranslation();
- const [form] = Form.useForm();
- const [open, setOpen] = useState(false);
- const [updateLoading, setUpdateLoading] = useState(false);
- const [update_bill] = useMutation(UPDATE_BILL);
- const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
- const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
- const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
+ const { t } = useTranslation();
+ const [form] = Form.useForm();
+ const [open, setOpen] = useState(false);
+ const [updateLoading, setUpdateLoading] = useState(false);
+ const [update_bill] = useMutation(UPDATE_BILL);
+ const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
+ const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
+ const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
- const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
- variables: {billid: search.billid},
- skip: !!!search.billid,
- fetchPolicy: "network-only",
- nextFetchPolicy: "network-only",
- });
+ const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
+ variables: { billid: search.billid },
+ skip: !!!search.billid,
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
- // ... rest of the code remains the same
+ // ... rest of the code remains the same
- const handleSave = () => {
- //It's got a previously deducted bill line!
- if (
- data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
- form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
- 0
- )
- setOpen(true);
- else {
- form.submit();
- }
- };
+ const handleSave = () => {
+ //It's got a previously deducted bill line!
+ if (
+ data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
+ form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > 0
+ )
+ setOpen(true);
+ else {
+ form.submit();
+ }
+ };
- const handleFinish = async (values) => {
- setUpdateLoading(true);
- //let adjustmentsToInsert = {};
+ const handleFinish = async (values) => {
+ setUpdateLoading(true);
+ //let adjustmentsToInsert = {};
- const {billlines, upload, ...bill} = values;
- const updates = [];
- updates.push(
- update_bill({
- variables: {billId: search.billid, bill: bill},
- })
- );
-
- billlines.forEach((l) => {
- delete l.selected;
- });
-
- //Find bill lines that were deleted.
- const deletedJobLines = [];
-
- data.bills_by_pk.billlines.forEach((a) => {
- const matchingRecord = billlines.find((b) => b.id === a.id);
- if (!matchingRecord) {
- deletedJobLines.push(a);
- }
- });
-
- deletedJobLines.forEach((d) => {
- updates.push(deleteBillLine({variables: {id: d.id}}));
- });
-
- billlines.forEach((billline) => {
- const {deductedfromlbr, inventories, jobline, ...il} = billline;
- delete il.__typename;
-
- if (il.id) {
- updates.push(
- updateBillLine({
- variables: {
- billLineId: il.id,
- billLine: {
- ...il,
- deductedfromlbr: deductedfromlbr,
- joblineid: il.joblineid === "noline" ? null : il.joblineid,
- },
- },
- })
- );
- } else {
- //It's a new line, have to insert it.
- updates.push(
- insertBillLine({
- variables: {
- billLines: [
- {
- ...il,
- deductedfromlbr: deductedfromlbr,
- billid: search.billid,
- joblineid: il.joblineid === "noline" ? null : il.joblineid,
- },
- ],
- },
- })
- );
- }
- });
-
- await Promise.all(updates);
-
- insertAuditTrail({
- jobid: bill.jobid,
- billid: search.billid,
- operation: AuditTrailMapping.billupdated(bill.invoice_number),
- type: "billupdated",
- });
-
- await refetch();
- form.setFieldsValue(transformData(data));
- form.resetFields();
- setOpen(false);
- setUpdateLoading(false);
- };
-
- if (error) return ;
- if (!search.billid) return <>>; //{t("bills.labels.noneselected")}
;
-
- const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
-
- return (
- <>
- {loading && }
- {data && (
- <>
-
-
-
- form.submit()}
- onCancel={() => setOpen(false)}
- okButtonProps={{loading: updateLoading}}
- title={t("bills.labels.editadjwarning")}
- >
-
- {t("general.actions.save")}
-
-
-
-
-
- }
- />
-
- >
- )}
- >
+ const { billlines, upload, ...bill } = values;
+ const updates = [];
+ updates.push(
+ update_bill({
+ variables: { billId: search.billid, bill: bill }
+ })
);
+
+ billlines.forEach((l) => {
+ delete l.selected;
+ });
+
+ //Find bill lines that were deleted.
+ const deletedJobLines = [];
+
+ data.bills_by_pk.billlines.forEach((a) => {
+ const matchingRecord = billlines.find((b) => b.id === a.id);
+ if (!matchingRecord) {
+ deletedJobLines.push(a);
+ }
+ });
+
+ deletedJobLines.forEach((d) => {
+ updates.push(deleteBillLine({ variables: { id: d.id } }));
+ });
+
+ billlines.forEach((billline) => {
+ const { deductedfromlbr, inventories, jobline, ...il } = billline;
+ delete il.__typename;
+
+ if (il.id) {
+ updates.push(
+ updateBillLine({
+ variables: {
+ billLineId: il.id,
+ billLine: {
+ ...il,
+ deductedfromlbr: deductedfromlbr,
+ joblineid: il.joblineid === "noline" ? null : il.joblineid
+ }
+ }
+ })
+ );
+ } else {
+ //It's a new line, have to insert it.
+ updates.push(
+ insertBillLine({
+ variables: {
+ billLines: [
+ {
+ ...il,
+ deductedfromlbr: deductedfromlbr,
+ billid: search.billid,
+ joblineid: il.joblineid === "noline" ? null : il.joblineid
+ }
+ ]
+ }
+ })
+ );
+ }
+ });
+
+ await Promise.all(updates);
+
+ insertAuditTrail({
+ jobid: bill.jobid,
+ billid: search.billid,
+ operation: AuditTrailMapping.billupdated(bill.invoice_number),
+ type: "billupdated"
+ });
+
+ await refetch();
+ form.setFieldsValue(transformData(data));
+ form.resetFields();
+ setOpen(false);
+ setUpdateLoading(false);
+ };
+
+ if (error) return ;
+ if (!search.billid) return <>>; //{t("bills.labels.noneselected")}
;
+
+ const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
+
+ return (
+ <>
+ {loading && }
+ {data && (
+ <>
+
+
+
+ form.submit()}
+ onCancel={() => setOpen(false)}
+ okButtonProps={{ loading: updateLoading }}
+ title={t("bills.labels.editadjwarning")}
+ >
+
+ {t("general.actions.save")}
+
+
+
+
+
+ }
+ />
+
+ >
+ )}
+ >
+ );
}
const transformData = (data) => {
- return data
- ? {
- ...data.bills_by_pk,
+ return data
+ ? {
+ ...data.bills_by_pk,
- billlines: data.bills_by_pk.billlines.map((i) => {
- return {
- ...i,
- joblineid: !!i.joblineid ? i.joblineid : "noline",
- applicable_taxes: {
- federal:
- (i.applicable_taxes && i.applicable_taxes.federal) || false,
- state: (i.applicable_taxes && i.applicable_taxes.state) || false,
- local: (i.applicable_taxes && i.applicable_taxes.local) || false,
- },
- };
- }),
- date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
- }
- : {};
+ billlines: data.bills_by_pk.billlines.map((i) => {
+ return {
+ ...i,
+ joblineid: !!i.joblineid ? i.joblineid : "noline",
+ applicable_taxes: {
+ federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
+ state: (i.applicable_taxes && i.applicable_taxes.state) || false,
+ local: (i.applicable_taxes && i.applicable_taxes.local) || false
+ }
+ };
+ }),
+ date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null
+ }
+ : {};
};
diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx
index 62ac07258..44833c1b5 100644
--- a/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx
+++ b/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx
@@ -1,189 +1,168 @@
-import {Button, Checkbox, Form, Modal} from "antd";
+import { Button, Checkbox, Form, Modal } from "antd";
import queryString from "query-string";
-import React, {useEffect, useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {useLocation, useNavigate} from "react-router-dom";
-import {createStructuredSelector} from "reselect";
-import {insertAuditTrail} from "../../redux/application/application.actions";
-import {setModalContext} from "../../redux/modals/modals.actions";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { useLocation, useNavigate } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { insertAuditTrail } from "../../redux/application/application.actions";
+import { setModalContext } from "../../redux/modals/modals.actions";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- setPartsOrderContext: (context) =>
- dispatch(setModalContext({context: context, modal: "partsOrder"})),
- insertAuditTrail: ({jobid, operation, type}) =>
- dispatch(insertAuditTrail({jobid, operation, type })),
+ setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
+ insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(BillDetailEditReturn);
+export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
-export function BillDetailEditReturn({
- setPartsOrderContext,
- insertAuditTrail,
- bodyshop,
- data,
- disabled,
- }) {
- const search = queryString.parse(useLocation().search);
- const history = useNavigate();
- const {t} = useTranslation();
- const [form] = Form.useForm();
- const [open, setOpen] = useState(false);
+export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, bodyshop, data, disabled }) {
+ const search = queryString.parse(useLocation().search);
+ const history = useNavigate();
+ const { t } = useTranslation();
+ const [form] = Form.useForm();
+ const [open, setOpen] = useState(false);
- const handleFinish = ({billlines}) => {
- const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
+ const handleFinish = ({ billlines }) => {
+ const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
- setPartsOrderContext({
- actions: {},
- context: {
- jobId: data.bills_by_pk.jobid,
- vendorId: data.bills_by_pk.vendorid,
- returnFromBill: data.bills_by_pk.id,
- invoiceNumber: data.bills_by_pk.invoice_number,
- linesToOrder: data.bills_by_pk.billlines
- .filter((l) => selectedLines.includes(l.id))
- .map((i) => {
- return {
- line_desc: i.line_desc,
- // db_price: i.actual_price,
- act_price: i.actual_price,
- cost: i.actual_cost,
- quantity: i.quantity,
- joblineid: i.joblineid,
- oem_partno: i.jobline && i.jobline.oem_partno,
- part_type: i.jobline && i.jobline.part_type,
- };
- }),
- isReturn: true,
- },
- });
- delete search.billid;
+ setPartsOrderContext({
+ actions: {},
+ context: {
+ jobId: data.bills_by_pk.jobid,
+ vendorId: data.bills_by_pk.vendorid,
+ returnFromBill: data.bills_by_pk.id,
+ invoiceNumber: data.bills_by_pk.invoice_number,
+ linesToOrder: data.bills_by_pk.billlines
+ .filter((l) => selectedLines.includes(l.id))
+ .map((i) => {
+ return {
+ line_desc: i.line_desc,
+ // db_price: i.actual_price,
+ act_price: i.actual_price,
+ cost: i.actual_cost,
+ quantity: i.quantity,
+ joblineid: i.joblineid,
+ oem_partno: i.jobline && i.jobline.oem_partno,
+ part_type: i.jobline && i.jobline.part_type
+ };
+ }),
+ isReturn: true
+ }
+ });
+ delete search.billid;
- history({search: queryString.stringify(search)});
- setOpen(false);
- };
- useEffect(() => {
- if (open === false) form.resetFields();
- }, [open, form]);
+ history({ search: queryString.stringify(search) });
+ setOpen(false);
+ };
+ useEffect(() => {
+ if (open === false) form.resetFields();
+ }, [open, form]);
- return (
- <>
- setOpen(false)}
- destroyOnClose
- title={t("bills.actions.return")}
- onOk={() => form.submit()}
- >
-
- {(fields, {add, remove, move}) => {
- return (
-
-
-
-
- {
- form.setFieldsValue({
- billlines: form
- .getFieldsValue()
- .billlines.map((b) => ({
- ...b,
- selected: e.target.checked,
- })),
- });
- }}
- />
-
- {t("billlines.fields.line_desc")}
- {t("billlines.fields.quantity")}
- {t("billlines.fields.actual_price")}
- {t("billlines.fields.actual_cost")}
-
-
-
- {fields.map((field, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
- );
- }}
-
-
-
- {
- setOpen(true);
- }}
- >
- {t("bills.actions.return")}
-
- >
- );
+ return (
+ <>
+ setOpen(false)}
+ destroyOnClose
+ title={t("bills.actions.return")}
+ onOk={() => form.submit()}
+ >
+
+ {(fields, { add, remove, move }) => {
+ return (
+
+
+
+
+ {
+ form.setFieldsValue({
+ billlines: form.getFieldsValue().billlines.map((b) => ({
+ ...b,
+ selected: e.target.checked
+ }))
+ });
+ }}
+ />
+
+ {t("billlines.fields.line_desc")}
+ {t("billlines.fields.quantity")}
+ {t("billlines.fields.actual_price")}
+ {t("billlines.fields.actual_cost")}
+
+
+
+ {fields.map((field, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+ }}
+
+
+
+ {
+ setOpen(true);
+ }}
+ >
+ {t("bills.actions.return")}
+
+ >
+ );
}
diff --git a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx
index ab7e9bc20..3cebb3e07 100644
--- a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx
+++ b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx
@@ -1,40 +1,38 @@
-import {Drawer, Grid} from "antd";
+import { Drawer, Grid } from "antd";
import queryString from "query-string";
import React from "react";
-import {useLocation, useNavigate} from "react-router-dom";
+import { useLocation, useNavigate } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component";
export default function BillDetailEditcontainer() {
- const search = queryString.parse(useLocation().search);
- const history = useNavigate();
+ const search = queryString.parse(useLocation().search);
+ const history = useNavigate();
- const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
- .filter((screen) => !!screen[1])
- .slice(-1)[0];
+ const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
+ .filter((screen) => !!screen[1])
+ .slice(-1)[0];
- const bpoints = {
- xs: "100%",
- sm: "100%",
- md: "100%",
- lg: "100%",
- xl: "90%",
- xxl: "90%",
- };
- const drawerPercentage = selectedBreakpoint
- ? bpoints[selectedBreakpoint[0]]
- : "100%";
+ const bpoints = {
+ xs: "100%",
+ sm: "100%",
+ md: "100%",
+ lg: "100%",
+ xl: "90%",
+ xxl: "90%"
+ };
+ const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
- return (
- {
- delete search.billid;
- history({search: queryString.stringify(search)});
- }}
- destroyOnClose
- open={search.billid}
- >
-
-
- );
+ return (
+ {
+ delete search.billid;
+ history({ search: queryString.stringify(search) });
+ }}
+ destroyOnClose
+ open={search.billid}
+ >
+
+
+ );
}
diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
index c93a4a0be..18d6db174 100644
--- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
+++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
@@ -1,471 +1,426 @@
-import {useApolloClient, useMutation} from "@apollo/client";
-import {useSplitTreatments} from "@splitsoftware/splitio-react";
-import {Button, Checkbox, Form, Modal, notification, Space} from "antd";
+import { useApolloClient, useMutation } from "@apollo/client";
+import { useSplitTreatments } from "@splitsoftware/splitio-react";
+import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
import _ from "lodash";
-import React, {useEffect, useMemo, useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {INSERT_NEW_BILL} from "../../graphql/bills.queries";
-import {UPDATE_INVENTORY_LINES} from "../../graphql/inventory.queries";
-import {UPDATE_JOB_LINE} from "../../graphql/jobs-lines.queries";
-import {QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB,} from "../../graphql/jobs.queries";
-import {MUTATION_MARK_RETURN_RECEIVED} from "../../graphql/parts-orders.queries";
-import {insertAuditTrail} from "../../redux/application/application.actions";
-import {toggleModalVisible} from "../../redux/modals/modals.actions";
-import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
-import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
+import React, { useEffect, useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
+import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
+import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
+import { QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB } from "../../graphql/jobs.queries";
+import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
+import { insertAuditTrail } from "../../redux/application/application.actions";
+import { toggleModalVisible } from "../../redux/modals/modals.actions";
+import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
+import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
-import {GenerateDocument} from "../../utils/RenderTemplate";
-import {TemplateList} from "../../utils/TemplateConstants";
+import { GenerateDocument } from "../../utils/RenderTemplate";
+import { TemplateList } from "../../utils/TemplateConstants";
import confirmDialog from "../../utils/asyncConfirm";
import useLocalStorage from "../../utils/useLocalStorage";
import BillFormContainer from "../bill-form/bill-form.container";
-import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility";
-import {handleUpload as handleLocalUpload} from "../documents-local-upload/documents-local-upload.utility";
-import {handleUpload} from "../documents-upload/documents-upload.utility";
+import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
+import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
+import { handleUpload } from "../documents-upload/documents-upload.utility";
const mapStateToProps = createStructuredSelector({
- billEnterModal: selectBillEnterModal,
- bodyshop: selectBodyshop,
- currentUser: selectCurrentUser,
+ billEnterModal: selectBillEnterModal,
+ bodyshop: selectBodyshop,
+ currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
- toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
- insertAuditTrail: ({jobid, billid, operation, type}) =>
- dispatch(insertAuditTrail({jobid, billid, operation, type })),
+ toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
+ insertAuditTrail: ({ jobid, billid, operation, type }) =>
+ dispatch(insertAuditTrail({ jobid, billid, operation, type }))
});
const Templates = TemplateList("job_special");
-function BillEnterModalContainer({
- billEnterModal,
- toggleModalVisible,
- bodyshop,
- currentUser,
- insertAuditTrail,
- }) {
- const [form] = Form.useForm();
- const {t} = useTranslation();
- const [enterAgain, setEnterAgain] = useState(false);
- const [insertBill] = useMutation(INSERT_NEW_BILL);
- const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
- const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
- const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
- const [loading, setLoading] = useState(false);
- const client = useApolloClient();
- const [generateLabel, setGenerateLabel] = useLocalStorage(
- "enter_bill_generate_label",
- false
- );
+function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop, currentUser, insertAuditTrail }) {
+ const [form] = Form.useForm();
+ const { t } = useTranslation();
+ const [enterAgain, setEnterAgain] = useState(false);
+ const [insertBill] = useMutation(INSERT_NEW_BILL);
+ const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
+ const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
+ const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
+ const [loading, setLoading] = useState(false);
+ const client = useApolloClient();
+ const [generateLabel, setGenerateLabel] = useLocalStorage("enter_bill_generate_label", false);
- const {treatments: {Enhanced_Payroll}} = useSplitTreatments({
- attributes: {},
- names: ["Enhanced_Payroll"],
- splitKey: bodyshop.imexshopid,
+ const {
+ treatments: { Enhanced_Payroll }
+ } = useSplitTreatments({
+ attributes: {},
+ names: ["Enhanced_Payroll"],
+ splitKey: bodyshop.imexshopid
+ });
+
+ const formValues = useMemo(() => {
+ return {
+ ...billEnterModal.context.bill,
+ //Added as a part of IO-2436 for capturing parts price changes.
+ billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({
+ ...line,
+ original_actual_price: line.actual_price
+ })),
+ jobid: (billEnterModal.context.job && billEnterModal.context.job.id) || null,
+ federal_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) || 0,
+ state_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) || 0,
+ local_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) || 0
+ };
+ }, [billEnterModal, bodyshop]);
+
+ const handleFinish = async (values) => {
+ let totals = CalculateBillTotal(values);
+ if (totals.discrepancy.getAmount() !== 0) {
+ if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
+ return;
+ }
+ }
+
+ setLoading(true);
+ const { upload, location, outstanding_returns, inventory, federal_tax_exempt, ...remainingValues } = values;
+
+ let adjustmentsToInsert = {};
+ let payrollAdjustmentsToInsert = [];
+
+ const r1 = await insertBill({
+ variables: {
+ bill: [
+ {
+ ...remainingValues,
+ billlines: {
+ data:
+ remainingValues.billlines &&
+ remainingValues.billlines.map((i) => {
+ const {
+ deductedfromlbr,
+ lbr_adjustment,
+ location: lineLocation,
+ part_type,
+ create_ppc,
+ original_actual_price,
+ ...restI
+ } = i;
+
+ if (Enhanced_Payroll.treatment === "on") {
+ if (
+ deductedfromlbr &&
+ true //payroll is on
+ ) {
+ payrollAdjustmentsToInsert.push({
+ id: i.joblineid,
+ convertedtolbr: true,
+ convertedtolbr_data: {
+ mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1,
+ mod_lbr_ty: lbr_adjustment.mod_lbr_ty
+ }
+ });
+ }
+ } else {
+ if (deductedfromlbr) {
+ adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
+ (adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
+ restI.actual_price / lbr_adjustment.rate;
+ }
+ }
+
+ return {
+ ...restI,
+ deductedfromlbr: deductedfromlbr,
+ lbr_adjustment,
+ joblineid: i.joblineid === "noline" ? null : i.joblineid,
+ applicable_taxes: {
+ federal: (i.applicable_taxes && i.applicable_taxes.federal) || false,
+ state: (i.applicable_taxes && i.applicable_taxes.state) || false,
+ local: (i.applicable_taxes && i.applicable_taxes.local) || false
+ }
+ };
+ })
+ }
+ }
+ ]
+ },
+ refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
+ awaitRefetchQueries: true
});
- const formValues = useMemo(() => {
- return {
- ...billEnterModal.context.bill,
- //Added as a part of IO-2436 for capturing parts price changes.
- billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({
- ...line,
- original_actual_price: line.actual_price,
- })),
- jobid:
- (billEnterModal.context.job && billEnterModal.context.job.id) || null,
- federal_tax_rate:
- (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) ||
- 0,
- state_tax_rate:
- (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) ||
- 0,
- local_tax_rate:
- (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) ||
- 0,
- };
- }, [billEnterModal, bodyshop]);
-
- const handleFinish = async (values) => {
- let totals = CalculateBillTotal(values);
- if (totals.discrepancy.getAmount() !== 0) {
- if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
- return;
+ await Promise.all(
+ payrollAdjustmentsToInsert.map((li) => {
+ return updateJobLines({
+ variables: {
+ lineId: li.id,
+ line: {
+ convertedtolbr: li.convertedtolbr,
+ convertedtolbr_data: li.convertedtolbr_data
}
- }
-
- setLoading(true);
- const {
- upload,
- location,
- outstanding_returns,
- inventory,
- federal_tax_exempt,
- ...remainingValues
- } = values;
-
- let adjustmentsToInsert = {};
- let payrollAdjustmentsToInsert = [];
-
- const r1 = await insertBill({
- variables: {
- bill: [
- {
- ...remainingValues,
- billlines: {
- data:
- remainingValues.billlines &&
- remainingValues.billlines.map((i) => {
- const {
- deductedfromlbr,
- lbr_adjustment,
- location: lineLocation,
- part_type,
- create_ppc,
- original_actual_price,
- ...restI
- } = i;
-
- if (Enhanced_Payroll.treatment === "on") {
- if (
- deductedfromlbr &&
- true //payroll is on
- ) {
- payrollAdjustmentsToInsert.push({
- id: i.joblineid,
- convertedtolbr: true,
- convertedtolbr_data: {
- mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1,
- mod_lbr_ty: lbr_adjustment.mod_lbr_ty,
- },
- });
- }
- } else {
- if (deductedfromlbr) {
- adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] =
- (adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) -
- restI.actual_price / lbr_adjustment.rate;
- }
- }
-
- return {
- ...restI,
- deductedfromlbr: deductedfromlbr,
- lbr_adjustment,
- joblineid: i.joblineid === "noline" ? null : i.joblineid,
- applicable_taxes: {
- federal:
- (i.applicable_taxes && i.applicable_taxes.federal) ||
- false,
- state:
- (i.applicable_taxes && i.applicable_taxes.state) ||
- false,
- local:
- (i.applicable_taxes && i.applicable_taxes.local) ||
- false,
- },
- };
- }),
- },
- },
- ],
- },
- refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
- awaitRefetchQueries: true
+ }
});
+ })
+ );
- await Promise.all(
- payrollAdjustmentsToInsert.map((li) => {
- return updateJobLines({
- variables: {
- lineId: li.id,
- line: {
- convertedtolbr: li.convertedtolbr,
- convertedtolbr_data: li.convertedtolbr_data,
- },
- },
- });
- })
- );
-
- const adjKeys = Object.keys(adjustmentsToInsert);
- if (adjKeys.length > 0) {
- //Query the adjustments, merge, and update them.
- const existingAdjustments = await client.query({
- query: QUERY_JOB_LBR_ADJUSTMENTS,
- variables: {
- id: values.jobid,
- },
- });
-
- const newAdjustments = _.cloneDeep(
- existingAdjustments.data.jobs_by_pk.lbr_adjustments
- );
-
- adjKeys.forEach((key) => {
- newAdjustments[key] =
- (newAdjustments[key] || 0) + adjustmentsToInsert[key];
-
- insertAuditTrail({
- jobid: values.jobid,
- operation: AuditTrailMapping.jobmodifylbradj({
- mod_lbr_ty: key,
- hours: adjustmentsToInsert[key].toFixed(1),
- }),
- type: "jobmodifylbradj",});
- });
-
- const jobUpdate = client.mutate({
- mutation: UPDATE_JOB,
- variables: {
- jobId: values.jobid,
- job: {lbr_adjustments: newAdjustments},
- },
- });
- if (!!jobUpdate.errors) {
- notification["error"]({
- message: t("jobs.errors.saving", {
- message: JSON.stringify(jobUpdate.errors),
- }),
- });
- return;
- }
+ const adjKeys = Object.keys(adjustmentsToInsert);
+ if (adjKeys.length > 0) {
+ //Query the adjustments, merge, and update them.
+ const existingAdjustments = await client.query({
+ query: QUERY_JOB_LBR_ADJUSTMENTS,
+ variables: {
+ id: values.jobid
}
+ });
- const markPolReceived =
- outstanding_returns &&
- outstanding_returns.filter((o) => o.cm_received === true);
+ const newAdjustments = _.cloneDeep(existingAdjustments.data.jobs_by_pk.lbr_adjustments);
- if (markPolReceived && markPolReceived.length > 0) {
- const r2 = await updatePartsOrderLines({
- variables: {partsLineIds: markPolReceived.map((p) => p.id)},
- refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID" ],
- });
- if (!!r2.errors) {
- setLoading(false);
- setEnterAgain(false);
- notification["error"]({
- message: t("parts_orders.errors.updating", {
- message: JSON.stringify(r2.errors),
- }),
- });
- }
- }
-
- if (!!r1.errors) {
- setLoading(false);
- setEnterAgain(false);
- notification["error"]({
- message: t("bills.errors.creating", {
- message: JSON.stringify(r1.errors),
- }),
- });
- }
-
- const billId = r1.data.insert_bills.returning[0].id;
- const markInventoryConsumed =
- inventory && inventory.filter((i) => i.consumefrominventory);
-
- if (markInventoryConsumed && markInventoryConsumed.length > 0) {
- const r2 = await updateInventoryLines({
- variables: {
- InventoryIds: markInventoryConsumed.map((p) => p.id),
- consumedbybillid: billId,
- },
- });
- if (!!r2.errors) {
- setLoading(false);
- setEnterAgain(false);
- notification["error"]({
- message: t("inventory.errors.updating", {
- message: JSON.stringify(r2.errors),
- }),
- });
- }
- }
- //If it's not a credit memo, update the statuses.
-
- if (!values.is_credit_memo) {
- await Promise.all(
- remainingValues.billlines
- .filter((il) => il.joblineid !== "noline")
- .map((li) => {
- return updateJobLines({
- variables: {
- lineId: li.joblineid,
- line: {
- location: li.location || location,
- status:
- bodyshop.md_order_statuses.default_received || "Received*",
- //Added parts price changes.
- ...(li.create_ppc &&
- li.original_actual_price !== li.actual_price
- ? {
- act_price_before_ppc: li.original_actual_price,
- act_price: li.actual_price,
- }
- : {}),
- },
- },
- });
- })
- );
- }
-
- /////////////////////////
- if (upload && upload.length > 0) {
- //insert Each of the documents?
-
- if (bodyshop.uselocalmediaserver) {
- upload.forEach((u) => {
- handleLocalUpload({
- ev: {file: u.originFileObj},
- context: {
- jobid: values.jobid,
- invoice_number: remainingValues.invoice_number,
- vendorid: remainingValues.vendorid,
- },
- });
- });
- } else {
- upload.forEach((u) => {
- handleUpload(
- {file: u.originFileObj},
- {
- bodyshop: bodyshop,
- uploaded_by: currentUser.email,
- jobId: values.jobid,
- billId: billId,
- tagsArray: null,
- callback: null,
- }
- );
- });
- }
- }
- ///////////////////////////
- setLoading(false);
- notification["success"]({
- message: t("bills.successes.created"),
- });
-
- if (generateLabel) {
- GenerateDocument(
- {
- name: Templates.parts_invoice_label_single.key,
- variables: {
- id: billId,
- },
- },
- {},
- "p"
- );
- }
-
- if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
+ adjKeys.forEach((key) => {
+ newAdjustments[key] = (newAdjustments[key] || 0) + adjustmentsToInsert[key];
insertAuditTrail({
- jobid: values.jobid,
- billid: billId,
- operation: AuditTrailMapping.billposted(
- r1.data.insert_bills.returning[0].invoice_number
- ),
- type: "billposted",
+ jobid: values.jobid,
+ operation: AuditTrailMapping.jobmodifylbradj({
+ mod_lbr_ty: key,
+ hours: adjustmentsToInsert[key].toFixed(1)
+ }),
+ type: "jobmodifylbradj"
+ });
+ });
+
+ const jobUpdate = client.mutate({
+ mutation: UPDATE_JOB,
+ variables: {
+ jobId: values.jobid,
+ job: { lbr_adjustments: newAdjustments }
+ }
+ });
+ if (!!jobUpdate.errors) {
+ notification["error"]({
+ message: t("jobs.errors.saving", {
+ message: JSON.stringify(jobUpdate.errors)
+ })
+ });
+ return;
+ }
+ }
+
+ const markPolReceived = outstanding_returns && outstanding_returns.filter((o) => o.cm_received === true);
+
+ if (markPolReceived && markPolReceived.length > 0) {
+ const r2 = await updatePartsOrderLines({
+ variables: { partsLineIds: markPolReceived.map((p) => p.id) },
+ refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"]
+ });
+ if (!!r2.errors) {
+ setLoading(false);
+ setEnterAgain(false);
+ notification["error"]({
+ message: t("parts_orders.errors.updating", {
+ message: JSON.stringify(r2.errors)
+ })
+ });
+ }
+ }
+
+ if (!!r1.errors) {
+ setLoading(false);
+ setEnterAgain(false);
+ notification["error"]({
+ message: t("bills.errors.creating", {
+ message: JSON.stringify(r1.errors)
+ })
+ });
+ }
+
+ const billId = r1.data.insert_bills.returning[0].id;
+ const markInventoryConsumed = inventory && inventory.filter((i) => i.consumefrominventory);
+
+ if (markInventoryConsumed && markInventoryConsumed.length > 0) {
+ const r2 = await updateInventoryLines({
+ variables: {
+ InventoryIds: markInventoryConsumed.map((p) => p.id),
+ consumedbybillid: billId
+ }
+ });
+ if (!!r2.errors) {
+ setLoading(false);
+ setEnterAgain(false);
+ notification["error"]({
+ message: t("inventory.errors.updating", {
+ message: JSON.stringify(r2.errors)
+ })
+ });
+ }
+ }
+ //If it's not a credit memo, update the statuses.
+
+ if (!values.is_credit_memo) {
+ await Promise.all(
+ remainingValues.billlines
+ .filter((il) => il.joblineid !== "noline")
+ .map((li) => {
+ return updateJobLines({
+ variables: {
+ lineId: li.joblineid,
+ line: {
+ location: li.location || location,
+ status: bodyshop.md_order_statuses.default_received || "Received*",
+ //Added parts price changes.
+ ...(li.create_ppc && li.original_actual_price !== li.actual_price
+ ? {
+ act_price_before_ppc: li.original_actual_price,
+ act_price: li.actual_price
+ }
+ : {})
+ }
+ }
+ });
+ })
+ );
+ }
+
+ /////////////////////////
+ if (upload && upload.length > 0) {
+ //insert Each of the documents?
+
+ if (bodyshop.uselocalmediaserver) {
+ upload.forEach((u) => {
+ handleLocalUpload({
+ ev: { file: u.originFileObj },
+ context: {
+ jobid: values.jobid,
+ invoice_number: remainingValues.invoice_number,
+ vendorid: remainingValues.vendorid
+ }
+ });
+ });
+ } else {
+ upload.forEach((u) => {
+ handleUpload(
+ { file: u.originFileObj },
+ {
+ bodyshop: bodyshop,
+ uploaded_by: currentUser.email,
+ jobId: values.jobid,
+ billId: billId,
+ tagsArray: null,
+ callback: null
+ }
+ );
+ });
+ }
+ }
+ ///////////////////////////
+ setLoading(false);
+ notification["success"]({
+ message: t("bills.successes.created")
});
- if (enterAgain) {
- form.resetFields();
- form.setFieldsValue({
- ...formValues,
- vendorid:values.vendorid,
- billlines: [],
- });
- // form.resetFields();
- } else {
- toggleModalVisible();
- }
- setEnterAgain(false);
- };
+ if (generateLabel) {
+ GenerateDocument(
+ {
+ name: Templates.parts_invoice_label_single.key,
+ variables: {
+ id: billId
+ }
+ },
+ {},
+ "p"
+ );
+ }
- const handleCancel = () => {
- const r = window.confirm(t("general.labels.cancel"));
- if (r === true) {
- toggleModalVisible();
- }
- };
+ if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
- useEffect(() => {
- if (enterAgain) form.submit();
- }, [enterAgain, form]);
+ insertAuditTrail({
+ jobid: values.jobid,
+ billid: billId,
+ operation: AuditTrailMapping.billposted(r1.data.insert_bills.returning[0].invoice_number),
+ type: "billposted"
+ });
- useEffect(() => {
- if (billEnterModal.open) {
- form.setFieldsValue(formValues);
- } else {
- form.resetFields();
- }
- }, [billEnterModal.open, form, formValues]);
+ if (enterAgain) {
+ form.resetFields();
+ form.setFieldsValue({
+ ...formValues,
+ vendorid: values.vendorid,
+ billlines: []
+ });
+ // form.resetFields();
+ } else {
+ toggleModalVisible();
+ }
+ setEnterAgain(false);
+ };
- return (
- form.submit()}
- onCancel={handleCancel}
- afterClose={() => {
- form.resetFields();
- setLoading(false);
- }}
- footer={
-
- setGenerateLabel(e.target.checked)}
- >
- {t("bills.labels.generatepartslabel")}
-
- {t("general.actions.cancel")}
- form.submit()}>
- {t("general.actions.save")}
-
- {billEnterModal.context && billEnterModal.context.id ? null : (
- {
- setEnterAgain(true);
- }}
- >
- {t("general.actions.saveandnew")}
-
- )}
-
- }
- destroyOnClose
- >
-
-
- );
+ {t("general.actions.saveandnew")}
+
+ )}
+
+ }
+ destroyOnClose
+ >
+
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(BillEnterModalContainer);
+export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalContainer);
diff --git a/client/src/components/bill-form-lines-extended/bill-form-lines-extended.component.jsx b/client/src/components/bill-form-lines-extended/bill-form-lines-extended.component.jsx
index ef54f443c..2cb93f328 100644
--- a/client/src/components/bill-form-lines-extended/bill-form-lines-extended.component.jsx
+++ b/client/src/components/bill-form-lines-extended/bill-form-lines-extended.component.jsx
@@ -1,136 +1,114 @@
-import {Form, Input, Table} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
+import { Form, Input, Table } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
-import {alphaSort} from "../../utils/sorters";
+import { alphaSort } from "../../utils/sorters";
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
-export default function BillFormLinesExtended({
- lineData,
- discount,
- form,
- responsibilityCenters,
- disabled,
- }) {
- const [search, setSearch] = useState("");
- const {t} = useTranslation();
- const columns = [
+export default function BillFormLinesExtended({ lineData, discount, form, responsibilityCenters, disabled }) {
+ const [search, setSearch] = useState("");
+ const { t } = useTranslation();
+ const columns = [
+ {
+ title: t("joblines.fields.line_desc"),
+ dataIndex: "line_desc",
+ key: "line_desc",
+ width: "10%",
+ sorter: (a, b) => alphaSort(a.line_desc, b.line_desc)
+ },
+ {
+ title: t("joblines.fields.oem_partno"),
+ dataIndex: "oem_partno",
+ key: "oem_partno",
+ width: "10%",
+ sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno)
+ },
+ {
+ title: t("joblines.fields.part_type"),
+ dataIndex: "part_type",
+ key: "part_type",
+ width: "10%",
+ filters: [
{
- title: t("joblines.fields.line_desc"),
- dataIndex: "line_desc",
- key: "line_desc",
- width: "10%",
- sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
+ text: t("jobs.labels.partsfilter"),
+ value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"]
},
{
- title: t("joblines.fields.oem_partno"),
- dataIndex: "oem_partno",
- key: "oem_partno",
- width: "10%",
- sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
+ text: t("joblines.fields.part_types.PAN"),
+ value: ["PAN", "PAP"]
},
{
- title: t("joblines.fields.part_type"),
- dataIndex: "part_type",
- key: "part_type",
- width: "10%",
- filters: [
- {
- text: t("jobs.labels.partsfilter"),
- value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
- },
- {
- text: t("joblines.fields.part_types.PAN"),
- value: ["PAN", "PAP"],
- },
- {
- text: t("joblines.fields.part_types.PAL"),
- value: ["PAL"],
- },
- {
- text: t("joblines.fields.part_types.PAA"),
- value: ["PAA"],
- },
- {
- text: t("joblines.fields.part_types.PAS"),
- value: ["PAS", "PASL"],
- },
- ],
- onFilter: (value, record) => value.includes(record.part_type),
- render: (text, record) =>
- record.part_type
- ? t(`joblines.fields.part_types.${record.part_type}`)
- : null,
+ text: t("joblines.fields.part_types.PAL"),
+ value: ["PAL"]
},
+ {
+ text: t("joblines.fields.part_types.PAA"),
+ value: ["PAA"]
+ },
+ {
+ text: t("joblines.fields.part_types.PAS"),
+ value: ["PAS", "PASL"]
+ }
+ ],
+ onFilter: (value, record) => value.includes(record.part_type),
+ render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
+ },
- {
- title: t("joblines.fields.act_price"),
- dataIndex: "act_price",
- key: "act_price",
- width: "10%",
- sorter: (a, b) => a.act_price - b.act_price,
- shouldCellUpdate: false,
- render: (text, record) => (
- <>
-
- {record.db_ref === "900510" || record.db_ref === "900511"
- ? record.prt_dsmk_m
- : record.act_price}
-
- {record.part_qty ? `(x ${record.part_qty})` : null}
- {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
- {`(${record.prt_dsmk_p}%)`}
- ) : (
- <>>
- )}
- >
- ),
- },
- {
- title: t("billlines.fields.posting"),
- dataIndex: "posting",
- key: "posting",
+ {
+ title: t("joblines.fields.act_price"),
+ dataIndex: "act_price",
+ key: "act_price",
+ width: "10%",
+ sorter: (a, b) => a.act_price - b.act_price,
+ shouldCellUpdate: false,
+ render: (text, record) => (
+ <>
+
+ {record.db_ref === "900510" || record.db_ref === "900511" ? record.prt_dsmk_m : record.act_price}
+
+ {record.part_qty ? `(x ${record.part_qty})` : null}
+ {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
+ {`(${record.prt_dsmk_p}%)`}
+ ) : (
+ <>>
+ )}
+ >
+ )
+ },
+ {
+ title: t("billlines.fields.posting"),
+ dataIndex: "posting",
+ key: "posting",
- render: (text, record, index) => (
-
-
-
- ),
- },
- ];
-
- const data =
- search === ""
- ? lineData
- : lineData.filter(
- (l) =>
- (l.line_desc &&
- l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
- (l.oem_partno &&
- l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
- (l.act_price &&
- l.act_price.toString().startsWith(search.toString()))
- );
-
- return (
-
- console.log(form.getFieldsValue())}>form
- setSearch(e.target.value)} allowClear/>
-
+ render: (text, record, index) => (
+
+
- );
+ )
+ }
+ ];
+
+ const data =
+ search === ""
+ ? lineData
+ : lineData.filter(
+ (l) =>
+ (l.line_desc && l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
+ (l.oem_partno && l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
+ (l.act_price && l.act_price.toString().startsWith(search.toString()))
+ );
+
+ return (
+
+ console.log(form.getFieldsValue())}>form
+ setSearch(e.target.value)} allowClear />
+
+
+ );
}
diff --git a/client/src/components/bill-form-lines-extended/bill-form-lines.extended.formitem.component.jsx b/client/src/components/bill-form-lines-extended/bill-form-lines.extended.formitem.component.jsx
index 2761a20e1..50dd3ab10 100644
--- a/client/src/components/bill-form-lines-extended/bill-form-lines.extended.formitem.component.jsx
+++ b/client/src/components/bill-form-lines-extended/bill-form-lines.extended.formitem.component.jsx
@@ -1,284 +1,216 @@
import React from "react";
-import {MinusCircleFilled, PlusCircleFilled, WarningOutlined,} from "@ant-design/icons";
-import {Button, Form, Input, InputNumber, Select, Space, Switch} from "antd";
-import {useTranslation} from "react-i18next";
+import { MinusCircleFilled, PlusCircleFilled, WarningOutlined } from "@ant-design/icons";
+import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
+import { useTranslation } from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import CiecaSelect from "../../utils/Ciecaselect";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(BillFormItemsExtendedFormItem);
+export default connect(mapStateToProps, mapDispatchToProps)(BillFormItemsExtendedFormItem);
export function BillFormItemsExtendedFormItem({
- value,
- bodyshop,
- form,
- record,
- index,
- disabled,
- responsibilityCenters,
- discount,
- }) {
- // const { billlineskeys } = form.getFieldsValue("billlineskeys");
-
- const {t} = useTranslation();
- if (!value)
- return (
- {
- const values = form.getFieldsValue("billlineskeys");
-
- form.setFieldsValue({
- ...values,
- billlineskeys: {
- ...(values.billlineskeys || {}),
- [record.id]: {
- joblineid: record.id,
- line_desc: record.line_desc,
- quantity: record.part_qty || 1,
- actual_price: record.act_price,
- cost_center: record.part_type
- ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
- ? record.part_type
- : responsibilityCenters.defaults &&
- (responsibilityCenters.defaults.costs[record.part_type] ||
- null)
- : null,
- },
- },
- });
- }}
- >
-
-
- );
+ value,
+ bodyshop,
+ form,
+ record,
+ index,
+ disabled,
+ responsibilityCenters,
+ discount
+}) {
+ // const { billlineskeys } = form.getFieldsValue("billlineskeys");
+ const { t } = useTranslation();
+ if (!value)
return (
-
-
-
-
-
-
-
-
- {
- const {billlineskeys} = form.getFieldsValue("billlineskeys");
- form.setFieldsValue({
- billlineskeys: {
- ...billlineskeys,
- [record.id]: {
- ...billlineskeys[billlineskeys],
- actual_cost: !!billlineskeys[billlineskeys].actual_cost
- ? billlineskeys[billlineskeys].actual_cost
- : Math.round(
- (parseFloat(e.target.value) * (1 - discount) +
- Number.EPSILON) *
- 100
- ) / 100,
- },
- },
- });
- }}
- />
-
-
-
-
-
- {() => {
- const line = value;
- if (!!!line) return null;
- const lineDiscount = (
- 1 -
- Math.round((line.actual_cost / line.actual_price) * 100) / 100
- ).toPrecision(2);
+ {
+ const values = form.getFieldsValue("billlineskeys");
- if (lineDiscount - discount === 0) return
;
- return ;
- }}
-
-
-
- {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
- ? CiecaSelect(true, false)
- : responsibilityCenters.costs.map((item) => (
- {item.name}
- ))}
-
-
-
-
- {bodyshop.md_parts_locations.map((loc, idx) => (
-
- {loc}
-
- ))}
-
-
-
-
-
-
- {() => {
- if (
- form.getFieldsValue("billlineskeys").billlineskeys[record.id]
- .deductedfromlbr
- )
- return (
-
-
-
-
- {t("joblines.fields.lbr_types.LAA")}
-
-
- {t("joblines.fields.lbr_types.LAB")}
-
-
- {t("joblines.fields.lbr_types.LAD")}
-
-
- {t("joblines.fields.lbr_types.LAE")}
-
-
- {t("joblines.fields.lbr_types.LAF")}
-
-
- {t("joblines.fields.lbr_types.LAG")}
-
-
- {t("joblines.fields.lbr_types.LAM")}
-
-
- {t("joblines.fields.lbr_types.LAR")}
-
-
- {t("joblines.fields.lbr_types.LAS")}
-
-
- {t("joblines.fields.lbr_types.LAU")}
-
-
- {t("joblines.fields.lbr_types.LA1")}
-
-
- {t("joblines.fields.lbr_types.LA2")}
-
-
- {t("joblines.fields.lbr_types.LA3")}
-
-
- {t("joblines.fields.lbr_types.LA4")}
-
-
-
-
-
-
-
- );
- return <>>;
- }}
-
-
-
-
-
-
-
-
-
-
-
-
- {
- const values = form.getFieldsValue("billlineskeys");
-
- form.setFieldsValue({
- ...values,
- billlineskeys: {
- ...(values.billlineskeys || {}),
- [record.id]: null,
- },
- });
- }}
- >
-
-
-
+ form.setFieldsValue({
+ ...values,
+ billlineskeys: {
+ ...(values.billlineskeys || {}),
+ [record.id]: {
+ joblineid: record.id,
+ line_desc: record.line_desc,
+ quantity: record.part_qty || 1,
+ actual_price: record.act_price,
+ cost_center: record.part_type
+ ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
+ ? record.part_type
+ : responsibilityCenters.defaults && (responsibilityCenters.defaults.costs[record.part_type] || null)
+ : null
+ }
+ }
+ });
+ }}
+ >
+
+
);
+
+ return (
+
+
+
+
+
+
+
+
+ {
+ const { billlineskeys } = form.getFieldsValue("billlineskeys");
+ form.setFieldsValue({
+ billlineskeys: {
+ ...billlineskeys,
+ [record.id]: {
+ ...billlineskeys[billlineskeys],
+ actual_cost: !!billlineskeys[billlineskeys].actual_cost
+ ? billlineskeys[billlineskeys].actual_cost
+ : Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
+ }
+ }
+ });
+ }}
+ />
+
+
+
+
+
+ {() => {
+ const line = value;
+ if (!!!line) return null;
+ const lineDiscount = (1 - Math.round((line.actual_cost / line.actual_price) * 100) / 100).toPrecision(2);
+
+ if (lineDiscount - discount === 0) return
;
+ return ;
+ }}
+
+
+
+ {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
+ ? CiecaSelect(true, false)
+ : responsibilityCenters.costs.map((item) => {item.name} )}
+
+
+
+
+ {bodyshop.md_parts_locations.map((loc, idx) => (
+
+ {loc}
+
+ ))}
+
+
+
+
+
+
+ {() => {
+ if (form.getFieldsValue("billlineskeys").billlineskeys[record.id].deductedfromlbr)
+ return (
+
+
+
+ {t("joblines.fields.lbr_types.LAA")}
+ {t("joblines.fields.lbr_types.LAB")}
+ {t("joblines.fields.lbr_types.LAD")}
+ {t("joblines.fields.lbr_types.LAE")}
+ {t("joblines.fields.lbr_types.LAF")}
+ {t("joblines.fields.lbr_types.LAG")}
+ {t("joblines.fields.lbr_types.LAM")}
+ {t("joblines.fields.lbr_types.LAR")}
+ {t("joblines.fields.lbr_types.LAS")}
+ {t("joblines.fields.lbr_types.LAU")}
+ {t("joblines.fields.lbr_types.LA1")}
+ {t("joblines.fields.lbr_types.LA2")}
+ {t("joblines.fields.lbr_types.LA3")}
+ {t("joblines.fields.lbr_types.LA4")}
+
+
+
+
+
+
+ );
+ return <>>;
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ const values = form.getFieldsValue("billlineskeys");
+
+ form.setFieldsValue({
+ ...values,
+ billlineskeys: {
+ ...(values.billlineskeys || {}),
+ [record.id]: null
+ }
+ });
+ }}
+ >
+
+
+
+ );
}
diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx
index 2cdcddf72..508be6af5 100644
--- a/client/src/components/bill-form/bill-form.component.jsx
+++ b/client/src/components/bill-form/bill-form.component.jsx
@@ -1,30 +1,30 @@
-import Icon, { UploadOutlined } from '@ant-design/icons';
-import { useApolloClient } from '@apollo/client';
-import { useSplitTreatments } from '@splitsoftware/splitio-react';
-import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from 'antd';
-import React, { useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { MdOpenInNew } from 'react-icons/md';
-import { connect } from 'react-redux';
-import { Link } from 'react-router-dom';
-import { createStructuredSelector } from 'reselect';
-import { CHECK_BILL_INVOICE_NUMBER } from '../../graphql/bills.queries';
-import { selectBodyshop } from '../../redux/user/user.selectors';
-import dayjs from '../../utils/day';
-import InstanceRenderManager from '../../utils/instanceRenderMgr';
-import AlertComponent from '../alert/alert.component';
-import BillFormLinesExtended from '../bill-form-lines-extended/bill-form-lines-extended.component';
-import FormDatePicker from '../form-date-picker/form-date-picker.component';
-import FormFieldsChanged from '../form-fields-changed-alert/form-fields-changed-alert.component';
-import CurrencyInput from '../form-items-formatted/currency-form-item.component';
-import JobSearchSelect from '../job-search-select/job-search-select.component';
-import LayoutFormRow from '../layout-form-row/layout-form-row.component';
-import VendorSearchSelect from '../vendor-search-select/vendor-search-select.component';
-import BillFormLines from './bill-form.lines.component';
-import { CalculateBillTotal } from './bill-form.totals.utility';
+import Icon, { UploadOutlined } from "@ant-design/icons";
+import { useApolloClient } from "@apollo/client";
+import { useSplitTreatments } from "@splitsoftware/splitio-react";
+import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { MdOpenInNew } from "react-icons/md";
+import { connect } from "react-redux";
+import { Link } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import dayjs from "../../utils/day";
+import InstanceRenderManager from "../../utils/instanceRenderMgr";
+import AlertComponent from "../alert/alert.component";
+import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
+import FormDatePicker from "../form-date-picker/form-date-picker.component";
+import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
+import CurrencyInput from "../form-items-formatted/currency-form-item.component";
+import JobSearchSelect from "../job-search-select/job-search-select.component";
+import LayoutFormRow from "../layout-form-row/layout-form-row.component";
+import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
+import BillFormLines from "./bill-form.lines.component";
+import { CalculateBillTotal } from "./bill-form.totals.utility";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({});
@@ -41,18 +41,18 @@ export function BillFormComponent({
job,
loadOutstandingReturns,
loadInventory,
- preferredMake,
+ preferredMake
}) {
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const {
- treatments: { Extended_Bill_Posting, ClosingPeriod },
+ treatments: { Extended_Bill_Posting, ClosingPeriod }
} = useSplitTreatments({
attributes: {},
- names: ['Extended_Bill_Posting', 'ClosingPeriod'],
- splitKey: bodyshop.imexshopid,
+ names: ["Extended_Bill_Posting", "ClosingPeriod"],
+ splitKey: bodyshop.imexshopid
});
const handleVendorSelect = (props, opt) => {
@@ -62,16 +62,16 @@ export function BillFormComponent({
!billEdit &&
loadOutstandingReturns({
variables: {
- jobId: form.getFieldValue('jobid'),
- vendorId: opt.value,
- },
+ jobId: form.getFieldValue("jobid"),
+ vendorId: opt.value
+ }
});
};
const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate
if (!checked) return;
- const values = form.getFieldsValue('billlines');
+ const values = form.getFieldsValue("billlines");
// Gate bill lines
if (!values?.billlines?.length) return;
@@ -83,26 +83,26 @@ export function BillFormComponent({
};
useEffect(() => {
- if (job) form.validateFields(['is_credit_memo']);
+ if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]);
useEffect(() => {
- const vendorId = form.getFieldValue('vendorid');
+ const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
}
}
- const jobId = form.getFieldValue('jobid');
+ const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({ variables: { id: jobId } });
- if (form.getFieldValue('is_credit_memo') && vendorId && !billEdit) {
+ if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
jobId: jobId,
- vendorId: vendorId,
- },
+ vendorId: vendorId
+ }
});
}
}
@@ -118,24 +118,24 @@ export function BillFormComponent({
setDiscount,
vendorAutoCompleteOptions,
loadLines,
- bodyshop.inhousevendorid,
+ bodyshop.inhousevendorid
]);
return (
-
+
{
- if (
- form.getFieldValue('jobid') !== null &&
- form.getFieldValue('jobid') !== undefined
- ) {
- loadLines({ variables: { id: form.getFieldValue('jobid') } });
- if (
- form.getFieldValue('vendorid') !== null &&
- form.getFieldValue('vendorid') !== undefined
- ) {
+ if (form.getFieldValue("jobid") !== null && form.getFieldValue("jobid") !== undefined) {
+ loadLines({ variables: { id: form.getFieldValue("jobid") } });
+ if (form.getFieldValue("vendorid") !== null && form.getFieldValue("vendorid") !== undefined) {
loadOutstandingReturns({
variables: {
- jobId: form.getFieldValue('jobid'),
- vendorId: form.getFieldValue('vendorid'),
- },
+ jobId: form.getFieldValue("jobid"),
+ vendorId: form.getFieldValue("vendorid")
+ }
});
}
}
@@ -164,22 +158,22 @@ export function BillFormComponent({
/>
({
validator(rule, value) {
- if (value && !getFieldValue(['isinhouse']) && value === bodyshop.inhousevendorid) {
- return Promise.reject(t('bills.validation.manualinhouse'));
+ if (value && !getFieldValue(["isinhouse"]) && value === bodyshop.inhousevendorid) {
+ return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
- },
- }),
+ }
+ })
]}
>
- {t('bills.labels.iouexists')}
-
+ {t("bills.labels.iouexists")}
+
{iou.ro_number}
@@ -216,89 +206,85 @@ export function BillFormComponent({
))}
({
async validator(rule, value) {
- const vendorid = getFieldValue('vendorid');
+ const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
- vendorid: vendorid,
- },
+ vendorid: vendorid
+ }
});
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
- response.data.bills_aggregate.nodes[0].id === form.getFieldValue('id')
+ response.data.bills_aggregate.nodes[0].id === form.getFieldValue("id")
) {
return Promise.resolve();
}
- return Promise.reject(t('bills.validation.unique_invoice_number'));
+ return Promise.reject(t("bills.validation.unique_invoice_number"));
} else {
return Promise.resolve();
}
- },
- }),
+ }
+ })
]}
>
({
validator(rule, value) {
- if (ClosingPeriod.treatment === 'on' && bodyshop.accountingconfig.ClosingPeriod) {
+ if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
if (
dayjs(value)
- .startOf('day')
- .isSameOrAfter(
- dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf('day')
- ) &&
+ .startOf("day")
+ .isSameOrAfter(dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf("day")) &&
dayjs(value)
- .startOf('day')
- .isSameOrBefore(
- dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf('day')
- )
+ .startOf("day")
+ .isSameOrBefore(dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf("day"))
) {
return Promise.resolve();
} else {
- return Promise.reject(t('bills.validation.closingperiod'));
+ return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
- },
- }),
+ }
+ })
]}
>
({
validator(rule, value) {
- if (value === true && getFieldValue('jobid') && getFieldValue('vendorid')) {
+ if (value === true && getFieldValue("jobid") && getFieldValue("vendorid")) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
@@ -316,31 +302,31 @@ export function BillFormComponent({
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
- return Promise.reject(t('bills.labels.onlycmforinvoiced'));
+ return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
return Promise.resolve();
- },
- }),
+ }
+ })
]}
>
{!billEdit && (
-
-
+
+
{bodyshop.md_parts_locations.map((loc, idx) => (
{loc}
@@ -353,40 +339,36 @@ export function BillFormComponent({
{InstanceRenderManager({
imex: (
-
+
- ),
+ )
})}
-
+
{InstanceRenderManager({
imex: (
<>
-
+
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
-
+
) : null}
>
- ),
+ )
})}
{() => {
const values = form.getFieldsValue([
- 'billlines',
- 'total',
- 'federal_tax_rate',
- 'state_tax_rate',
- 'local_tax_rate',
+ "billlines",
+ "total",
+ "federal_tax_rate",
+ "state_tax_rate",
+ "local_tax_rate"
]);
let totals;
if (!!values.total && !!values.billlines && values.billlines.length > 0)
@@ -394,56 +376,48 @@ export function BillFormComponent({
if (!!totals)
return (
-
-
+
+
{InstanceRenderManager({
imex: (
- ),
+ )
})}
-
+
{InstanceRenderManager({
imex: (
- ),
+ )
})}
- {form.getFieldValue('is_credit_memo') ? (
-
+ {form.getFieldValue("is_credit_memo") ? (
+
) : null}
);
@@ -451,9 +425,9 @@ export function BillFormComponent({
}}
- {t('bills.labels.bill_lines')}
+ {t("bills.labels.bill_lines")}
- {Extended_Bill_Posting.treatment === 'on' ? (
+ {Extended_Bill_Posting.treatment === "on" ? (
)}
-
- {t('documents.labels.upload')}
+
+ {t("documents.labels.upload")}
{
if (Array.isArray(e)) {
diff --git a/client/src/components/bill-form/bill-form.container.jsx b/client/src/components/bill-form/bill-form.container.jsx
index 67b3c9ab3..e24418b12 100644
--- a/client/src/components/bill-form/bill-form.container.jsx
+++ b/client/src/components/bill-form/bill-form.container.jsx
@@ -1,83 +1,67 @@
-import {useLazyQuery, useQuery} from "@apollo/client";
-import {useSplitTreatments} from "@splitsoftware/splitio-react";
+import { useLazyQuery, useQuery } from "@apollo/client";
+import { useSplitTreatments } from "@splitsoftware/splitio-react";
import React from "react";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {QUERY_OUTSTANDING_INVENTORY} from "../../graphql/inventory.queries";
-import {GET_JOB_LINES_TO_ENTER_BILL} from "../../graphql/jobs-lines.queries";
-import {QUERY_UNRECEIVED_LINES} from "../../graphql/parts-orders.queries";
-import {SEARCH_VENDOR_AUTOCOMPLETE} from "../../graphql/vendors.queries";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
+import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
+import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
+import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
import BillFormComponent from "./bill-form.component";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
-export function BillFormContainer({
- bodyshop,
- form,
- billEdit,
- disabled,
- disableInvNumber,
- }) {
- const {treatments: {Simple_Inventory}} = useSplitTreatments({
- attributes: {},
- names: ["Simple_Inventory"],
- splitKey: bodyshop && bodyshop.imexshopid,
- });
+export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
+ const {
+ treatments: { Simple_Inventory }
+ } = useSplitTreatments({
+ attributes: {},
+ names: ["Simple_Inventory"],
+ splitKey: bodyshop && bodyshop.imexshopid
+ });
- const {data: VendorAutoCompleteData} = useQuery(
- SEARCH_VENDOR_AUTOCOMPLETE,
- {fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
- );
+ const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE, {
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
- const [loadLines, {data: lineData}] = useLazyQuery(
- GET_JOB_LINES_TO_ENTER_BILL
- );
+ const [loadLines, { data: lineData }] = useLazyQuery(GET_JOB_LINES_TO_ENTER_BILL);
- const [loadOutstandingReturns, {loading: returnLoading, data: returnData}] =
- useLazyQuery(QUERY_UNRECEIVED_LINES);
- const [loadInventory, {loading: inventoryLoading, data: inventoryData}] =
- useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
+ const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] = useLazyQuery(QUERY_UNRECEIVED_LINES);
+ const [loadInventory, { loading: inventoryLoading, data: inventoryData }] = useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
- return (
- <>
-
- {!billEdit && (
-
- )}
- {Simple_Inventory.treatment === "on" && (
-
- )}
- >
- );
+ return (
+ <>
+
+ {!billEdit && }
+ {Simple_Inventory.treatment === "on" && (
+
+ )}
+ >
+ );
}
export default connect(mapStateToProps, null)(BillFormContainer);
diff --git a/client/src/components/bill-form/bill-form.lines.component.jsx b/client/src/components/bill-form/bill-form.lines.component.jsx
index d800ccf00..555be39e3 100644
--- a/client/src/components/bill-form/bill-form.lines.component.jsx
+++ b/client/src/components/bill-form/bill-form.lines.component.jsx
@@ -1,17 +1,6 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
-import {
- Button,
- Checkbox,
- Form,
- Input,
- InputNumber,
- Select,
- Space,
- Switch,
- Table,
- Tooltip,
-} from "antd";
+import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -24,893 +13,641 @@ import CurrencyInput from "../form-items-formatted/currency-form-item.component"
import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({
- //currentUser: selectCurrentUser
- bodyshop: selectBodyshop,
+ //currentUser: selectCurrentUser
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function BillEnterModalLinesComponent({
- bodyshop,
- disabled,
- lineData,
- discount,
- form,
- responsibilityCenters,
- billEdit,
- billid,
+ bodyshop,
+ disabled,
+ lineData,
+ discount,
+ form,
+ responsibilityCenters,
+ billEdit,
+ billid
}) {
- const { t } = useTranslation();
- const { setFieldsValue, getFieldsValue, getFieldValue } = form;
+ const { t } = useTranslation();
+ const { setFieldsValue, getFieldsValue, getFieldValue } = form;
- const {
- treatments: { Simple_Inventory, Enhanced_Payroll },
- } = useSplitTreatments({
- attributes: {},
- names: ["Simple_Inventory", "Enhanced_Payroll"],
- splitKey: bodyshop && bodyshop.imexshopid,
+ const {
+ treatments: { Simple_Inventory, Enhanced_Payroll }
+ } = useSplitTreatments({
+ attributes: {},
+ names: ["Simple_Inventory", "Enhanced_Payroll"],
+ splitKey: bodyshop && bodyshop.imexshopid
+ });
+
+ const columns = (remove) => {
+ return [
+ {
+ title: t("billlines.fields.jobline"),
+ dataIndex: "joblineid",
+ editable: true,
+ width: "20rem",
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}joblinename`,
+ name: [field.name, "joblineid"],
+ label: t("billlines.fields.jobline"),
+ rules: [
+ {
+ required: true
+ //message: t("general.validation.required"),
+ }
+ ]
+ };
+ },
+ wrapper: (props) => (
+ prev.is_credit_memo !== cur.is_credit_memo}>
+ {() => {
+ return props.children;
+ }}
+
+ ),
+ formInput: (record, index) => (
+ {
+ setFieldsValue({
+ billlines: getFieldsValue(["billlines"]).billlines.map((item, idx) => {
+ if (idx === index) {
+ return {
+ ...item,
+ line_desc: opt.line_desc,
+ quantity: opt.part_qty || 1,
+ actual_price: opt.cost,
+ original_actual_price: opt.cost,
+ cost_center: opt.part_type
+ ? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
+ ? opt.part_type !== "PAE"
+ ? opt.part_type
+ : null
+ : responsibilityCenters.defaults &&
+ (responsibilityCenters.defaults.costs[opt.part_type] || null)
+ : null
+ };
+ }
+ return item;
+ })
+ });
+ }}
+ />
+ )
+ },
+ {
+ title: t("billlines.fields.line_desc"),
+ dataIndex: "line_desc",
+ editable: true,
+
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}line_desc`,
+ name: [field.name, "line_desc"],
+ label: t("billlines.fields.line_desc"),
+ rules: [
+ {
+ required: true
+ //message: t("general.validation.required"),
+ }
+ ]
+ };
+ },
+ formInput: (record, index) =>
+ },
+ {
+ title: t("billlines.fields.quantity"),
+ dataIndex: "quantity",
+ editable: true,
+ width: "4rem",
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}quantity`,
+ name: [field.name, "quantity"],
+ label: t("billlines.fields.quantity"),
+ rules: [
+ {
+ required: true
+ //message: t("general.validation.required"),
+ },
+ ({ getFieldValue }) => ({
+ validator(rule, value) {
+ if (value && getFieldValue("billlines")[field.fieldKey]?.inventories?.length > value) {
+ return Promise.reject(
+ t("bills.validation.inventoryquantity", {
+ number: getFieldValue("billlines")[field.fieldKey]?.inventories?.length
+ })
+ );
+ }
+ return Promise.resolve();
+ }
+ })
+ ]
+ };
+ },
+ formInput: (record, index) =>
+ },
+ {
+ title: t("billlines.fields.actual_price"),
+ dataIndex: "actual_price",
+ width: "8rem",
+ editable: true,
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}actual_price`,
+ name: [field.name, "actual_price"],
+ label: t("billlines.fields.actual_price"),
+ rules: [
+ {
+ required: true
+ //message: t("general.validation.required"),
+ }
+ ]
+ };
+ },
+ formInput: (record, index) => (
+ {
+ setFieldsValue({
+ billlines: getFieldsValue("billlines").billlines.map((item, idx) => {
+ if (idx === index) {
+ return {
+ ...item,
+ actual_cost: !!item.actual_cost
+ ? item.actual_cost
+ : Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
+ };
+ }
+ return item;
+ })
+ });
+ }}
+ />
+ ),
+ additional: (record, index) =>
+ InstanceRenderManager({
+ rome: (
+
+ {() => {
+ const billLine = getFieldValue(["billlines", record.name]);
+ const jobLine = lineData.find((line) => line.id === billLine?.joblineid);
+
+ if (!billEdit && billLine && jobLine && billLine?.actual_price !== jobLine?.act_price) {
+ return (
+
+
+
+
+ {t("joblines.fields.create_ppc")}
+
+ );
+ } else {
+ return null;
+ }
+ }}
+
+ )
+ //Do not need to set for promanager as it will default to Rome.
+ })
+ },
+ {
+ title: t("billlines.fields.actual_cost"),
+ dataIndex: "actual_cost",
+ editable: true,
+ width: "8rem",
+
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}actual_cost`,
+ name: [field.name, "actual_cost"],
+ label: t("billlines.fields.actual_cost"),
+ rules: [
+ {
+ required: true
+ //message: t("general.validation.required"),
+ }
+ ]
+ };
+ },
+ formInput: (record, index) => (
+
+ {() => {
+ const line = getFieldsValue(["billlines"]).billlines[index];
+ if (!!!line) return null;
+ let lineDiscount = 1 - line.actual_cost / line.actual_price;
+ if (isNaN(lineDiscount)) lineDiscount = 0;
+ return (
+
+ 0.005
+ ? lineDiscount > discount
+ ? "orange"
+ : "red"
+ : "green"
+ }}
+ />
+
+ );
+ }}
+
+ }
+ />
+ )
+ // additional: (record, index) => (
+ //
+ // {() => {
+ // const line = getFieldsValue(["billlines"]).billlines[index];
+ // if (!!!line) return null;
+ // const lineDiscount = (
+ // 1 -
+ // Math.round((line.actual_cost / line.actual_price) * 100) / 100
+ // ).toPrecision(2);
+
+ // return (
+ //
+ //
+ //
+ // );
+ // }}
+ //
+ // ),
+ },
+ {
+ title: t("billlines.fields.cost_center"),
+ dataIndex: "cost_center",
+ editable: true,
+
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}cost_center`,
+ name: [field.name, "cost_center"],
+ label: t("billlines.fields.cost_center"),
+ valuePropName: "value",
+ rules: [
+ {
+ required: true
+ //message: t("general.validation.required"),
+ }
+ ]
+ };
+ },
+ formInput: (record, index) => (
+
+ {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
+ ? CiecaSelect(true, false)
+ : responsibilityCenters.costs.map((item) => {item.name} )}
+
+ )
+ },
+ ...(billEdit
+ ? []
+ : [
+ {
+ title: t("billlines.fields.location"),
+ dataIndex: "location",
+ editable: true,
+ label: t("billlines.fields.location"),
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}location`,
+ name: [field.name, "location"]
+ };
+ },
+ formInput: (record, index) => (
+
+ {bodyshop.md_parts_locations.map((loc, idx) => (
+
+ {loc}
+
+ ))}
+
+ )
+ }
+ ]),
+ {
+ title: t("billlines.labels.deductedfromlbr"),
+ dataIndex: "deductedfromlbr",
+ editable: true,
+ formItemProps: (field) => {
+ return {
+ valuePropName: "checked",
+ key: `${field.index}deductedfromlbr`,
+ name: [field.name, "deductedfromlbr"]
+ };
+ },
+ formInput: (record, index) => ,
+ additional: (record, index) => (
+
+ {() => {
+ const price = getFieldValue(["billlines", record.name, "actual_price"]);
+
+ const adjustmentRate = getFieldValue(["billlines", record.name, "lbr_adjustment", "rate"]);
+
+ const billline = getFieldValue(["billlines", record.name]);
+
+ const jobline = lineData.find((line) => line.id === billline?.joblineid);
+
+ const employeeTeamName = bodyshop.employee_teams.find((team) => team.id === jobline?.assigned_team);
+
+ if (getFieldValue(["billlines", record.name, "deductedfromlbr"]))
+ return (
+
+ {Enhanced_Payroll.treatment === "on" ? (
+
+ {t("joblines.fields.assigned_team", {
+ name: employeeTeamName?.name
+ })}
+ {`${jobline.mod_lb_hrs} units/${t(`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`)}`}
+
+ ) : null}
+
+
+
+ {t("joblines.fields.lbr_types.LAA")}
+ {t("joblines.fields.lbr_types.LAB")}
+ {t("joblines.fields.lbr_types.LAD")}
+ {t("joblines.fields.lbr_types.LAE")}
+ {t("joblines.fields.lbr_types.LAF")}
+ {t("joblines.fields.lbr_types.LAG")}
+ {t("joblines.fields.lbr_types.LAM")}
+ {t("joblines.fields.lbr_types.LAR")}
+ {t("joblines.fields.lbr_types.LAS")}
+ {t("joblines.fields.lbr_types.LAU")}
+ {t("joblines.fields.lbr_types.LA1")}
+ {t("joblines.fields.lbr_types.LA2")}
+ {t("joblines.fields.lbr_types.LA3")}
+ {t("joblines.fields.lbr_types.LA4")}
+
+
+ {Enhanced_Payroll.treatment === "on" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {price && adjustmentRate && `${(price / adjustmentRate).toFixed(1)} hrs`}
+
+ );
+ return <>>;
+ }}
+
+ )
+ },
+
+ ...InstanceRenderManager({
+ rome: [],
+ promanager: [],
+ imex: [
+ {
+ title: t("billlines.fields.federal_tax_applicable"),
+ dataIndex: "applicable_taxes.federal",
+ editable: true,
+
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}fedtax`,
+ valuePropName: "checked",
+ initialValue: InstanceRenderManager({
+ imex: true,
+ rome: false,
+ promanager: false
+ }),
+ name: [field.name, "applicable_taxes", "federal"]
+ };
+ },
+ formInput: (record, index) =>
+ }
+ ]
+ }),
+
+ {
+ title: t("billlines.fields.state_tax_applicable"),
+ dataIndex: "applicable_taxes.state",
+ editable: true,
+
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}statetax`,
+ valuePropName: "checked",
+ name: [field.name, "applicable_taxes", "state"]
+ };
+ },
+ formInput: (record, index) =>
+ },
+
+ ...InstanceRenderManager({
+ rome: [],
+ promanager: [],
+ imex: [
+ {
+ title: t("billlines.fields.local_tax_applicable"),
+ dataIndex: "applicable_taxes.local",
+ editable: true,
+
+ formItemProps: (field) => {
+ return {
+ key: `${field.index}localtax`,
+ valuePropName: "checked",
+ name: [field.name, "applicable_taxes", "local"]
+ };
+ },
+ formInput: (record, index) =>
+ }
+ ]
+ }),
+ {
+ title: t("general.labels.actions"),
+
+ dataIndex: "actions",
+ render: (text, record) => (
+
+ {() => (
+
+ 0}
+ onClick={() => remove(record.name)}
+ >
+
+
+ {Simple_Inventory.treatment === "on" && (
+
+ )}
+
+ )}
+
+ )
+ }
+ ];
+ };
+
+ const mergedColumns = (remove) =>
+ columns(remove).map((col) => {
+ if (!col.editable) return col;
+ return {
+ ...col,
+ onCell: (record) => ({
+ record,
+ formItemProps: col.formItemProps,
+ formInput: col.formInput,
+ additional: col.additional,
+ dataIndex: col.dataIndex,
+ title: col.title
+ })
+ };
});
- const columns = (remove) => {
- return [
- {
- title: t("billlines.fields.jobline"),
- dataIndex: "joblineid",
- editable: true,
- width: "20rem",
- formItemProps: (field) => {
- return {
- key: `${field.index}joblinename`,
- name: [field.name, "joblineid"],
- label: t("billlines.fields.jobline"),
- rules: [
- {
- required: true,
- //message: t("general.validation.required"),
- },
- ],
- };
- },
- wrapper: (props) => (
-
- prev.is_credit_memo !== cur.is_credit_memo
- }
- >
- {() => {
- return props.children;
- }}
-
- ),
- formInput: (record, index) => (
- {
- setFieldsValue({
- billlines: getFieldsValue([
- "billlines",
- ]).billlines.map((item, idx) => {
- if (idx === index) {
- return {
- ...item,
- line_desc: opt.line_desc,
- quantity: opt.part_qty || 1,
- actual_price: opt.cost,
- original_actual_price: opt.cost,
- cost_center: opt.part_type
- ? bodyshop.pbs_serialnumber ||
- bodyshop.cdk_dealerid
- ? opt.part_type !== "PAE"
- ? opt.part_type
- : null
- : responsibilityCenters.defaults &&
- (responsibilityCenters
- .defaults.costs[
- opt.part_type
- ] ||
- null)
- : null,
- };
- }
- return item;
- }),
- });
- }}
- />
- ),
- },
- {
- title: t("billlines.fields.line_desc"),
- dataIndex: "line_desc",
- editable: true,
-
- formItemProps: (field) => {
- return {
- key: `${field.index}line_desc`,
- name: [field.name, "line_desc"],
- label: t("billlines.fields.line_desc"),
- rules: [
- {
- required: true,
- //message: t("general.validation.required"),
- },
- ],
- };
- },
- formInput: (record, index) => ,
- },
- {
- title: t("billlines.fields.quantity"),
- dataIndex: "quantity",
- editable: true,
- width: "4rem",
- formItemProps: (field) => {
- return {
- key: `${field.index}quantity`,
- name: [field.name, "quantity"],
- label: t("billlines.fields.quantity"),
- rules: [
- {
- required: true,
- //message: t("general.validation.required"),
- },
- ({ getFieldValue }) => ({
- validator(rule, value) {
- if (
- value &&
- getFieldValue("billlines")[
- field.fieldKey
- ]?.inventories?.length > value
- ) {
- return Promise.reject(
- t(
- "bills.validation.inventoryquantity",
- {
- number: getFieldValue(
- "billlines"
- )[field.fieldKey]
- ?.inventories?.length,
- }
- )
- );
- }
- return Promise.resolve();
- },
- }),
- ],
- };
- },
- formInput: (record, index) => (
-
- ),
- },
- {
- title: t("billlines.fields.actual_price"),
- dataIndex: "actual_price",
- width: "8rem",
- editable: true,
- formItemProps: (field) => {
- return {
- key: `${field.index}actual_price`,
- name: [field.name, "actual_price"],
- label: t("billlines.fields.actual_price"),
- rules: [
- {
- required: true,
- //message: t("general.validation.required"),
- },
- ],
- };
- },
- formInput: (record, index) => (
- {
- setFieldsValue({
- billlines: getFieldsValue(
- "billlines"
- ).billlines.map((item, idx) => {
- if (idx === index) {
- return {
- ...item,
- actual_cost: !!item.actual_cost
- ? item.actual_cost
- : Math.round(
- (parseFloat(
- e.target.value
- ) *
- (1 - discount) +
- Number.EPSILON) *
- 100
- ) / 100,
- };
- }
- return item;
- }),
- });
- }}
- />
- ),
- additional: (record, index) =>
- InstanceRenderManager({
- rome: (
-
- {() => {
- const billLine = getFieldValue([
- "billlines",
- record.name,
- ]);
- const jobLine = lineData.find(
- (line) =>
- line.id === billLine?.joblineid
- );
-
- if (
- !billEdit &&
- billLine &&
- jobLine &&
- billLine?.actual_price !==
- jobLine?.act_price
- ) {
- return (
-
-
-
-
- {t(
- "joblines.fields.create_ppc"
- )}
-
- );
- } else {
- return null;
- }
- }}
-
- ),
- //Do not need to set for promanager as it will default to Rome.
- }),
- },
- {
- title: t("billlines.fields.actual_cost"),
- dataIndex: "actual_cost",
- editable: true,
- width: "8rem",
-
- formItemProps: (field) => {
- return {
- key: `${field.index}actual_cost`,
- name: [field.name, "actual_cost"],
- label: t("billlines.fields.actual_cost"),
- rules: [
- {
- required: true,
- //message: t("general.validation.required"),
- },
- ],
- };
- },
- formInput: (record, index) => (
-
- {() => {
- const line = getFieldsValue(["billlines"])
- .billlines[index];
- if (!!!line) return null;
- let lineDiscount =
- 1 -
- line.actual_cost / line.actual_price;
- if (isNaN(lineDiscount)) lineDiscount = 0;
- return (
-
- 0.005
- ? lineDiscount >
- discount
- ? "orange"
- : "red"
- : "green",
- }}
- />
-
- );
- }}
-
- }
- />
- ),
- // additional: (record, index) => (
- //
- // {() => {
- // const line = getFieldsValue(["billlines"]).billlines[index];
- // if (!!!line) return null;
- // const lineDiscount = (
- // 1 -
- // Math.round((line.actual_cost / line.actual_price) * 100) / 100
- // ).toPrecision(2);
-
- // return (
- //
- //
- //
- // );
- // }}
- //
- // ),
- },
- {
- title: t("billlines.fields.cost_center"),
- dataIndex: "cost_center",
- editable: true,
-
- formItemProps: (field) => {
- return {
- key: `${field.index}cost_center`,
- name: [field.name, "cost_center"],
- label: t("billlines.fields.cost_center"),
- valuePropName: "value",
- rules: [
- {
- required: true,
- //message: t("general.validation.required"),
- },
- ],
- };
- },
- formInput: (record, index) => (
-
- {bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
- ? CiecaSelect(true, false)
- : responsibilityCenters.costs.map((item) => (
-
- {item.name}
-
- ))}
-
- ),
- },
- ...(billEdit
- ? []
- : [
- {
- title: t("billlines.fields.location"),
- dataIndex: "location",
- editable: true,
- label: t("billlines.fields.location"),
- formItemProps: (field) => {
- return {
- key: `${field.index}location`,
- name: [field.name, "location"],
- };
- },
- formInput: (record, index) => (
-
- {bodyshop.md_parts_locations.map(
- (loc, idx) => (
-
- {loc}
-
- )
- )}
-
- ),
- },
- ]),
- {
- title: t("billlines.labels.deductedfromlbr"),
- dataIndex: "deductedfromlbr",
- editable: true,
- formItemProps: (field) => {
- return {
- valuePropName: "checked",
- key: `${field.index}deductedfromlbr`,
- name: [field.name, "deductedfromlbr"],
- };
- },
- formInput: (record, index) => ,
- additional: (record, index) => (
-
- {() => {
- const price = getFieldValue([
- "billlines",
- record.name,
- "actual_price",
- ]);
-
- const adjustmentRate = getFieldValue([
- "billlines",
- record.name,
- "lbr_adjustment",
- "rate",
- ]);
-
- const billline = getFieldValue([
- "billlines",
- record.name,
- ]);
-
- const jobline = lineData.find(
- (line) => line.id === billline?.joblineid
- );
-
- const employeeTeamName =
- bodyshop.employee_teams.find(
- (team) => team.id === jobline?.assigned_team
- );
-
- if (
- getFieldValue([
- "billlines",
- record.name,
- "deductedfromlbr",
- ])
- )
- return (
-
- {Enhanced_Payroll.treatment === "on" ? (
-
- {t(
- "joblines.fields.assigned_team",
- {
- name: employeeTeamName?.name,
- }
- )}
- {`${
- jobline.mod_lb_hrs
- } units/${t(
- `joblines.fields.lbr_types.${jobline.mod_lbr_ty}`
- )}`}
-
- ) : null}
-
-
-
-
- {t(
- "joblines.fields.lbr_types.LAA"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LAB"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LAD"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LAE"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LAF"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LAG"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LAM"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LAR"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LAS"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LAU"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LA1"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LA2"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LA3"
- )}
-
-
- {t(
- "joblines.fields.lbr_types.LA4"
- )}
-
-
-
- {Enhanced_Payroll.treatment === "on" ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
- {price &&
- adjustmentRate &&
- `${(
- price / adjustmentRate
- ).toFixed(1)} hrs`}
-
-
- );
- return <>>;
- }}
-
- ),
- },
-
- ...InstanceRenderManager({
- rome:[],
- promanager:[],
- imex: [
- {
- title: t("billlines.fields.federal_tax_applicable"),
- dataIndex: "applicable_taxes.federal",
- editable: true,
-
- formItemProps: (field) => {
- return {
- key: `${field.index}fedtax`,
- valuePropName: 'checked',
- initialValue: InstanceRenderManager({
- imex: true,
- rome: false,
- promanager: false,
- }),
- name: [field.name, 'applicable_taxes', 'federal'],
- };
- },
- formInput: (record, index) => (
-
- ),
- },
- ],
- }),
-
- {
- title: t("billlines.fields.state_tax_applicable"),
- dataIndex: "applicable_taxes.state",
- editable: true,
-
- formItemProps: (field) => {
- return {
- key: `${field.index}statetax`,
- valuePropName: "checked",
- name: [field.name, "applicable_taxes", "state"],
- };
- },
- formInput: (record, index) => ,
- },
-
- ...InstanceRenderManager({
- rome:[],
- promanager:[],
- imex: [
- {
- title: t("billlines.fields.local_tax_applicable"),
- dataIndex: "applicable_taxes.local",
- editable: true,
-
- formItemProps: (field) => {
- return {
- key: `${field.index}localtax`,
- valuePropName: "checked",
- name: [field.name, "applicable_taxes", "local"],
- };
- },
- formInput: (record, index) => (
-
- ),
- },
- ],
- }),
- {
- title: t("general.labels.actions"),
-
- dataIndex: "actions",
- render: (text, record) => (
-
- {() => (
-
- 0
- }
- onClick={() => remove(record.name)}
- >
-
-
- {Simple_Inventory.treatment === "on" && (
-
- )}
-
- )}
-
- ),
- },
- ];
- };
-
- const mergedColumns = (remove) =>
- columns(remove).map((col) => {
- if (!col.editable) return col;
- return {
- ...col,
- onCell: (record) => ({
- record,
- formItemProps: col.formItemProps,
- formInput: col.formInput,
- additional: col.additional,
- dataIndex: col.dataIndex,
- title: col.title,
- }),
- };
- });
-
- return (
- {
- if (!billlines || billlines.length < 1) {
- return Promise.reject(
- new Error(t("billlines.validation.atleastone"))
- );
- }
- },
- },
- ]}
- >
- {(fields, { add, remove, move }) => {
- return (
- <>
-
-
- {
- add();
- }}
- style={{ width: "100%" }}
- >
- {t("billlines.actions.newline")}
-
-
- >
- );
- }}
-
- );
+ return (
+ {
+ if (!billlines || billlines.length < 1) {
+ return Promise.reject(new Error(t("billlines.validation.atleastone")));
+ }
+ }
+ }
+ ]}
+ >
+ {(fields, { add, remove, move }) => {
+ return (
+ <>
+
+
+ {
+ add();
+ }}
+ style={{ width: "100%" }}
+ >
+ {t("billlines.actions.newline")}
+
+
+ >
+ );
+ }}
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(BillEnterModalLinesComponent);
+export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalLinesComponent);
const EditableCell = ({
- dataIndex,
- title,
- inputType,
- record,
- index,
- children,
- formInput,
- formItemProps,
- additional,
- wrapper,
- ...restProps
+ dataIndex,
+ title,
+ inputType,
+ record,
+ index,
+ children,
+ formInput,
+ formItemProps,
+ additional,
+ wrapper,
+ ...restProps
}) => {
- if (additional)
- return (
-
-
-
- {(formInput && formInput(record, record.name)) ||
- children}
-
- {additional && additional(record, record.name)}
-
-
- );
- if (wrapper)
- return (
-
-
-
- {(formInput && formInput(record, record.name)) ||
- children}
-
-
-
- );
+ if (additional)
return (
-
-
- {(formInput && formInput(record, record.name)) || children}
-
-
+
+
+
+ {(formInput && formInput(record, record.name)) || children}
+
+ {additional && additional(record, record.name)}
+
+
);
+ if (wrapper)
+ return (
+
+
+
+ {(formInput && formInput(record, record.name)) || children}
+
+
+
+ );
+ return (
+
+
+ {(formInput && formInput(record, record.name)) || children}
+
+
+ );
};
diff --git a/client/src/components/bill-form/bill-form.totals.utility.js b/client/src/components/bill-form/bill-form.totals.utility.js
index eb5c67517..cec5c9152 100644
--- a/client/src/components/bill-form/bill-form.totals.utility.js
+++ b/client/src/components/bill-form/bill-form.totals.utility.js
@@ -1,47 +1,42 @@
import Dinero from "dinero.js";
export const CalculateBillTotal = (invoice) => {
- const {total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate} =
- invoice;
+ const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } = invoice;
- //TODO Determine why this recalculates so many times.
- let subtotal = Dinero({amount: 0});
- let federalTax = Dinero({amount: 0});
- let stateTax = Dinero({amount: 0});
- let localTax = Dinero({amount: 0});
+ //TODO Determine why this recalculates so many times.
+ let subtotal = Dinero({ amount: 0 });
+ let federalTax = Dinero({ amount: 0 });
+ let stateTax = Dinero({ amount: 0 });
+ let localTax = Dinero({ amount: 0 });
- if (!!!billlines) return null;
+ if (!!!billlines) return null;
- billlines.forEach((i) => {
- if (!!i) {
- const itemTotal = Dinero({
- amount: Math.round((i.actual_cost || 0) * 100),
- }).multiply(i.quantity || 1);
+ billlines.forEach((i) => {
+ if (!!i) {
+ const itemTotal = Dinero({
+ amount: Math.round((i.actual_cost || 0) * 100)
+ }).multiply(i.quantity || 1);
- subtotal = subtotal.add(itemTotal);
- if (i.applicable_taxes?.federal) {
- federalTax = federalTax.add(
- itemTotal.percentage(federal_tax_rate || 0)
- );
- }
- if (i.applicable_taxes?.state)
- stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
- if (i.applicable_taxes?.local)
- localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
- }
- });
+ subtotal = subtotal.add(itemTotal);
+ if (i.applicable_taxes?.federal) {
+ federalTax = federalTax.add(itemTotal.percentage(federal_tax_rate || 0));
+ }
+ if (i.applicable_taxes?.state) stateTax = stateTax.add(itemTotal.percentage(state_tax_rate || 0));
+ if (i.applicable_taxes?.local) localTax = localTax.add(itemTotal.percentage(local_tax_rate || 0));
+ }
+ });
- const invoiceTotal = Dinero({amount: Math.round((total || 0) * 100)});
- const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
- const discrepancy = enteredTotal.subtract(invoiceTotal);
+ const invoiceTotal = Dinero({ amount: Math.round((total || 0) * 100) });
+ const enteredTotal = subtotal.add(federalTax).add(stateTax).add(localTax);
+ const discrepancy = enteredTotal.subtract(invoiceTotal);
- return {
- subtotal,
- federalTax,
- stateTax,
- localTax,
- enteredTotal,
- invoiceTotal,
- discrepancy,
- };
+ return {
+ subtotal,
+ federalTax,
+ stateTax,
+ localTax,
+ enteredTotal,
+ invoiceTotal,
+ discrepancy
+ };
};
diff --git a/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx b/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx
index a9cab74b9..7cbdd27b9 100644
--- a/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx
+++ b/client/src/components/bill-inventory-table/bill-inventory-table.component.jsx
@@ -1,173 +1,153 @@
-import {Checkbox, Form, Skeleton, Typography} from "antd";
-import React, {useEffect} from "react";
-import {useTranslation} from "react-i18next";
+import { Checkbox, Form, Skeleton, Typography } from "antd";
+import React, { useEffect } from "react";
+import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-inventory-table.styles.scss";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectBodyshop} from "../../redux/user/user.selectors";
-import {selectBillEnterModal} from "../../redux/modals/modals.selectors";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
- billEnterModal: selectBillEnterModal,
+ bodyshop: selectBodyshop,
+ billEnterModal: selectBillEnterModal
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
-export function BillInventoryTable({
- billEnterModal,
- bodyshop,
- form,
- billEdit,
- inventoryLoading,
- inventoryData,
- }) {
- const {t} = useTranslation();
+export function BillInventoryTable({ billEnterModal, bodyshop, form, billEdit, inventoryLoading, inventoryData }) {
+ const { t } = useTranslation();
- useEffect(() => {
- if (inventoryData && inventoryData.inventory) {
- form.setFieldsValue({
- inventory: billEnterModal.context.consumeinventoryid
- ? inventoryData.inventory.map((i) => {
- if (i.id === billEnterModal.context.consumeinventoryid)
- i.consumefrominventory = true;
- return i;
- })
- : inventoryData.inventory,
- });
+ useEffect(() => {
+ if (inventoryData && inventoryData.inventory) {
+ form.setFieldsValue({
+ inventory: billEnterModal.context.consumeinventoryid
+ ? inventoryData.inventory.map((i) => {
+ if (i.id === billEnterModal.context.consumeinventoryid) i.consumefrominventory = true;
+ return i;
+ })
+ : inventoryData.inventory
+ });
+ }
+ }, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
+
+ return (
+ prev.vendorid !== cur.vendorid} noStyle>
+ {() => {
+ const is_inhouse = form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
+
+ if (!is_inhouse || billEdit) {
+ return null;
}
- }, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
- return (
- prev.vendorid !== cur.vendorid}
- noStyle
- >
- {() => {
- const is_inhouse =
- form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
+ if (inventoryLoading) return ;
- if (!is_inhouse || billEdit) {
- return null;
- }
+ return (
+
+ {(fields, { add, remove, move }) => {
+ return (
+ <>
+ {t("inventory.labels.inventory")}
+
+
+
+ {t("billlines.fields.line_desc")}
+ {t("vendors.fields.name")}
+ {t("billlines.fields.quantity")}
+ {t("billlines.fields.actual_price")}
+ {t("billlines.fields.actual_cost")}
+ {t("inventory.fields.comment")}
+ {t("inventory.actions.consumefrominventory")}
+
+
+
+ {fields.map((field, index) => (
+
+
+
+
+
+
- if (inventoryLoading) return ;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- return (
-
- {(fields, {add, remove, move}) => {
- return (
- <>
-
- {t("inventory.labels.inventory")}
-
-
-
-
- {t("billlines.fields.line_desc")}
- {t("vendors.fields.name")}
- {t("billlines.fields.quantity")}
- {t("billlines.fields.actual_price")}
- {t("billlines.fields.actual_cost")}
- {t("inventory.fields.comment")}
- {t("inventory.actions.consumefrominventory")}
-
-
-
- {fields.map((field, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
- >
- );
- }}
-
- );
+
+
+
+
+
+
+ ))}
+
+
+ >
+ );
}}
-
- );
+
+ );
+ }}
+
+ );
}
diff --git a/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss b/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss
index 9a47de1dc..173714f41 100644
--- a/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss
+++ b/client/src/components/bill-inventory-table/bill-inventory-table.styles.scss
@@ -16,4 +16,4 @@
tr:hover {
background-color: #f5f5f5;
}
-}
\ No newline at end of file
+}
diff --git a/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx b/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx
index 75616849a..187cf29bf 100644
--- a/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx
+++ b/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx
@@ -1,98 +1,73 @@
-import {Select} from "antd";
-import React, {forwardRef} from "react";
-import {useTranslation} from "react-i18next";
-import InstanceRenderMgr from '../../utils/instanceRenderMgr';
+import { Select } from "antd";
+import React, { forwardRef } from "react";
+import { useTranslation } from "react-i18next";
+import InstanceRenderMgr from "../../utils/instanceRenderMgr";
//To be used as a form element only.
-const {Option} = Select;
-const BillLineSearchSelect = (
- {options, disabled, allowRemoved, ...restProps},
- ref
-) => {
- const {t} = useTranslation();
+const { Option } = Select;
+const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
+ const { t } = useTranslation();
- return (
- {
- return (
- (option.line_desc &&
- option.line_desc
- .toLowerCase()
- .includes(inputValue.toLowerCase())) ||
- (option.oem_partno &&
- option.oem_partno
- .toLowerCase()
- .includes(inputValue.toLowerCase())) ||
- (option.alt_partno &&
- option.alt_partno
- .toLowerCase()
- .includes(inputValue.toLowerCase())) ||
- (option.act_price &&
- option.act_price.toString().startsWith(inputValue.toString()))
- );
- }}
- notFoundContent={"Removed."}
- {...restProps}
- >
-
- {t("billlines.labels.other")}
-
- {options
- ? options.map((item) => (
-
+ return (
+ {
+ return (
+ (option.line_desc && option.line_desc.toLowerCase().includes(inputValue.toLowerCase())) ||
+ (option.oem_partno && option.oem_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
+ (option.alt_partno && option.alt_partno.toLowerCase().includes(inputValue.toLowerCase())) ||
+ (option.act_price && option.act_price.toString().startsWith(inputValue.toString()))
+ );
+ }}
+ notFoundContent={"Removed."}
+ {...restProps}
+ >
+
+ {t("billlines.labels.other")}
+
+ {options
+ ? options.map((item) => (
+
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
- item.oem_partno ? ` - ${item.oem_partno}` : ""
+ item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
- {
- InstanceRenderMgr
- (
- {
- rome: item.act_price === 0 && item.mod_lb_hrs > 0 && (
-
- {`${item.mod_lb_hrs} units`}
-
- )
+ {InstanceRenderMgr({
+ rome: item.act_price === 0 && item.mod_lb_hrs > 0 && (
+ {`${item.mod_lb_hrs} units`}
+ )
+ })}
- }
- )
-
-
- }
-
-
- {item.act_price
- ? `$${item.act_price && item.act_price.toFixed(2)}`
- : ``}
+
+ {item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
-
- ))
- : null}
-
- );
+
+ ))
+ : null}
+
+ );
};
export default forwardRef(BillLineSearchSelect);
diff --git a/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx b/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx
index a014bf775..5a5720135 100644
--- a/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx
+++ b/client/src/components/bill-mark-exported-button/bill-mark-exported-button.component.jsx
@@ -1,97 +1,89 @@
-import {gql, useMutation} from "@apollo/client";
-import {Button, notification} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
+import { gql, useMutation } from "@apollo/client";
+import { Button, notification } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectAuthLevel, selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
-import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
-import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
+import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
+import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
- authLevel: selectAuthLevel,
- currentUser: selectCurrentUser,
+ bodyshop: selectBodyshop,
+ authLevel: selectAuthLevel,
+ currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(BillMarkExportedButton);
+export default connect(mapStateToProps, mapDispatchToProps)(BillMarkExportedButton);
-export function BillMarkExportedButton({
- currentUser,
- bodyshop,
- authLevel,
- bill,
- }) {
- const {t} = useTranslation();
- const [loading, setLoading] = useState(false);
- const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
+export function BillMarkExportedButton({ currentUser, bodyshop, authLevel, bill }) {
+ const { t } = useTranslation();
+ const [loading, setLoading] = useState(false);
+ const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
- const [updateBill] = useMutation(gql`
- mutation UPDATE_BILL($billId: uuid!) {
- update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
- returning {
- id
- exported
- exported_at
- }
- }
+ const [updateBill] = useMutation(gql`
+ mutation UPDATE_BILL($billId: uuid!) {
+ update_bills(where: { id: { _eq: $billId } }, _set: { exported: true }) {
+ returning {
+ id
+ exported
+ exported_at
}
- `);
+ }
+ }
+ `);
- const handleUpdate = async () => {
- setLoading(true);
- const result = await updateBill({
- variables: {billId: bill.id},
- });
-
- await insertExportLog({
- variables: {
- logs: [
- {
- bodyshopid: bodyshop.id,
- billid: bill.id,
- successful: true,
- message: JSON.stringify([t("general.labels.markedexported")]),
- useremail: currentUser.email,
- },
- ],
- },
- });
-
- if (!result.errors) {
- notification["success"]({
- message: t("bills.successes.markexported"),
- });
- } else {
- notification["error"]({
- message: t("bills.errors.saving", {
- error: JSON.stringify(result.errors),
- }),
- });
- }
- setLoading(false);
- //Get the owner details, populate it all back into the job.
- };
-
- const hasAccess = HasRbacAccess({
- bodyshop,
- authLevel,
- action: "bills:reexport",
+ const handleUpdate = async () => {
+ setLoading(true);
+ const result = await updateBill({
+ variables: { billId: bill.id }
});
- if (hasAccess)
- return (
-
- {t("bills.labels.markexported")}
-
- );
+ await insertExportLog({
+ variables: {
+ logs: [
+ {
+ bodyshopid: bodyshop.id,
+ billid: bill.id,
+ successful: true,
+ message: JSON.stringify([t("general.labels.markedexported")]),
+ useremail: currentUser.email
+ }
+ ]
+ }
+ });
- return <>>;
+ if (!result.errors) {
+ notification["success"]({
+ message: t("bills.successes.markexported")
+ });
+ } else {
+ notification["error"]({
+ message: t("bills.errors.saving", {
+ error: JSON.stringify(result.errors)
+ })
+ });
+ }
+ setLoading(false);
+ //Get the owner details, populate it all back into the job.
+ };
+
+ const hasAccess = HasRbacAccess({
+ bodyshop,
+ authLevel,
+ action: "bills:reexport"
+ });
+
+ if (hasAccess)
+ return (
+
+ {t("bills.labels.markexported")}
+
+ );
+
+ return <>>;
}
diff --git a/client/src/components/bill-print-button/bill-print-button.component.jsx b/client/src/components/bill-print-button/bill-print-button.component.jsx
index dd3bac20e..f63b9a13f 100644
--- a/client/src/components/bill-print-button/bill-print-button.component.jsx
+++ b/client/src/components/bill-print-button/bill-print-button.component.jsx
@@ -1,38 +1,38 @@
-import {Button, Space} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {GenerateDocument} from "../../utils/RenderTemplate";
-import {TemplateList} from "../../utils/TemplateConstants";
+import { Button, Space } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { GenerateDocument } from "../../utils/RenderTemplate";
+import { TemplateList } from "../../utils/TemplateConstants";
-export default function BillPrintButton({billid}) {
- const {t} = useTranslation();
- const [loading, setLoading] = useState(false);
- const Templates = TemplateList("job_special");
+export default function BillPrintButton({ billid }) {
+ const { t } = useTranslation();
+ const [loading, setLoading] = useState(false);
+ const Templates = TemplateList("job_special");
- const submitHandler = async () => {
- setLoading(true);
- try {
- await GenerateDocument(
- {
- name: Templates.parts_invoice_label_single.key,
- variables: {
- id: billid,
- },
- },
- {},
- "p"
- );
- } catch (e) {
- console.warn("Warning: Error generating a document.");
- }
- setLoading(false);
- };
+ const submitHandler = async () => {
+ setLoading(true);
+ try {
+ await GenerateDocument(
+ {
+ name: Templates.parts_invoice_label_single.key,
+ variables: {
+ id: billid
+ }
+ },
+ {},
+ "p"
+ );
+ } catch (e) {
+ console.warn("Warning: Error generating a document.");
+ }
+ setLoading(false);
+ };
- return (
-
-
- {t("bills.labels.printlabels")}
-
-
- );
+ return (
+
+
+ {t("bills.labels.printlabels")}
+
+
+ );
}
diff --git a/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx b/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx
index 3f5ab32b8..81da224d7 100644
--- a/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx
+++ b/client/src/components/bill-reexport-button/bill-reexport-button.component.jsx
@@ -1,79 +1,72 @@
-import {gql, useMutation} from "@apollo/client";
-import {Button, notification} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
+import { gql, useMutation } from "@apollo/client";
+import { Button, notification } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors";
-import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
+import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
- authLevel: selectAuthLevel,
+ bodyshop: selectBodyshop,
+ authLevel: selectAuthLevel
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(BillMarkForReexportButton);
+export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton);
-export function BillMarkForReexportButton({bodyshop, authLevel, bill}) {
- const {t} = useTranslation();
- const [loading, setLoading] = useState(false);
+export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
+ const { t } = useTranslation();
+ const [loading, setLoading] = useState(false);
- const [updateBill] = useMutation(gql`
- mutation UPDATE_BILL($billId: uuid!) {
- update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
- returning {
- id
- exported
- exported_at
- }
- }
+ const [updateBill] = useMutation(gql`
+ mutation UPDATE_BILL($billId: uuid!) {
+ update_bills(where: { id: { _eq: $billId } }, _set: { exported: false }) {
+ returning {
+ id
+ exported
+ exported_at
}
- `);
+ }
+ }
+ `);
- const handleUpdate = async () => {
- setLoading(true);
- const result = await updateBill({
- variables: {billId: bill.id},
- });
-
- if (!result.errors) {
- notification["success"]({
- message: t("bills.successes.reexport"),
- });
- } else {
- notification["error"]({
- message: t("bills.errors.saving", {
- error: JSON.stringify(result.errors),
- }),
- });
- }
- setLoading(false);
- //Get the owner details, populate it all back into the job.
- };
-
- const hasAccess = HasRbacAccess({
- bodyshop,
- authLevel,
- action: "bills:reexport",
+ const handleUpdate = async () => {
+ setLoading(true);
+ const result = await updateBill({
+ variables: { billId: bill.id }
});
- if (hasAccess)
- return (
-
- {t("bills.labels.markforreexport")}
-
- );
+ if (!result.errors) {
+ notification["success"]({
+ message: t("bills.successes.reexport")
+ });
+ } else {
+ notification["error"]({
+ message: t("bills.errors.saving", {
+ error: JSON.stringify(result.errors)
+ })
+ });
+ }
+ setLoading(false);
+ //Get the owner details, populate it all back into the job.
+ };
- return <>>;
+ const hasAccess = HasRbacAccess({
+ bodyshop,
+ authLevel,
+ action: "bills:reexport"
+ });
+
+ if (hasAccess)
+ return (
+
+ {t("bills.labels.markforreexport")}
+
+ );
+
+ return <>>;
}
diff --git a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx
index 5b35bd196..63d322b79 100644
--- a/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx
+++ b/client/src/components/billline-add-inventory/billline-add-inventory.component.jsx
@@ -1,151 +1,136 @@
-import {FileAddFilled} from "@ant-design/icons";
-import {useMutation} from "@apollo/client";
-import {Button, notification, Tooltip} from "antd";
-import {t} from "i18next";
+import { FileAddFilled } from "@ant-design/icons";
+import { useMutation } from "@apollo/client";
+import { Button, notification, Tooltip } from "antd";
+import { t } from "i18next";
import dayjs from "./../../utils/day";
-import React, {useState} from "react";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {INSERT_INVENTORY_AND_CREDIT} from "../../graphql/inventory.queries";
-import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
-import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility";
+import React, { useState } from "react";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
+import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
+import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import queryString from "query-string";
-import {useLocation} from "react-router-dom";
+import { useLocation } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
- currentUser: selectCurrentUser,
+ bodyshop: selectBodyshop,
+ currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(BilllineAddInventory);
+export default connect(mapStateToProps, mapDispatchToProps)(BilllineAddInventory);
-export function BilllineAddInventory({
- currentUser,
- bodyshop,
- billline,
- disabled,
- jobid,
- }) {
- const [loading, setLoading] = useState(false);
- const {billid} = queryString.parse(useLocation().search);
- const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
+export function BilllineAddInventory({ currentUser, bodyshop, billline, disabled, jobid }) {
+ const [loading, setLoading] = useState(false);
+ const { billid } = queryString.parse(useLocation().search);
+ const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
- const addToInventory = async () => {
- setLoading(true);
+ const addToInventory = async () => {
+ setLoading(true);
- //Check to make sure there are no existing items already in the inventory.
+ //Check to make sure there are no existing items already in the inventory.
- const cm = {
- vendorid: bodyshop.inhousevendorid,
- invoice_number: "ih",
- jobid: jobid,
- isinhouse: true,
- is_credit_memo: true,
- date: dayjs().format("YYYY-MM-DD"),
- federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
- state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
- local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
- total: 0,
- billlines: [
- {
- actual_price: billline.actual_price,
- actual_cost: billline.actual_cost,
- quantity: billline.quantity,
- line_desc: billline.line_desc,
- cost_center: billline.cost_center,
- deductedfromlbr: billline.deductedfromlbr,
- applicable_taxes: {
- local: billline.applicable_taxes.local,
- state: billline.applicable_taxes.state,
- federal: billline.applicable_taxes.federal,
- },
- },
- ],
- };
-
- cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
-
- const insertResult = await insertInventoryLine({
- variables: {
- joblineId:
- billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
- //Unfortunately, we can't send null as the GQL syntax validation fails.
- joblineStatus: bodyshop.md_order_statuses.default_returned,
- inv: {
- shopid: bodyshop.id,
- billlineid: billline.id,
- actual_price: billline.actual_price,
- actual_cost: billline.actual_cost,
- quantity: billline.quantity,
- line_desc: billline.line_desc,
- },
- cm: {...cm, billlines: {data: cm.billlines}}, //Fix structure for apollo insert.
- pol: {
- returnfrombill: billid,
- vendorid: bodyshop.inhousevendorid,
- deliver_by: dayjs().format("YYYY-MM-DD"),
- parts_order_lines: {
- data: [
- {
- line_desc: billline.line_desc,
-
- act_price: billline.actual_price,
- cost: billline.actual_cost,
- quantity: billline.quantity,
- job_line_id:
- billline.joblineid === "noline" ? null : billline.joblineid,
- part_type: billline.jobline && billline.jobline.part_type,
- cm_received: true,
- },
- ],
- },
- order_date: "2022-06-01",
- orderedby: currentUser.email,
- jobid: jobid,
- user_email: currentUser.email,
- return: true,
- status: "Ordered",
- },
- },
- refetchQueries: ["QUERY_BILL_BY_PK"],
- });
-
- if (!insertResult.errors) {
- notification.open({
- type: "success",
- message: t("inventory.successes.inserted"),
- });
- } else {
- notification.open({
- type: "error",
- message: t("inventory.errors.inserting", {
- error: JSON.stringify(insertResult.errors),
- }),
- });
+ const cm = {
+ vendorid: bodyshop.inhousevendorid,
+ invoice_number: "ih",
+ jobid: jobid,
+ isinhouse: true,
+ is_credit_memo: true,
+ date: dayjs().format("YYYY-MM-DD"),
+ federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
+ state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
+ local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
+ total: 0,
+ billlines: [
+ {
+ actual_price: billline.actual_price,
+ actual_cost: billline.actual_cost,
+ quantity: billline.quantity,
+ line_desc: billline.line_desc,
+ cost_center: billline.cost_center,
+ deductedfromlbr: billline.deductedfromlbr,
+ applicable_taxes: {
+ local: billline.applicable_taxes.local,
+ state: billline.applicable_taxes.state,
+ federal: billline.applicable_taxes.federal
+ }
}
-
- setLoading(false);
+ ]
};
- return (
-
- = billline.quantity
- }
- onClick={addToInventory}
- >
-
- {billline?.inventories?.length > 0 && (
- ({billline?.inventories?.length} in inv)
- )}
-
-
- );
+ cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
+
+ const insertResult = await insertInventoryLine({
+ variables: {
+ joblineId: billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
+ //Unfortunately, we can't send null as the GQL syntax validation fails.
+ joblineStatus: bodyshop.md_order_statuses.default_returned,
+ inv: {
+ shopid: bodyshop.id,
+ billlineid: billline.id,
+ actual_price: billline.actual_price,
+ actual_cost: billline.actual_cost,
+ quantity: billline.quantity,
+ line_desc: billline.line_desc
+ },
+ cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
+ pol: {
+ returnfrombill: billid,
+ vendorid: bodyshop.inhousevendorid,
+ deliver_by: dayjs().format("YYYY-MM-DD"),
+ parts_order_lines: {
+ data: [
+ {
+ line_desc: billline.line_desc,
+
+ act_price: billline.actual_price,
+ cost: billline.actual_cost,
+ quantity: billline.quantity,
+ job_line_id: billline.joblineid === "noline" ? null : billline.joblineid,
+ part_type: billline.jobline && billline.jobline.part_type,
+ cm_received: true
+ }
+ ]
+ },
+ order_date: "2022-06-01",
+ orderedby: currentUser.email,
+ jobid: jobid,
+ user_email: currentUser.email,
+ return: true,
+ status: "Ordered"
+ }
+ },
+ refetchQueries: ["QUERY_BILL_BY_PK"]
+ });
+
+ if (!insertResult.errors) {
+ notification.open({
+ type: "success",
+ message: t("inventory.successes.inserted")
+ });
+ } else {
+ notification.open({
+ type: "error",
+ message: t("inventory.errors.inserting", {
+ error: JSON.stringify(insertResult.errors)
+ })
+ });
+ }
+
+ setLoading(false);
+ };
+
+ return (
+
+ = billline.quantity}
+ onClick={addToInventory}
+ >
+
+ {billline?.inventories?.length > 0 && ({billline?.inventories?.length} in inv)
}
+
+
+ );
}
diff --git a/client/src/components/bills-list-table/bills-list-table.component.jsx b/client/src/components/bills-list-table/bills-list-table.component.jsx
index a03dea379..5db868406 100644
--- a/client/src/components/bills-list-table/bills-list-table.component.jsx
+++ b/client/src/components/bills-list-table/bills-list-table.component.jsx
@@ -1,236 +1,209 @@
-import {EditFilled, SyncOutlined} from "@ant-design/icons";
-import {Button, Card, Checkbox, Input, Space, Table} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectJobReadOnly} from "../../redux/application/application.selectors";
-import {setModalContext} from "../../redux/modals/modals.actions";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { EditFilled, SyncOutlined } from "@ant-design/icons";
+import { Button, Card, Checkbox, Input, Space, Table } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectJobReadOnly } from "../../redux/application/application.selectors";
+import { setModalContext } from "../../redux/modals/modals.actions";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
-import {DateFormatter} from "../../utils/DateFormatter";
-import {alphaSort, dateSort} from "../../utils/sorters";
-import {TemplateList} from "../../utils/TemplateConstants";
+import { DateFormatter } from "../../utils/DateFormatter";
+import { alphaSort, dateSort } from "../../utils/sorters";
+import { TemplateList } from "../../utils/TemplateConstants";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
- jobRO: selectJobReadOnly,
- bodyshop: selectBodyshop,
+ jobRO: selectJobReadOnly,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- setPartsOrderContext: (context) =>
- dispatch(setModalContext({context: context, modal: "partsOrder"})),
- setBillEnterContext: (context) =>
- dispatch(setModalContext({context: context, modal: "billEnter"})),
- setReconciliationContext: (context) =>
- dispatch(setModalContext({context: context, modal: "reconciliation"})),
+ setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
+ setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
+ setReconciliationContext: (context) => dispatch(setModalContext({ context: context, modal: "reconciliation" }))
});
export function BillsListTableComponent({
- bodyshop,
- jobRO,
- job,
- billsQuery,
- handleOnRowClick,
- setPartsOrderContext,
- setBillEnterContext,
- setReconciliationContext,
- }) {
- const {t} = useTranslation();
+ bodyshop,
+ jobRO,
+ job,
+ billsQuery,
+ handleOnRowClick,
+ setPartsOrderContext,
+ setBillEnterContext,
+ setReconciliationContext
+}) {
+ const { t } = useTranslation();
- const [state, setState] = useState({
- sortedInfo: {},
- });
- // const search = queryString.parse(useLocation().search);
- // const selectedBill = search.billid;
- const [searchText, setSearchText] = useState("");
+ const [state, setState] = useState({
+ sortedInfo: {}
+ });
+ // const search = queryString.parse(useLocation().search);
+ // const selectedBill = search.billid;
+ const [searchText, setSearchText] = useState("");
- const Templates = TemplateList("bill");
- const bills = billsQuery.data ? billsQuery.data.bills : [];
- const { refetch } = billsQuery;
- const recordActions = (record, showView = false) => (
+ const Templates = TemplateList("bill");
+ const bills = billsQuery.data ? billsQuery.data.bills : [];
+ const { refetch } = billsQuery;
+ const recordActions = (record, showView = false) => (
+
+ {showView && (
+ handleOnRowClick(record)}>
+
+
+ )}
+
+
+
+ {record.isinhouse && (
+
+ )}
+
+ );
+ const columns = [
+ {
+ title: t("bills.fields.vendorname"),
+ dataIndex: "vendorname",
+ key: "vendorname",
+ sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
+ sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
+ render: (text, record) => {record.vendor.name}
+ },
+ {
+ title: t("bills.fields.invoice_number"),
+ dataIndex: "invoice_number",
+ key: "invoice_number",
+ sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
+ sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order
+ },
+ {
+ title: t("bills.fields.date"),
+ dataIndex: "date",
+ key: "date",
+ sorter: (a, b) => dateSort(a.date, b.date),
+ sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
+ render: (text, record) => {record.date}
+ },
+ {
+ title: t("bills.fields.total"),
+ dataIndex: "total",
+ key: "total",
+ sorter: (a, b) => a.total - b.total,
+ sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
+ render: (text, record) => {record.total}
+ },
+ {
+ title: t("bills.fields.is_credit_memo"),
+ dataIndex: "is_credit_memo",
+ key: "is_credit_memo",
+ sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
+ sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
+ render: (text, record) =>
+ },
+ {
+ title: t("bills.fields.exported"),
+ dataIndex: "exported",
+ key: "exported",
+ sorter: (a, b) => a.exported - b.exported,
+ sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
+ render: (text, record) =>
+ },
+ {
+ title: t("general.labels.actions"),
+ dataIndex: "actions",
+ key: "actions",
+ render: (text, record) => recordActions(record, true)
+ }
+ ];
+
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
+
+ const filteredBills = bills
+ ? searchText === ""
+ ? bills
+ : bills.filter(
+ (b) =>
+ (b.invoice_number || "").toLowerCase().includes(searchText.toLowerCase()) ||
+ (b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) ||
+ (b.total || "").toString().toLowerCase().includes(searchText.toLowerCase())
+ )
+ : [];
+
+ return (
+
- {showView && (
- handleOnRowClick(record)}>
-
-
- )}
-
-
-
- {record.isinhouse && (
-
- )}
-
- );
- const columns = [
- {
- title: t("bills.fields.vendorname"),
- dataIndex: "vendorname",
- key: "vendorname",
- sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
- sortOrder:
- state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
- render: (text, record) => {record.vendor.name} ,
- },
- {
- title: t("bills.fields.invoice_number"),
- dataIndex: "invoice_number",
- key: "invoice_number",
- sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
- sortOrder:
- state.sortedInfo.columnKey === "invoice_number" &&
- state.sortedInfo.order,
- },
- {
- title: t("bills.fields.date"),
- dataIndex: "date",
- key: "date",
- sorter: (a, b) => dateSort(a.date, b.date),
- sortOrder:
- state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
- render: (text, record) => {record.date} ,
- },
- {
- title: t("bills.fields.total"),
- dataIndex: "total",
- key: "total",
- sorter: (a, b) => a.total - b.total,
- sortOrder:
- state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
- render: (text, record) => (
- {record.total}
- ),
- },
- {
- title: t("bills.fields.is_credit_memo"),
- dataIndex: "is_credit_memo",
- key: "is_credit_memo",
- sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
- sortOrder:
- state.sortedInfo.columnKey === "is_credit_memo" &&
- state.sortedInfo.order,
- render: (text, record) => ,
- },
- {
- title: t("bills.fields.exported"),
- dataIndex: "exported",
- key: "exported",
- sorter: (a, b) => a.exported - b.exported,
- sortOrder:
- state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
- render: (text, record) => ,
- },
- {
- title: t("general.labels.actions"),
- dataIndex: "actions",
- key: "actions",
- render: (text, record) => recordActions(record, true),
- },
- ];
-
- const handleTableChange = (pagination, filters, sorter) => {
- setState({...state, filteredInfo: filters, sortedInfo: sorter});
- };
-
- const filteredBills = bills
- ? searchText === ""
- ? bills
- : bills.filter(
- (b) =>
- (b.invoice_number || "")
- .toLowerCase()
- .includes(searchText.toLowerCase()) ||
- (b.vendor.name || "")
- .toLowerCase()
- .includes(searchText.toLowerCase()) ||
- (b.total || "")
- .toString()
- .toLowerCase()
- .includes(searchText.toLowerCase())
- )
- : [];
-
- return (
-
- refetch()}>
-
-
- {job && job.converted ? (
- <>
- {
- setBillEnterContext({
- actions: {refetch: billsQuery.refetch},
- context: {
- job,
- },
- });
- }}
- >
- {t("jobs.actions.postbills")}
-
- {
- setReconciliationContext({
- actions: {refetch: billsQuery.refetch},
- context: {
- job,
- bills: (billsQuery.data && billsQuery.data.bills) || [],
- },
- });
- }}
- >
- {t("jobs.actions.reconcile")}
-
- >
- ) : null}
-
- {
- e.preventDefault();
- setSearchText(e.target.value);
- }}
- />
-
- }
- >
- refetch()}>
+
+
+ {job && job.converted ? (
+ <>
+ {
+ setBillEnterContext({
+ actions: { refetch: billsQuery.refetch },
+ context: {
+ job
+ }
+ });
}}
- columns={columns}
- rowKey="id"
- dataSource={filteredBills}
- onChange={handleTableChange}
- />
-
- );
+ >
+ {t("jobs.actions.postbills")}
+
+ {
+ setReconciliationContext({
+ actions: { refetch: billsQuery.refetch },
+ context: {
+ job,
+ bills: (billsQuery.data && billsQuery.data.bills) || []
+ }
+ });
+ }}
+ >
+ {t("jobs.actions.reconcile")}
+
+ >
+ ) : null}
+
+ {
+ e.preventDefault();
+ setSearchText(e.target.value);
+ }}
+ />
+
+ }
+ >
+
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(BillsListTableComponent);
+export default connect(mapStateToProps, mapDispatchToProps)(BillsListTableComponent);
diff --git a/client/src/components/bills-vendors-list/bills-vendors-list.component.jsx b/client/src/components/bills-vendors-list/bills-vendors-list.component.jsx
index e6cf58cb9..433f18960 100644
--- a/client/src/components/bills-vendors-list/bills-vendors-list.component.jsx
+++ b/client/src/components/bills-vendors-list/bills-vendors-list.component.jsx
@@ -1,121 +1,112 @@
-import React, {useState} from "react";
-import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries";
-import {useQuery} from "@apollo/client";
+import React, { useState } from "react";
+import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
+import { useQuery } from "@apollo/client";
import queryString from "query-string";
-import {useLocation, useNavigate} from "react-router-dom";
-import {Input, Table} from "antd";
-import {useTranslation} from "react-i18next";
-import {alphaSort} from "../../utils/sorters";
+import { useLocation, useNavigate } from "react-router-dom";
+import { Input, Table } from "antd";
+import { useTranslation } from "react-i18next";
+import { alphaSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component";
export default function BillsVendorsList() {
- const search = queryString.parse(useLocation().search);
- const history = useNavigate();
+ const search = queryString.parse(useLocation().search);
+ const history = useNavigate();
- const {loading, error, data} = useQuery(QUERY_ALL_VENDORS, {
- fetchPolicy: "network-only",
- nextFetchPolicy: "network-only",
- });
+ const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
- const {t} = useTranslation();
+ const { t } = useTranslation();
- const [state, setState] = useState({
- sortedInfo: {},
- search: "",
- });
+ const [state, setState] = useState({
+ sortedInfo: {},
+ search: ""
+ });
- const handleTableChange = (pagination, filters, sorter) => {
- setState({...state, filteredInfo: filters, sortedInfo: sorter});
- };
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
- const columns = [
- {
- title: t("vendors.fields.name"),
- dataIndex: "name",
- key: "name",
- sorter: (a, b) => alphaSort(a.name, b.name),
- sortOrder:
- state.sortedInfo.columnKey === "name" && state.sortedInfo.order,
+ const columns = [
+ {
+ title: t("vendors.fields.name"),
+ dataIndex: "name",
+ key: "name",
+ sorter: (a, b) => alphaSort(a.name, b.name),
+ sortOrder: state.sortedInfo.columnKey === "name" && state.sortedInfo.order
+ },
+ {
+ title: t("vendors.fields.cost_center"),
+ dataIndex: "cost_center",
+ key: "cost_center",
+ sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
+ sortOrder: state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order
+ },
+ {
+ title: t("vendors.fields.city"),
+ dataIndex: "city",
+ key: "city"
+ }
+ ];
+
+ const handleOnRowClick = (record) => {
+ if (record) {
+ delete search.billid;
+ if (record.id) {
+ search.vendorid = record.id;
+ history.push({ search: queryString.stringify(search) });
+ }
+ } else {
+ delete search.vendorid;
+ history.push({ search: queryString.stringify(search) });
+ }
+ };
+
+ const handleSearch = (e) => {
+ setState({ ...state, search: e.target.value });
+ };
+
+ if (error) return ;
+
+ const dataSource = state.search
+ ? data.vendors.filter(
+ (v) =>
+ (v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (v.cost_center || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (v.city || "").toLowerCase().includes(state.search.toLowerCase())
+ )
+ : (data && data.vendors) || [];
+
+ return (
+ {
+ return (
+
+
+
+ );
+ }}
+ dataSource={dataSource}
+ pagination={{ position: "top" }}
+ columns={columns}
+ rowKey="id"
+ onChange={handleTableChange}
+ rowSelection={{
+ onSelect: (record) => {
+ handleOnRowClick(record);
},
- {
- title: t("vendors.fields.cost_center"),
- dataIndex: "cost_center",
- key: "cost_center",
- sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
- sortOrder:
- state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
- },
- {
- title: t("vendors.fields.city"),
- dataIndex: "city",
- key: "city",
- },
- ];
-
- const handleOnRowClick = (record) => {
- if (record) {
- delete search.billid;
- if (record.id) {
- search.vendorid = record.id;
- history.push({search: queryString.stringify(search)});
- }
- } else {
- delete search.vendorid;
- history.push({search: queryString.stringify(search)});
- }
- };
-
- const handleSearch = (e) => {
- setState({...state, search: e.target.value});
- };
-
- if (error) return ;
-
- const dataSource = state.search
- ? data.vendors.filter(
- (v) =>
- (v.name || "").toLowerCase().includes(state.search.toLowerCase()) ||
- (v.cost_center || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (v.city || "").toLowerCase().includes(state.search.toLowerCase())
- )
- : (data && data.vendors) || [];
-
- return (
- {
- return (
-
-
-
- );
- }}
- dataSource={dataSource}
- pagination={{position: "top"}}
- columns={columns}
- rowKey="id"
- onChange={handleTableChange}
- rowSelection={{
- onSelect: (record) => {
- handleOnRowClick(record);
- },
- selectedRowKeys: [search.vendorid],
- type: "radio",
- }}
- onRow={(record, rowIndex) => {
- return {
- onClick: (event) => {
- handleOnRowClick(record);
- }, // click row
- };
- }}
- />
- );
+ selectedRowKeys: [search.vendorid],
+ type: "radio"
+ }}
+ onRow={(record, rowIndex) => {
+ return {
+ onClick: (event) => {
+ handleOnRowClick(record);
+ } // click row
+ };
+ }}
+ />
+ );
}
diff --git a/client/src/components/breadcrumbs/breadcrumbs.component.jsx b/client/src/components/breadcrumbs/breadcrumbs.component.jsx
index 360eb480d..93a9d0192 100644
--- a/client/src/components/breadcrumbs/breadcrumbs.component.jsx
+++ b/client/src/components/breadcrumbs/breadcrumbs.component.jsx
@@ -1,64 +1,63 @@
-import {HomeFilled} from "@ant-design/icons";
-import {Breadcrumb, Col, Row} from "antd";
+import { HomeFilled } from "@ant-design/icons";
+import { Breadcrumb, Col, Row } from "antd";
import React from "react";
-import {connect} from "react-redux";
-import {Link} from "react-router-dom";
-import {createStructuredSelector} from "reselect";
-import {selectBreadcrumbs} from "../../redux/application/application.selectors";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { connect } from "react-redux";
+import { Link } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { selectBreadcrumbs } from "../../redux/application/application.selectors";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
import GlobalSearchOs from "../global-search/global-search-os.component";
import "./breadcrumbs.styles.scss";
-import {useSplitTreatments} from "@splitsoftware/splitio-react";
+import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
- breadcrumbs: selectBreadcrumbs,
- bodyshop: selectBodyshop,
+ breadcrumbs: selectBreadcrumbs,
+ bodyshop: selectBodyshop
});
-export function BreadCrumbs({breadcrumbs, bodyshop}) {
-
- const {treatments: {OpenSearch}} = useSplitTreatments({
- attributes: {},
- names: ["OpenSearch"],
- splitKey: bodyshop && bodyshop.imexshopid,
- });
- // TODO - Client Update - Technically key is not doing anything here
- return (
-
-
-
- {" "}
- {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
- ""}
-
- ),
- },
- ...breadcrumbs.map((item) =>
- item.link
- ? {
- key: item.label,
- title: {item.label},
- }
- : {
- key: item.label,
- title: item.label,
- }
- ),
- ]}
- />
-
-
- {OpenSearch.treatment === "on" ? : }
-
-
- );
+export function BreadCrumbs({ breadcrumbs, bodyshop }) {
+ const {
+ treatments: { OpenSearch }
+ } = useSplitTreatments({
+ attributes: {},
+ names: ["OpenSearch"],
+ splitKey: bodyshop && bodyshop.imexshopid
+ });
+ // TODO - Client Update - Technically key is not doing anything here
+ return (
+
+
+
+ {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || ""}
+
+ )
+ },
+ ...breadcrumbs.map((item) =>
+ item.link
+ ? {
+ key: item.label,
+ title: {item.label}
+ }
+ : {
+ key: item.label,
+ title: item.label
+ }
+ )
+ ]}
+ />
+
+
+ {OpenSearch.treatment === "on" ? : }
+
+
+ );
}
export default connect(mapStateToProps, null)(BreadCrumbs);
diff --git a/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table-modal.container.jsx b/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table-modal.container.jsx
index 04a49ca36..04c49e2db 100644
--- a/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table-modal.container.jsx
+++ b/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table-modal.container.jsx
@@ -1,99 +1,87 @@
-import {Button, Form, Modal} from "antd";
-import React, {useEffect, useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {logImEXEvent} from "../../firebase/firebase.utils";
-import {toggleModalVisible} from "../../redux/modals/modals.actions";
-import {selectCaBcEtfTableConvert} from "../../redux/modals/modals.selectors";
-import {GenerateDocument} from "../../utils/RenderTemplate";
-import {TemplateList} from "../../utils/TemplateConstants";
+import { Button, Form, Modal } from "antd";
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { logImEXEvent } from "../../firebase/firebase.utils";
+import { toggleModalVisible } from "../../redux/modals/modals.actions";
+import { selectCaBcEtfTableConvert } from "../../redux/modals/modals.selectors";
+import { GenerateDocument } from "../../utils/RenderTemplate";
+import { TemplateList } from "../../utils/TemplateConstants";
import CaBcEtfTableModalComponent from "./ca-bc-etf-table.modal.component";
const mapStateToProps = createStructuredSelector({
- caBcEtfTableModal: selectCaBcEtfTableConvert,
+ caBcEtfTableModal: selectCaBcEtfTableConvert
});
const mapDispatchToProps = (dispatch) => ({
- toggleModalVisible: () =>
- dispatch(toggleModalVisible("ca_bc_eftTableConvert")),
+ toggleModalVisible: () => dispatch(toggleModalVisible("ca_bc_eftTableConvert"))
});
-export function ContractsFindModalContainer({
- caBcEtfTableModal,
- toggleModalVisible,
- }) {
- const {t} = useTranslation();
+export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisible }) {
+ const { t } = useTranslation();
- const {open} = caBcEtfTableModal;
- const [loading, setLoading] = useState(false);
- const [form] = Form.useForm();
- const EtfTemplate = TemplateList("special").ca_bc_etf_table;
+ const { open } = caBcEtfTableModal;
+ const [loading, setLoading] = useState(false);
+ const [form] = Form.useForm();
+ const EtfTemplate = TemplateList("special").ca_bc_etf_table;
- const handleFinish = async (values) => {
- logImEXEvent("ca_bc_etf_table_parse");
- setLoading(true);
- const claimNumbers = [];
- values.table.split("\n").forEach((row, idx, arr) => {
- const {1: claim, 2: shortclaim, 4: amount} = row.split("\t");
- if (!claim || !shortclaim) return;
- const trimmedShortClaim = shortclaim.trim();
- // const trimmedClaim = claim.trim();
- if (amount.slice(-1) === "-") {
- }
+ const handleFinish = async (values) => {
+ logImEXEvent("ca_bc_etf_table_parse");
+ setLoading(true);
+ const claimNumbers = [];
+ values.table.split("\n").forEach((row, idx, arr) => {
+ const { 1: claim, 2: shortclaim, 4: amount } = row.split("\t");
+ if (!claim || !shortclaim) return;
+ const trimmedShortClaim = shortclaim.trim();
+ // const trimmedClaim = claim.trim();
+ if (amount.slice(-1) === "-") {
+ }
- claimNumbers.push({
- claim: trimmedShortClaim,
- amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount,
- });
- });
+ claimNumbers.push({
+ claim: trimmedShortClaim,
+ amount: amount.slice(-1) === "-" ? parseFloat(amount) * -1 : amount
+ });
+ });
- await GenerateDocument(
- {
- name: EtfTemplate.key,
- variables: {
- claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
- claimdata: claimNumbers,
- },
- },
- {},
- values.sendby === "email" ? "e" : "p"
- );
- setLoading(false);
- };
-
- useEffect(() => {
- if (open) {
- form.resetFields();
+ await GenerateDocument(
+ {
+ name: EtfTemplate.key,
+ variables: {
+ claimNumbers: `%(${claimNumbers.map((c) => c.claim).join("|")})%`,
+ claimdata: claimNumbers
}
- }, [open, form]);
-
- return (
- toggleModalVisible()}
- onOk={() => toggleModalVisible()}
- destroyOnClose
- forceRender
- >
-
-
+ },
+ {},
+ values.sendby === "email" ? "e" : "p"
);
+ setLoading(false);
+ };
+
+ useEffect(() => {
+ if (open) {
+ form.resetFields();
+ }
+ }, [open, form]);
+
+ return (
+ toggleModalVisible()}
+ onOk={() => toggleModalVisible()}
+ destroyOnClose
+ forceRender
+ >
+
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ContractsFindModalContainer);
+export default connect(mapStateToProps, mapDispatchToProps)(ContractsFindModalContainer);
diff --git a/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table.modal.component.jsx b/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table.modal.component.jsx
index 67659eaa6..e9224ae6b 100644
--- a/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table.modal.component.jsx
+++ b/client/src/components/ca-bc-etf-table-modal/ca-bc-etf-table.modal.component.jsx
@@ -1,42 +1,38 @@
-import {Form, Input, Radio} from "antd";
+import { Form, Input, Radio } from "antd";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
-export function PartsReceiveModalComponent({bodyshop, form}) {
- const {t} = useTranslation();
+export function PartsReceiveModalComponent({ bodyshop, form }) {
+ const { t } = useTranslation();
- return (
-
-
-
-
-
-
- {t("general.labels.email")}
- {t("general.labels.print")}
-
-
-
- );
+ return (
+
+
+
+
+
+
+ {t("general.labels.email")}
+ {t("general.labels.print")}
+
+
+
+ );
}
diff --git a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx
index 1cb2f3385..cbdd26770 100644
--- a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx
+++ b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx
@@ -1,50 +1,45 @@
-import React, {useState} from "react";
-import {Button, Form, InputNumber, Popover} from "antd";
-import {logImEXEvent} from "../../firebase/firebase.utils";
-import {useTranslation} from "react-i18next";
-import {CalculatorFilled} from "@ant-design/icons";
+import React, { useState } from "react";
+import { Button, Form, InputNumber, Popover } from "antd";
+import { logImEXEvent } from "../../firebase/firebase.utils";
+import { useTranslation } from "react-i18next";
+import { CalculatorFilled } from "@ant-design/icons";
-export default function CABCpvrtCalculator({disabled, form}) {
- const [visibility, setVisibility] = useState(false);
+export default function CABCpvrtCalculator({ disabled, form }) {
+ const [visibility, setVisibility] = useState(false);
- const {t} = useTranslation();
+ const { t } = useTranslation();
- const handleFinish = async (values) => {
- logImEXEvent("job_ca_bc_pvrt_calculate");
- form.setFieldsValue({
- ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
- });
- form.setFields([{name: "ca_bc_pvrt", touched: true}]);
- setVisibility(false);
- };
+ const handleFinish = async (values) => {
+ logImEXEvent("job_ca_bc_pvrt_calculate");
+ form.setFieldsValue({
+ ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2)
+ });
+ form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
+ setVisibility(false);
+ };
- const popContent = (
-
-
-
-
-
-
-
-
- {t("general.actions.calculate")}
-
- setVisibility(false)}>Close
-
-
- );
+ const popContent = (
+
+
+
+
+
+
+
+
+ {t("general.actions.calculate")}
+
+ setVisibility(false)}>Close
+
+
+ );
- return (
-
- setVisibility(true)}>
-
-
-
- );
+ return (
+
+ setVisibility(true)}>
+
+
+
+ );
}
diff --git a/client/src/components/card-payment-modal/card-payment-modal.component..jsx b/client/src/components/card-payment-modal/card-payment-modal.component..jsx
index 581fe7334..bec81dd2e 100644
--- a/client/src/components/card-payment-modal/card-payment-modal.component..jsx
+++ b/client/src/components/card-payment-modal/card-payment-modal.component..jsx
@@ -1,357 +1,328 @@
-import {DeleteFilled} from "@ant-design/icons";
-import {useLazyQuery, useMutation} from "@apollo/client";
-import {Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic,} from "antd";
+import { DeleteFilled } from "@ant-design/icons";
+import { useLazyQuery, useMutation } from "@apollo/client";
+import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
import axios from "axios";
import dayjs from "../../utils/day";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS,} from "../../graphql/payment_response.queries";
-import {INSERT_NEW_PAYMENT} from "../../graphql/payments.queries";
-import {insertAuditTrail} from "../../redux/application/application.actions";
-import {toggleModalVisible} from "../../redux/modals/modals.actions";
-import {selectCardPayment} from "../../redux/modals/modals.selectors";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
+import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
+import { insertAuditTrail } from "../../redux/application/application.actions";
+import { toggleModalVisible } from "../../redux/modals/modals.actions";
+import { selectCardPayment } from "../../redux/modals/modals.selectors";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({
- cardPaymentModal: selectCardPayment,
- bodyshop: selectBodyshop,
+ cardPaymentModal: selectCardPayment,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- insertAuditTrail: ({jobid, operation, type}) =>
- dispatch(insertAuditTrail({jobid, operation, type})),
- toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
+ insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
+ toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
});
-const CardPaymentModalComponent = ({
- bodyshop,
- cardPaymentModal,
- toggleModalVisible,
- insertAuditTrail,
- }) => {
- const {context} = cardPaymentModal;
+const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
+ const { context } = cardPaymentModal;
- const [form] = Form.useForm();
+ const [form] = Form.useForm();
- const [loading, setLoading] = useState(false);
- const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
- const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
- const {t} = useTranslation();
+ const [loading, setLoading] = useState(false);
+ const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
+ const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
+ const { t } = useTranslation();
- const [, {data, refetch, queryLoading}] = useLazyQuery(
- QUERY_RO_AND_OWNER_BY_JOB_PKS,
- {
- variables: {jobids: [context.jobid]},
- skip: true,
+ const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
+ variables: { jobids: [context.jobid] },
+ skip: true
+ });
+
+ console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
+ //Initialize the intellipay window.
+ const SetIntellipayCallbackFunctions = () => {
+ console.log("*** Set IntelliPay callback functions.");
+ window.intellipay.runOnClose(() => {
+ //window.intellipay.initialize();
+ });
+
+ window.intellipay.runOnApproval(async function (response) {
+ console.warn("*** Running On Approval Script ***");
+ form.setFieldValue("paymentResponse", response);
+ form.submit();
+ });
+
+ window.intellipay.runOnNonApproval(async function (response) {
+ // Mutate unsuccessful payment
+
+ const { payments } = form.getFieldsValue();
+
+ await insertPaymentResponse({
+ variables: {
+ paymentResponse: payments.map((payment) => ({
+ amount: payment.amount,
+ bodyshopid: bodyshop.id,
+ jobid: payment.jobid,
+ declinereason: response.declinereason,
+ ext_paymentid: response.paymentid.toString(),
+ successful: false,
+ response
+ }))
}
- );
+ });
- console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
- //Initialize the intellipay window.
- const SetIntellipayCallbackFunctions = () => {
- console.log("*** Set IntelliPay callback functions.");
- window.intellipay.runOnClose(() => {
- //window.intellipay.initialize();
- });
+ payments.forEach((payment) =>
+ insertAuditTrail({
+ jobid: payment.jobid,
+ operation: AuditTrailMapping.failedpayment(),
+ type: "failedpayment"
+ })
+ );
+ });
+ };
- window.intellipay.runOnApproval(async function (response) {
- console.warn("*** Running On Approval Script ***");
- form.setFieldValue("paymentResponse", response);
- form.submit();
- });
+ const handleFinish = async (values) => {
+ try {
+ await insertPayment({
+ variables: {
+ paymentInput: values.payments.map((payment) => ({
+ amount: payment.amount,
+ transactionid: (values.paymentResponse.paymentid || "").toString(),
+ payer: t("payments.labels.customer"),
+ type: values.paymentResponse.cardbrand,
+ jobid: payment.jobid,
+ date: dayjs(Date.now()),
+ payment_responses: {
+ data: [
+ {
+ amount: payment.amount,
+ bodyshopid: bodyshop.id,
- window.intellipay.runOnNonApproval(async function (response) {
- // Mutate unsuccessful payment
-
- const {payments} = form.getFieldsValue();
-
- await insertPaymentResponse({
- variables: {
- paymentResponse: payments.map((payment) => ({
- amount: payment.amount,
- bodyshopid: bodyshop.id,
- jobid: payment.jobid,
- declinereason: response.declinereason,
- ext_paymentid: response.paymentid.toString(),
- successful: false,
- response,
- })),
- },
- });
-
- payments.forEach((payment) =>
- insertAuditTrail({
- jobid: payment.jobid,
- operation: AuditTrailMapping.failedpayment(),
- type: "failedpayment",})
- );
- });
- };
-
- const handleFinish = async (values) => {
- try {
- await insertPayment({
- variables: {
- paymentInput: values.payments.map((payment) => ({
- amount: payment.amount,
- transactionid: (values.paymentResponse.paymentid || "").toString(),
- payer: t("payments.labels.customer"),
- type: values.paymentResponse.cardbrand,
- jobid: payment.jobid,
- date: dayjs(Date.now()),
- payment_responses: {
- data: [
- {
- amount: payment.amount,
- bodyshopid: bodyshop.id,
-
- jobid: payment.jobid,
- declinereason: values.paymentResponse.declinereason,
- ext_paymentid: values.paymentResponse.paymentid.toString(),
- successful: true,
- response: values.paymentResponse,
- },
- ],
- },
- })),
- },
- refetchQueries: ["GET_JOB_BY_PK"],
- });
- toggleModalVisible();
- } catch (error) {
- console.error(error);
- notification.open({
- type: "error",
- message: t("payments.errors.inserting", {error: error.message}),
- });
- } finally {
- setLoading(false);
- }
- };
-
- const handleIntelliPayCharge = async () => {
- setLoading(true);
-
- //Validate
- try {
- await form.validateFields();
- } catch (error) {
- setLoading(false);
- return;
- }
-
- try {
- const response = await axios.post("/intellipay/lightbox_credentials", {
- bodyshop,
- refresh: !!window.intellipay,
- });
-
- if (window.intellipay) {
- // eslint-disable-next-line no-eval
- eval(response.data);
- SetIntellipayCallbackFunctions();
- window.intellipay.autoOpen();
- } else {
- var rg = document.createRange();
- let node = rg.createContextualFragment(response.data);
- document.documentElement.appendChild(node);
- SetIntellipayCallbackFunctions();
- window.intellipay.isAutoOpen = true;
- window.intellipay.initialize();
+ jobid: payment.jobid,
+ declinereason: values.paymentResponse.declinereason,
+ ext_paymentid: values.paymentResponse.paymentid.toString(),
+ successful: true,
+ response: values.paymentResponse
+ }
+ ]
}
- } catch (error) {
- notification.open({
- type: "error",
- message: t("job_payments.notifications.error.openingip"),
- });
- setLoading(false);
- }
- };
+ }))
+ },
+ refetchQueries: ["GET_JOB_BY_PK"]
+ });
+ toggleModalVisible();
+ } catch (error) {
+ console.error(error);
+ notification.open({
+ type: "error",
+ message: t("payments.errors.inserting", { error: error.message })
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
- return (
-
-
-
- {(fields, {add, remove, move}) => {
- return (
-
- {fields.map((field, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- remove(field.name);
- }}
- />
-
-
-
- ))}
-
- {
- add();
- }}
- style={{width: "100%"}}
- >
- {t("general.actions.add")}
-
-
-
- );
- }}
-
+ const handleIntelliPayCharge = async () => {
+ setLoading(true);
-
- prevValues.payments?.map((p) => p?.jobid).join() !==
- curValues.payments?.map((p) => p?.jobid).join()
- }
+ //Validate
+ try {
+ await form.validateFields();
+ } catch (error) {
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const response = await axios.post("/intellipay/lightbox_credentials", {
+ bodyshop,
+ refresh: !!window.intellipay
+ });
+
+ if (window.intellipay) {
+ // eslint-disable-next-line no-eval
+ eval(response.data);
+ SetIntellipayCallbackFunctions();
+ window.intellipay.autoOpen();
+ } else {
+ var rg = document.createRange();
+ let node = rg.createContextualFragment(response.data);
+ document.documentElement.appendChild(node);
+ SetIntellipayCallbackFunctions();
+ window.intellipay.isAutoOpen = true;
+ window.intellipay.initialize();
+ }
+ } catch (error) {
+ notification.open({
+ type: "error",
+ message: t("job_payments.notifications.error.openingip")
+ });
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ {(fields, { add, remove, move }) => {
+ return (
+
+ {fields.map((field, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ remove(field.name);
+ }}
+ />
+
+
+
+ ))}
+
+ {
+ add();
+ }}
+ style={{ width: "100%" }}
>
- {() => {
- console.log("Updating the owner info section.");
- //If all of the job ids have been fileld in, then query and update the IP field.
- const {payments} = form.getFieldsValue();
- if (
- payments?.length > 0 &&
- payments?.filter((p) => p?.jobid).length === payments?.length
- ) {
- console.log("**Calling refetch.");
- refetch({jobids: payments.map((p) => p.jobid)});
- }
- console.log(
- "Acc info",
- data,
- payments && data && data.jobs.length > 0
- ? data.jobs.map((j) => j.ro_number).join(", ")
- : null
- );
- return (
- <>
- 0
- ? data.jobs.map((j) => j.ro_number).join(", ")
- : null
- }
- />
- 0
- ? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
- : null
- }
- />
- >
- );
- }}
-
-
- prevValues.payments?.map((p) => p?.amount).join() !==
- curValues.payments?.map((p) => p?.amount).join()
- }
- >
- {() => {
- const {payments} = form.getFieldsValue();
- const totalAmountToCharge = payments?.reduce((acc, val) => {
- return acc + (val?.amount || 0);
- }, 0);
+ {t("general.actions.add")}
+
+
+
+ );
+ }}
+
- return (
-
-
-
- 0)}
- onClick={handleIntelliPayCharge}
- >
- {t("job_payments.buttons.proceedtopayment")}
-
-
- );
- }}
-
+
+ prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
+ }
+ >
+ {() => {
+ console.log("Updating the owner info section.");
+ //If all of the job ids have been fileld in, then query and update the IP field.
+ const { payments } = form.getFieldsValue();
+ if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
+ console.log("**Calling refetch.");
+ refetch({ jobids: payments.map((p) => p.jobid) });
+ }
+ console.log(
+ "Acc info",
+ data,
+ payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
+ );
+ return (
+ <>
+ 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
+ }
+ />
+ 0 ? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea : null
+ }
+ />
+ >
+ );
+ }}
+
+
+ prevValues.payments?.map((p) => p?.amount).join() !== curValues.payments?.map((p) => p?.amount).join()
+ }
+ >
+ {() => {
+ const { payments } = form.getFieldsValue();
+ const totalAmountToCharge = payments?.reduce((acc, val) => {
+ return acc + (val?.amount || 0);
+ }, 0);
- {/* Lightbox payment response when it is completed */}
-
-
-
-
-
-
- );
+ return (
+
+
+
+ 0)}
+ onClick={handleIntelliPayCharge}
+ >
+ {t("job_payments.buttons.proceedtopayment")}
+
+
+ );
+ }}
+
+
+ {/* Lightbox payment response when it is completed */}
+
+
+
+
+
+
+ );
};
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(CardPaymentModalComponent);
+export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalComponent);
diff --git a/client/src/components/card-payment-modal/card-payment-modal.container..jsx b/client/src/components/card-payment-modal/card-payment-modal.container..jsx
index 4984ddce5..2f56615bd 100644
--- a/client/src/components/card-payment-modal/card-payment-modal.container..jsx
+++ b/client/src/components/card-payment-modal/card-payment-modal.container..jsx
@@ -1,57 +1,50 @@
-import {Button, Modal} from "antd";
+import { Button, Modal } from "antd";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {toggleModalVisible} from "../../redux/modals/modals.actions";
-import {selectCardPayment} from "../../redux/modals/modals.selectors";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { toggleModalVisible } from "../../redux/modals/modals.actions";
+import { selectCardPayment } from "../../redux/modals/modals.selectors";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import CardPaymentModalComponent from "./card-payment-modal.component.";
const mapStateToProps = createStructuredSelector({
- cardPaymentModal: selectCardPayment,
- bodyshop: selectBodyshop,
+ cardPaymentModal: selectCardPayment,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
+ toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
});
-function CardPaymentModalContainer({
- cardPaymentModal,
- toggleModalVisible,
- bodyshop,
- }) {
- const {open} = cardPaymentModal;
- const {t} = useTranslation();
+function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodyshop }) {
+ const { open } = cardPaymentModal;
+ const { t } = useTranslation();
- const handleCancel = () => {
- toggleModalVisible();
- };
+ const handleCancel = () => {
+ toggleModalVisible();
+ };
- const handleOK = () => {
- toggleModalVisible();
- };
+ const handleOK = () => {
+ toggleModalVisible();
+ };
- return (
-
- {t("job_payments.buttons.goback")}
- ,
- ]}
- width="80%"
- destroyOnClose
- >
-
-
- );
+ return (
+
+ {t("job_payments.buttons.goback")}
+
+ ]}
+ width="80%"
+ destroyOnClose
+ >
+
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(CardPaymentModalContainer);
+export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalContainer);
diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx
index d82115e72..0541cd167 100644
--- a/client/src/components/chat-affix/chat-affix.container.jsx
+++ b/client/src/components/chat-affix/chat-affix.container.jsx
@@ -1,100 +1,97 @@
-import {useApolloClient} from "@apollo/client";
-import {getToken, onMessage} from "@firebase/messaging";
-import {Button, notification, Space} from "antd";
+import { useApolloClient } from "@apollo/client";
+import { getToken, onMessage } from "@firebase/messaging";
+import { Button, notification, Space } from "antd";
import axios from "axios";
-import React, {useEffect} from "react";
-import {useTranslation} from "react-i18next";
-import {messaging, requestForToken} from "../../firebase/firebase.utils";
+import React, { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { messaging, requestForToken } from "../../firebase/firebase.utils";
import FcmHandler from "../../utils/fcm-handler";
import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss";
-export function ChatAffixContainer({bodyshop, chatVisible}) {
- const {t} = useTranslation();
- const client = useApolloClient();
-
- useEffect(() => {
- if (!bodyshop || !bodyshop.messagingservicesid) return;
+export function ChatAffixContainer({ bodyshop, chatVisible }) {
+ const { t } = useTranslation();
+ const client = useApolloClient();
- async function SubscribeToTopic() {
- try {
- const r = await axios.post("/notifications/subscribe", {
- fcm_tokens: await getToken(messaging, {
- vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY,
- }),
- type: "messaging",
- imexshopid: bodyshop.imexshopid,
- });
- console.log("FCM Topic Subscription", r.data);
- } catch (error) {
- console.log(
- "Error attempting to subscribe to messaging topic: ",
- error
- );
- notification.open({
- key: 'fcm',
- type: "warning",
- message: t("general.errors.fcm"),
- btn: (
-
- {
- await requestForToken();
- SubscribeToTopic();
- }}
- >
- {t("general.actions.tryagain")}
-
- {
- const win = window.open(
- "https://help.imex.online/en/article/enabling-notifications-o978xi/",
- "_blank"
- );
- win.focus();
- }}
- >
- {t("general.labels.help")}
-
-
- ),
- });
- }
- }
+ useEffect(() => {
+ if (!bodyshop || !bodyshop.messagingservicesid) return;
- SubscribeToTopic();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [bodyshop]);
+ async function SubscribeToTopic() {
+ try {
+ const r = await axios.post("/notifications/subscribe", {
+ fcm_tokens: await getToken(messaging, {
+ vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
+ }),
+ type: "messaging",
+ imexshopid: bodyshop.imexshopid
+ });
+ console.log("FCM Topic Subscription", r.data);
+ } catch (error) {
+ console.log("Error attempting to subscribe to messaging topic: ", error);
+ notification.open({
+ key: "fcm",
+ type: "warning",
+ message: t("general.errors.fcm"),
+ btn: (
+
+ {
+ await requestForToken();
+ SubscribeToTopic();
+ }}
+ >
+ {t("general.actions.tryagain")}
+
+ {
+ const win = window.open(
+ "https://help.imex.online/en/article/enabling-notifications-o978xi/",
+ "_blank"
+ );
+ win.focus();
+ }}
+ >
+ {t("general.labels.help")}
+
+
+ )
+ });
+ }
+ }
- useEffect(() => {
- function handleMessage(payload) {
- FcmHandler({
- client,
- payload: (payload && payload.data && payload.data.data) || payload.data,
- });
- }
+ SubscribeToTopic();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [bodyshop]);
- let stopMessageListener, channel;
- try {
- stopMessageListener = onMessage(messaging, handleMessage);
- channel = new BroadcastChannel("imex-sw-messages");
- channel.addEventListener("message", handleMessage);
- } catch (error) {
- console.log("Unable to set event listeners.");
- }
- return () => {
- stopMessageListener && stopMessageListener();
- channel && channel.removeEventListener("message", handleMessage);
- };
- }, [client]);
+ useEffect(() => {
+ function handleMessage(payload) {
+ FcmHandler({
+ client,
+ payload: (payload && payload.data && payload.data.data) || payload.data
+ });
+ }
- if (!bodyshop || !bodyshop.messagingservicesid) return <>>;
+ let stopMessageListener, channel;
+ try {
+ stopMessageListener = onMessage(messaging, handleMessage);
+ channel = new BroadcastChannel("imex-sw-messages");
+ channel.addEventListener("message", handleMessage);
+ } catch (error) {
+ console.log("Unable to set event listeners.");
+ }
+ return () => {
+ stopMessageListener && stopMessageListener();
+ channel && channel.removeEventListener("message", handleMessage);
+ };
+ }, [client]);
- return (
-
- {bodyshop && bodyshop.messagingservicesid ? : null}
-
- );
+ if (!bodyshop || !bodyshop.messagingservicesid) return <>>;
+
+ return (
+
+ {bodyshop && bodyshop.messagingservicesid ? : null}
+
+ );
}
export default ChatAffixContainer;
diff --git a/client/src/components/chat-archive-button/chat-archive-button.component.jsx b/client/src/components/chat-archive-button/chat-archive-button.component.jsx
index d7233d2cf..755e8f514 100644
--- a/client/src/components/chat-archive-button/chat-archive-button.component.jsx
+++ b/client/src/components/chat-archive-button/chat-archive-button.component.jsx
@@ -1,29 +1,27 @@
-import {useMutation} from "@apollo/client";
-import {Button} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {TOGGLE_CONVERSATION_ARCHIVE} from "../../graphql/conversations.queries";
+import { useMutation } from "@apollo/client";
+import { Button } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
-export default function ChatArchiveButton({conversation}) {
- const [loading, setLoading] = useState(false);
- const {t} = useTranslation();
- const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
- const handleToggleArchive = async () => {
- setLoading(true);
+export default function ChatArchiveButton({ conversation }) {
+ const [loading, setLoading] = useState(false);
+ const { t } = useTranslation();
+ const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
+ const handleToggleArchive = async () => {
+ setLoading(true);
- await updateConversation({
- variables: {id: conversation.id, archived: !conversation.archived},
- refetchQueries: ["CONVERSATION_LIST_QUERY"],
- });
+ await updateConversation({
+ variables: { id: conversation.id, archived: !conversation.archived },
+ refetchQueries: ["CONVERSATION_LIST_QUERY"]
+ });
- setLoading(false);
- };
+ setLoading(false);
+ };
- return (
-
- {conversation.archived
- ? t("messaging.labels.unarchive")
- : t("messaging.labels.archive")}
-
- );
+ return (
+
+ {conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
+
+ );
}
diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx
index d498949cf..a49c3b4b4 100644
--- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx
+++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx
@@ -1,122 +1,101 @@
-import {Badge, Card, List, Space, Tag} from "antd";
+import { Badge, Card, List, Space, Tag } from "antd";
import React from "react";
-import {connect} from "react-redux";
-import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized";
-import {createStructuredSelector} from "reselect";
-import {setSelectedConversation} from "../../redux/messaging/messaging.actions";
-import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
-import {TimeAgoFormatter} from "../../utils/DateFormatter";
+import { connect } from "react-redux";
+import { AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList } from "react-virtualized";
+import { createStructuredSelector } from "reselect";
+import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
+import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
+import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
-import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
+import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import _ from "lodash";
import "./chat-conversation-list.styles.scss";
const mapStateToProps = createStructuredSelector({
- selectedConversation: selectSelectedConversation,
+ selectedConversation: selectSelectedConversation
});
const mapDispatchToProps = (dispatch) => ({
- setSelectedConversation: (conversationId) =>
- dispatch(setSelectedConversation(conversationId)),
+ setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
});
function ChatConversationListComponent({
- conversationList,
- selectedConversation,
- setSelectedConversation,
- loadMoreConversations,
- }) {
- const cache = new CellMeasurerCache({
- fixedWidth: true,
- defaultHeight: 60,
- });
+ conversationList,
+ selectedConversation,
+ setSelectedConversation,
+ loadMoreConversations
+}) {
+ const cache = new CellMeasurerCache({
+ fixedWidth: true,
+ defaultHeight: 60
+ });
- const rowRenderer = ({index, key, style, parent}) => {
- const item = conversationList[index];
- const cardContentRight =
- {item.updated_at} ;
- const cardContentLeft = item.job_conversations.length > 0
- ? item.job_conversations.map((j, idx) => (
- {j.job.ro_number}
- ))
- : null;
+ const rowRenderer = ({ index, key, style, parent }) => {
+ const item = conversationList[index];
+ const cardContentRight = {item.updated_at} ;
+ const cardContentLeft =
+ item.job_conversations.length > 0
+ ? item.job_conversations.map((j, idx) => {j.job.ro_number} )
+ : null;
- const names = <>{_.uniq(item.job_conversations.map((j, idx) =>
- OwnerNameDisplayFunction(j.job)
- ))}>
+ const names = <>{_.uniq(item.job_conversations.map((j, idx) => OwnerNameDisplayFunction(j.job)))}>;
- const cardTitle = <>
- {item.label && {item.label} }
- {item.job_conversations.length > 0 ? (
-
- {names}
-
- ) : (
-
- {item.phone_num}
-
- )}
- >
- const cardExtra =
+ const cardTitle = (
+ <>
+ {item.label && {item.label} }
+ {item.job_conversations.length > 0 ? (
+ {names}
+ ) : (
+
+ {item.phone_num}
+
+ )}
+ >
+ );
+ const cardExtra = ;
- const getCardStyle = () =>
- item.id === selectedConversation
- ? {backgroundColor: 'rgba(128, 128, 128, 0.2)'}
- : {backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff'};
-
- return (
-
- setSelectedConversation(item.id)}
- style={style}
- className={`chat-list-item
- ${
- item.id === selectedConversation
- ? "chat-list-selected-conversation"
- : null
- }`}
- >
-
-
- {cardContentLeft}
-
- {cardContentRight}
-
-
-
- );
- };
+ const getCardStyle = () =>
+ item.id === selectedConversation
+ ? { backgroundColor: "rgba(128, 128, 128, 0.2)" }
+ : { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
return (
-
-
- {({height, width}) => (
- {
- if (scrollTop + clientHeight === scrollHeight) {
- loadMoreConversations();
- }
- }}
- />
- )}
-
-
+
+ setSelectedConversation(item.id)}
+ style={style}
+ className={`chat-list-item
+ ${item.id === selectedConversation ? "chat-list-selected-conversation" : null}`}
+ >
+
+ {cardContentLeft}
+ {cardContentRight}
+
+
+
);
+ };
+
+ return (
+
+
+ {({ height, width }) => (
+ {
+ if (scrollTop + clientHeight === scrollHeight) {
+ loadMoreConversations();
+ }
+ }}
+ />
+ )}
+
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ChatConversationListComponent);
+export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationListComponent);
diff --git a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx
index bdeb99ae2..d851e6c92 100644
--- a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx
+++ b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx
@@ -1,56 +1,56 @@
-import {useMutation} from "@apollo/client";
-import {Tag} from "antd";
+import { useMutation } from "@apollo/client";
+import { Tag } from "antd";
import React from "react";
-import {Link} from "react-router-dom";
-import {logImEXEvent} from "../../firebase/firebase.utils";
-import {REMOVE_CONVERSATION_TAG} from "../../graphql/job-conversations.queries";
+import { Link } from "react-router-dom";
+import { logImEXEvent } from "../../firebase/firebase.utils";
+import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
-export default function ChatConversationTitleTags({jobConversations}) {
- const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
+export default function ChatConversationTitleTags({ jobConversations }) {
+ const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
- const handleRemoveTag = (jobId) => {
- const convId = jobConversations[0].conversationid;
- if (!!convId) {
- removeJobConversation({
- variables: {
- conversationId: convId,
- jobId: jobId,
- },
- update(cache) {
- cache.modify({
- id: cache.identify({id: convId, __typename: "conversations"}),
- fields: {
- job_conversations(ex) {
- return ex.filter((e) => e.jobid !== jobId);
- },
- },
- });
- },
- });
- logImEXEvent("messaging_remove_job_tag", {
- conversationId: convId,
- jobId: jobId,
- });
+ const handleRemoveTag = (jobId) => {
+ const convId = jobConversations[0].conversationid;
+ if (!!convId) {
+ removeJobConversation({
+ variables: {
+ conversationId: convId,
+ jobId: jobId
+ },
+ update(cache) {
+ cache.modify({
+ id: cache.identify({ id: convId, __typename: "conversations" }),
+ fields: {
+ job_conversations(ex) {
+ return ex.filter((e) => e.jobid !== jobId);
+ }
+ }
+ });
}
- };
+ });
+ logImEXEvent("messaging_remove_job_tag", {
+ conversationId: convId,
+ jobId: jobId
+ });
+ }
+ };
- return (
-
- {jobConversations.map((item) => (
- handleRemoveTag(item.job.id)}
- >
-
- {`${item.job.ro_number || "?"} | `}
-
-
-
- ))}
-
- );
+ return (
+
+ {jobConversations.map((item) => (
+ handleRemoveTag(item.job.id)}
+ >
+
+ {`${item.job.ro_number || "?"} | `}
+
+
+
+ ))}
+
+ );
}
diff --git a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx
index bdc1a35a3..41cf0b441 100644
--- a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx
+++ b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx
@@ -1,4 +1,4 @@
-import {Space} from "antd";
+import { Space } from "antd";
import React from "react";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
@@ -7,21 +7,15 @@ import ChatLabelComponent from "../chat-label/chat-label.component";
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
-export default function ChatConversationTitle({conversation}) {
- return (
-
-
- {conversation && conversation.phone_num}
-
-
-
-
-
-
-
- );
+export default function ChatConversationTitle({ conversation }) {
+ return (
+
+ {conversation && conversation.phone_num}
+
+
+
+
+
+
+ );
}
diff --git a/client/src/components/chat-conversation/chat-conversation.component.jsx b/client/src/components/chat-conversation/chat-conversation.component.jsx
index 7e11bef10..d4a9695c0 100644
--- a/client/src/components/chat-conversation/chat-conversation.component.jsx
+++ b/client/src/components/chat-conversation/chat-conversation.component.jsx
@@ -6,26 +6,21 @@ import ChatSendMessage from "../chat-send-message/chat-send-message.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
import "./chat-conversation.styles.scss";
-export default function ChatConversationComponent({
- subState,
- conversation,
- messages,
- handleMarkConversationAsRead,
- }) {
- const [loading, error] = subState;
+export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) {
+ const [loading, error] = subState;
- if (loading) return ;
- if (error) return ;
+ if (loading) return ;
+ if (error) return ;
- return (
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+ );
}
diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx
index d989fe514..2b91d2320 100644
--- a/client/src/components/chat-conversation/chat-conversation.container.jsx
+++ b/client/src/components/chat-conversation/chat-conversation.container.jsx
@@ -1,87 +1,81 @@
-import {useMutation, useQuery, useSubscription} from "@apollo/client";
-import React, {useState} from "react";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS,} from "../../graphql/conversations.queries";
-import {MARK_MESSAGES_AS_READ_BY_CONVERSATION} from "../../graphql/messages.queries";
-import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
+import { useMutation, useQuery, useSubscription } from "@apollo/client";
+import React, { useState } from "react";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
+import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
+import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import ChatConversationComponent from "./chat-conversation.component";
import axios from "axios";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
- selectedConversation: selectSelectedConversation,
- bodyshop: selectBodyshop,
+ selectedConversation: selectSelectedConversation,
+ bodyshop: selectBodyshop
});
export default connect(mapStateToProps, null)(ChatConversationContainer);
-export function ChatConversationContainer({bodyshop, selectedConversation}) {
- const {
- loading: convoLoading,
- error: convoError,
- data: convoData,
- } = useQuery(GET_CONVERSATION_DETAILS, {
- variables: {conversationId: selectedConversation},
- fetchPolicy: "network-only",
- nextFetchPolicy: "network-only",
- });
+export function ChatConversationContainer({ bodyshop, selectedConversation }) {
+ const {
+ loading: convoLoading,
+ error: convoError,
+ data: convoData
+ } = useQuery(GET_CONVERSATION_DETAILS, {
+ variables: { conversationId: selectedConversation },
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
- const {loading, error, data} = useSubscription(
- CONVERSATION_SUBSCRIPTION_BY_PK,
- {
- variables: {conversationId: selectedConversation},
+ const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
+ variables: { conversationId: selectedConversation }
+ });
+
+ const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
+
+ const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, {
+ variables: { conversationId: selectedConversation },
+ refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
+ update(cache) {
+ cache.modify({
+ id: cache.identify({
+ __typename: "conversations",
+ id: selectedConversation
+ }),
+ fields: {
+ messages_aggregate(cached) {
+ return { aggregate: { count: 0 } };
+ }
}
- );
+ });
+ }
+ });
- const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
+ const unreadCount =
+ data &&
+ data.messages &&
+ data.messages.reduce((acc, val) => {
+ return !val.read && !val.isoutbound ? acc + 1 : acc;
+ }, 0);
- const [markConversationRead] = useMutation(
- MARK_MESSAGES_AS_READ_BY_CONVERSATION,
- {
- variables: {conversationId: selectedConversation},
- refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
- update(cache) {
- cache.modify({
- id: cache.identify({
- __typename: "conversations",
- id: selectedConversation,
- }),
- fields: {
- messages_aggregate(cached) {
- return {aggregate: {count: 0}};
- },
- },
- });
- },
- }
- );
+ const handleMarkConversationAsRead = async () => {
+ if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
+ setMarkingAsReadInProgress(true);
+ await markConversationRead({});
+ await axios.post("/sms/markConversationRead", {
+ conversationid: selectedConversation,
+ imexshopid: bodyshop.imexshopid
+ });
+ setMarkingAsReadInProgress(false);
+ }
+ };
- const unreadCount =
- data &&
- data.messages &&
- data.messages.reduce((acc, val) => {
- return !val.read && !val.isoutbound ? acc + 1 : acc;
- }, 0);
-
- const handleMarkConversationAsRead = async () => {
- if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
- setMarkingAsReadInProgress(true);
- await markConversationRead({});
- await axios.post("/sms/markConversationRead", {
- conversationid: selectedConversation,
- imexshopid: bodyshop.imexshopid,
- });
- setMarkingAsReadInProgress(false);
- }
- };
-
- return (
-
- );
+ return (
+
+ );
}
diff --git a/client/src/components/chat-label/chat-label.component.jsx b/client/src/components/chat-label/chat-label.component.jsx
index 95cb1af4f..7157d02c8 100644
--- a/client/src/components/chat-label/chat-label.component.jsx
+++ b/client/src/components/chat-label/chat-label.component.jsx
@@ -1,68 +1,59 @@
-import {PlusOutlined} from "@ant-design/icons";
-import {useMutation} from "@apollo/client";
-import {Input, notification, Spin, Tag, Tooltip} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {UPDATE_CONVERSATION_LABEL} from "../../graphql/conversations.queries";
+import { PlusOutlined } from "@ant-design/icons";
+import { useMutation } from "@apollo/client";
+import { Input, notification, Spin, Tag, Tooltip } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
-export default function ChatLabel({conversation}) {
- const [loading, setLoading] = useState(false);
- const [editing, setEditing] = useState(false);
- const [value, setValue] = useState(conversation.label);
+export default function ChatLabel({ conversation }) {
+ const [loading, setLoading] = useState(false);
+ const [editing, setEditing] = useState(false);
+ const [value, setValue] = useState(conversation.label);
- const {t} = useTranslation();
- const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
+ const { t } = useTranslation();
+ const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
- const handleSave = async () => {
- setLoading(true);
- try {
- const response = await updateLabel({
- variables: {id: conversation.id, label: value},
- });
- if (response.errors) {
- notification["error"]({
- message: t("messages.errors.updatinglabel", {
- error: JSON.stringify(response.errors),
- }),
- });
- } else {
- setEditing(false);
- }
- } catch (error) {
- notification["error"]({
- message: t("messages.errors.updatinglabel", {
- error: JSON.stringify(error),
- }),
- });
- } finally {
- setLoading(false);
- }
- };
- if (editing) {
- return (
-
- setValue(e.target.value)}
- onBlur={handleSave}
- allowClear
- />
- {loading && }
-
- );
- } else {
- return conversation.label && conversation.label.trim() !== "" ? (
- setEditing(true)}>
- {conversation.label}
-
- ) : (
-
- setEditing(true)}
- />
-
- );
+ const handleSave = async () => {
+ setLoading(true);
+ try {
+ const response = await updateLabel({
+ variables: { id: conversation.id, label: value }
+ });
+ if (response.errors) {
+ notification["error"]({
+ message: t("messages.errors.updatinglabel", {
+ error: JSON.stringify(response.errors)
+ })
+ });
+ } else {
+ setEditing(false);
+ }
+ } catch (error) {
+ notification["error"]({
+ message: t("messages.errors.updatinglabel", {
+ error: JSON.stringify(error)
+ })
+ });
+ } finally {
+ setLoading(false);
}
+ };
+ if (editing) {
+ return (
+
+ setValue(e.target.value)} onBlur={handleSave} allowClear />
+ {loading && }
+
+ );
+ } else {
+ return conversation.label && conversation.label.trim() !== "" ? (
+ setEditing(true)}>
+ {conversation.label}
+
+ ) : (
+
+ setEditing(true)} />
+
+ );
+ }
}
diff --git a/client/src/components/chat-media-selector/chat-media-selector.component.jsx b/client/src/components/chat-media-selector/chat-media-selector.component.jsx
index c2f32883f..0526fde43 100644
--- a/client/src/components/chat-media-selector/chat-media-selector.component.jsx
+++ b/client/src/components/chat-media-selector/chat-media-selector.component.jsx
@@ -1,100 +1,82 @@
-import {PictureFilled} from "@ant-design/icons";
-import {useQuery} from "@apollo/client";
-import {Badge, Popover} from "antd";
-import React, {useEffect, useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {GET_DOCUMENTS_BY_JOB} from "../../graphql/documents.queries";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { PictureFilled } from "@ant-design/icons";
+import { useQuery } from "@apollo/client";
+import { Badge, Popover } from "antd";
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-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";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
-export function ChatMediaSelector({
- bodyshop,
- selectedMedia,
- setSelectedMedia,
- conversation,
- }) {
- const {t} = useTranslation();
- const [open, setOpen] = useState(false);
+export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
+ const { t } = useTranslation();
+ const [open, setOpen] = useState(false);
- const {loading, error, data} = useQuery(GET_DOCUMENTS_BY_JOB, {
- fetchPolicy: "network-only",
- nextFetchPolicy: "network-only",
- variables: {
- jobId:
- conversation.job_conversations[0] &&
- conversation.job_conversations[0].jobid,
- },
+ const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only",
+ variables: {
+ jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
+ },
- skip:
- !open ||
- !conversation.job_conversations ||
- conversation.job_conversations.length === 0,
- });
+ skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
+ });
- const handleVisibleChange = (change) => {
- setOpen(change);
- };
+ const handleVisibleChange = (change) => {
+ setOpen(change);
+ };
- useEffect(() => {
- setSelectedMedia([]);
- }, [setSelectedMedia, conversation]);
+ useEffect(() => {
+ setSelectedMedia([]);
+ }, [setSelectedMedia, conversation]);
- const content = (
-
- {loading &&
}
- {error &&
}
- {selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
-
{t("messaging.labels.maxtenimages")}
- ) : null}
- {!bodyshop.uselocalmediaserver && data && (
-
- )}
- {bodyshop.uselocalmediaserver && open && (
-
- )}
-
- );
+ const content = (
+
+ {loading &&
}
+ {error &&
}
+ {selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
+
{t("messaging.labels.maxtenimages")}
+ ) : null}
+ {!bodyshop.uselocalmediaserver && data && (
+
+ )}
+ {bodyshop.uselocalmediaserver && open && (
+
+ )}
+
+ );
- return (
- {t("messaging.errors.noattachedjobs")}
- ) : (
- content
- )
- }
- title={t("messaging.labels.selectmedia")}
- trigger="click"
- open={open}
- onOpenChange={handleVisibleChange}
- >
- s.isSelected).length}>
-
-
-
- );
+ return (
+ {t("messaging.errors.noattachedjobs")} : content
+ }
+ title={t("messaging.labels.selectmedia")}
+ trigger="click"
+ open={open}
+ onOpenChange={handleVisibleChange}
+ >
+ s.isSelected).length}>
+
+
+
+ );
}
diff --git a/client/src/components/chat-messages-list/chat-message-list.component.jsx b/client/src/components/chat-messages-list/chat-message-list.component.jsx
index e32580900..55aa81dc0 100644
--- a/client/src/components/chat-messages-list/chat-message-list.component.jsx
+++ b/client/src/components/chat-messages-list/chat-message-list.component.jsx
@@ -1,113 +1,106 @@
import Icon from "@ant-design/icons";
-import {Tooltip} from "antd";
+import { Tooltip } from "antd";
import i18n from "i18next";
import dayjs from "../../utils/day";
-import React, {useEffect, useRef} from "react";
-import {MdDone, MdDoneAll} from "react-icons/md";
-import {AutoSizer, CellMeasurer, CellMeasurerCache, List,} from "react-virtualized";
-import {DateTimeFormatter} from "../../utils/DateFormatter";
+import React, { useEffect, useRef } from "react";
+import { MdDone, MdDoneAll } from "react-icons/md";
+import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
+import { DateTimeFormatter } from "../../utils/DateFormatter";
import "./chat-message-list.styles.scss";
-export default function ChatMessageListComponent({messages}) {
- const virtualizedListRef = useRef(null);
+export default function ChatMessageListComponent({ messages }) {
+ const virtualizedListRef = useRef(null);
- const _cache = new CellMeasurerCache({
- fixedWidth: true,
- // minHeight: 50,
- defaultHeight: 100,
- });
+ const _cache = new CellMeasurerCache({
+ fixedWidth: true,
+ // minHeight: 50,
+ defaultHeight: 100
+ });
- const scrollToBottom = (renderedrows) => {
- //console.log("Scrolling to", messages.length);
- // !!virtualizedListRef.current &&
- // virtualizedListRef.current.scrollToRow(messages.length);
- // Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
- //Scrolling does not work on this version of React.
- };
+ const scrollToBottom = (renderedrows) => {
+ //console.log("Scrolling to", messages.length);
+ // !!virtualizedListRef.current &&
+ // virtualizedListRef.current.scrollToRow(messages.length);
+ // Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
+ //Scrolling does not work on this version of React.
+ };
- useEffect(scrollToBottom, [messages]);
-
- const _rowRenderer = ({index, key, parent, style}) => {
- return (
-
- {({measure, registerChild}) => (
-
-
- {MessageRender(messages[index])}
- {StatusRender(messages[index].status)}
-
- {messages[index].isoutbound && (
-
- {i18n.t("messaging.labels.sentby", {
- by: messages[index].userid,
- time: dayjs(messages[index].created_at).format(
- "MM/DD/YYYY @ hh:mm a"
- ),
- })}
-
- )}
-
- )}
-
- );
- };
+ useEffect(scrollToBottom, [messages]);
+ const _rowRenderer = ({ index, key, parent, style }) => {
return (
-
-
- {({height, width}) => (
-
- )}
-
-
+
+ {({ measure, registerChild }) => (
+
+
+ {MessageRender(messages[index])}
+ {StatusRender(messages[index].status)}
+
+ {messages[index].isoutbound && (
+
+ {i18n.t("messaging.labels.sentby", {
+ by: messages[index].userid,
+ time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a")
+ })}
+
+ )}
+
+ )}
+
);
+ };
+
+ return (
+
+
+ {({ height, width }) => (
+
+ )}
+
+
+ );
}
const MessageRender = (message) => {
- return (
-
-
- {message.image_path &&
- message.image_path.map((i, idx) => (
-
- ))}
-
{message.text}
+ return (
+
+
+ {message.image_path &&
+ message.image_path.map((i, idx) => (
+
-
- );
+ ))}
+
{message.text}
+
+
+ );
};
const StatusRender = (status) => {
- switch (status) {
- case "sent":
- return
;
- case "delivered":
- return
;
- default:
- return null;
- }
+ switch (status) {
+ case "sent":
+ return
;
+ case "delivered":
+ return
;
+ default:
+ return null;
+ }
};
diff --git a/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx b/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx
index 61480c310..b0b6054e4 100644
--- a/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx
+++ b/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx
@@ -1,55 +1,49 @@
-import {PlusCircleFilled} from "@ant-design/icons";
-import {Button, Form, Popover} from "antd";
+import { PlusCircleFilled } from "@ant-design/icons";
+import { Button, Form, Popover } from "antd";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {openChatByPhone} from "../../redux/messaging/messaging.actions";
-import PhoneFormItem, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { openChatByPhone } from "../../redux/messaging/messaging.actions";
+import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
const mapStateToProps = createStructuredSelector({
- //currentUser: selectCurrentUser
+ //currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
- openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
+ openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
});
-export function ChatNewConversation({openChatByPhone}) {
- const {t} = useTranslation();
- const [form] = Form.useForm();
- const handleFinish = (values) => {
- openChatByPhone({phone_num: values.phoneNumber});
- form.resetFields();
- };
+export function ChatNewConversation({ openChatByPhone }) {
+ const { t } = useTranslation();
+ const [form] = Form.useForm();
+ const handleFinish = (values) => {
+ openChatByPhone({ phone_num: values.phoneNumber });
+ form.resetFields();
+ };
- const popContent = (
-
-
- PhoneItemFormatterValidation(getFieldValue, "phoneNumber"),
- ]}
- >
-
-
-
- {t("messaging.actions.new")}
-
-
-
- );
+ const popContent = (
+
+
PhoneItemFormatterValidation(getFieldValue, "phoneNumber")]}
+ >
+
+
+
+ {t("messaging.actions.new")}
+
+
+
+ );
- return (
-
-
-
- );
+ return (
+
+
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ChatNewConversation);
+export default connect(mapStateToProps, mapDispatchToProps)(ChatNewConversation);
diff --git a/client/src/components/chat-open-button/chat-open-button.component.jsx b/client/src/components/chat-open-button/chat-open-button.component.jsx
index 539c5ec36..d2f0de528 100644
--- a/client/src/components/chat-open-button/chat-open-button.component.jsx
+++ b/client/src/components/chat-open-button/chat-open-button.component.jsx
@@ -1,54 +1,47 @@
-import {notification} from "antd";
+import { notification } from "antd";
import parsePhoneNumber from "libphonenumber-js";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {openChatByPhone} from "../../redux/messaging/messaging.actions";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { openChatByPhone } from "../../redux/messaging/messaging.actions";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
-import {createStructuredSelector} from "reselect";
-import {selectBodyshop} from "../../redux/user/user.selectors";
-import {searchingForConversation} from "../../redux/messaging/messaging.selectors";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
- searchingForConversation: searchingForConversation,
+ bodyshop: selectBodyshop,
+ searchingForConversation: searchingForConversation
});
const mapDispatchToProps = (dispatch) => ({
- openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
+ openChatByPhone: (phone) => dispatch(openChatByPhone(phone))
});
-export function ChatOpenButton({
- bodyshop,
- searchingForConversation,
- phone,
- jobid,
- openChatByPhone,
- }) {
- const {t} = useTranslation();
- if (!phone) return <>>;
+export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
+ const { t } = useTranslation();
+ if (!phone) return <>>;
- if (!bodyshop.messagingservicesid)
- return
{phone} ;
+ if (!bodyshop.messagingservicesid) return
{phone} ;
- return (
-
{
- e.stopPropagation();
- const p = parsePhoneNumber(phone, "CA");
- if (searchingForConversation) return; //This is to prevent finding the same thing twice.
- if (p && p.isValid()) {
- openChatByPhone({phone_num: p.formatInternational(), jobid: jobid});
- } else {
- notification["error"]({message: t("messaging.error.invalidphone")});
- }
- }}
- >
- {phone}
-
- );
+ return (
+
{
+ e.stopPropagation();
+ const p = parsePhoneNumber(phone, "CA");
+ if (searchingForConversation) return; //This is to prevent finding the same thing twice.
+ if (p && p.isValid()) {
+ openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
+ } else {
+ notification["error"]({ message: t("messaging.error.invalidphone") });
+ }
+ }}
+ >
+ {phone}
+
+ );
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatOpenButton);
diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx
index 052193b1e..c608f8b11 100644
--- a/client/src/components/chat-popup/chat-popup.component.jsx
+++ b/client/src/components/chat-popup/chat-popup.component.jsx
@@ -1,13 +1,13 @@
-import {InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined,} from "@ant-design/icons";
-import {useLazyQuery, useQuery} from "@apollo/client";
-import {Badge, Card, Col, Row, Space, Tag, Tooltip, Typography} from "antd";
-import React, {useCallback, useEffect, useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT,} from "../../graphql/conversations.queries";
-import {toggleChatVisible} from "../../redux/messaging/messaging.actions";
-import {selectChatVisible, selectSelectedConversation,} from "../../redux/messaging/messaging.selectors";
+import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
+import { useLazyQuery, useQuery } from "@apollo/client";
+import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
+import React, { useCallback, useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT } from "../../graphql/conversations.queries";
+import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
+import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component";
import ChatConversationContainer from "../chat-conversation/chat-conversation.container";
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
@@ -15,119 +15,102 @@ import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import "./chat-popup.styles.scss";
const mapStateToProps = createStructuredSelector({
- selectedConversation: selectSelectedConversation,
- chatVisible: selectChatVisible,
+ selectedConversation: selectSelectedConversation,
+ chatVisible: selectChatVisible
});
const mapDispatchToProps = (dispatch) => ({
- toggleChatVisible: () => dispatch(toggleChatVisible()),
+ toggleChatVisible: () => dispatch(toggleChatVisible())
});
-export function ChatPopupComponent({
- chatVisible,
- selectedConversation,
- toggleChatVisible,
- }) {
- const {t} = useTranslation();
- const [pollInterval, setpollInterval] = useState(0);
+export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
+ const { t } = useTranslation();
+ const [pollInterval, setpollInterval] = useState(0);
- const {data: unreadData} = useQuery(UNREAD_CONVERSATION_COUNT, {
- fetchPolicy: "network-only",
- nextFetchPolicy: "network-only",
- ...(pollInterval > 0 ? {pollInterval} : {}),
- });
+ const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only",
+ ...(pollInterval > 0 ? { pollInterval } : {})
+ });
- const [getConversations, {loading, data, refetch, fetchMore}] =
- useLazyQuery(CONVERSATION_LIST_QUERY, {
- fetchPolicy: "network-only",
- nextFetchPolicy: "network-only",
- skip: !chatVisible,
- ...(pollInterval > 0 ? {pollInterval} : {}),
- });
+ const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only",
+ skip: !chatVisible,
+ ...(pollInterval > 0 ? { pollInterval } : {})
+ });
- const fcmToken = sessionStorage.getItem("fcmtoken");
+ const fcmToken = sessionStorage.getItem("fcmtoken");
- useEffect(() => {
- if (fcmToken) {
- setpollInterval(0);
- } else {
- setpollInterval(60000);
+ useEffect(() => {
+ if (fcmToken) {
+ setpollInterval(0);
+ } else {
+ setpollInterval(60000);
+ }
+ }, [fcmToken]);
+
+ useEffect(() => {
+ if (chatVisible)
+ getConversations({
+ variables: {
+ offset: 0
}
- }, [fcmToken]);
+ });
+ }, [chatVisible, getConversations]);
- useEffect(() => {
- if (chatVisible)
- getConversations({
- variables: {
- offset: 0,
- },
- });
- }, [chatVisible, getConversations]);
+ const loadMoreConversations = useCallback(() => {
+ if (data)
+ fetchMore({
+ variables: {
+ offset: data.conversations.length
+ }
+ });
+ }, [data, fetchMore]);
- const loadMoreConversations = useCallback(() => {
- if (data)
- fetchMore({
- variables: {
- offset: data.conversations.length,
- },
- });
- }, [data, fetchMore]);
+ const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
- const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
+ return (
+
+
+ {chatVisible ? (
+
+
+ {t("messaging.labels.messaging")}
+
+
+
+
+ refetch()} />
+ {pollInterval > 0 && {t("messaging.labels.nopush")} }
+
+
toggleChatVisible()}
+ style={{ position: "absolute", right: ".5rem", top: ".5rem" }}
+ />
- return (
-
-
- {chatVisible ? (
-
-
-
- {t("messaging.labels.messaging")}
-
-
-
-
-
- refetch()}
- />
- {pollInterval > 0 && (
- {t("messaging.labels.nopush")}
- )}
-
- toggleChatVisible()}
- style={{position: "absolute", right: ".5rem", top: ".5rem"}}
- />
-
-
-
- {loading ? (
-
- ) : (
-
- )}
-
-
- {selectedConversation ? : null}
-
-
-
+
+
+ {loading ? (
+
) : (
- toggleChatVisible()}
- style={{cursor: "pointer"}}
- >
-
- {t("messaging.labels.messaging")}
-
+
)}
-
-
- );
+
+ {selectedConversation ? : null}
+
+
+ ) : (
+ toggleChatVisible()} style={{ cursor: "pointer" }}>
+
+ {t("messaging.labels.messaging")}
+
+ )}
+
+
+ );
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent);
diff --git a/client/src/components/chat-presets/chat-presets.component.jsx b/client/src/components/chat-presets/chat-presets.component.jsx
index 5fee9c0fe..63b79c373 100644
--- a/client/src/components/chat-presets/chat-presets.component.jsx
+++ b/client/src/components/chat-presets/chat-presets.component.jsx
@@ -1,38 +1,34 @@
-import {PlusCircleOutlined} from "@ant-design/icons";
-import {Dropdown} from "antd";
+import { PlusCircleOutlined } from "@ant-design/icons";
+import { Dropdown } from "antd";
import React from "react";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {setMessage} from "../../redux/messaging/messaging.actions";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { setMessage } from "../../redux/messaging/messaging.actions";
+import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
- //currentUser: selectCurrentUser
- bodyshop: selectBodyshop,
+ //currentUser: selectCurrentUser
+ bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
- setMessage: (message) => dispatch(setMessage(message)),
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
+ setMessage: (message) => dispatch(setMessage(message))
});
-export function ChatPresetsComponent({bodyshop, setMessage, className}) {
+export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
+ const items = bodyshop.md_messaging_presets.map((i, idx) => ({
+ key: idx,
+ label: i.label,
+ onClick: () => setMessage(i.text)
+ }));
- const items = bodyshop.md_messaging_presets.map((i, idx) => ({
- key: idx,
- label: (i.label),
- onClick: () => setMessage(i.text),
- }));
-
- return (
-
- );
+ return (
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ChatPresetsComponent);
+export default connect(mapStateToProps, mapDispatchToProps)(ChatPresetsComponent);
diff --git a/client/src/components/chat-print-button/chat-print-button.component.jsx b/client/src/components/chat-print-button/chat-print-button.component.jsx
index 2e61c0002..0b175b769 100644
--- a/client/src/components/chat-print-button/chat-print-button.component.jsx
+++ b/client/src/components/chat-print-button/chat-print-button.component.jsx
@@ -1,46 +1,46 @@
-import {MailOutlined, PrinterOutlined} from "@ant-design/icons";
-import {Space, Spin} from "antd";
-import React, {useState} from "react";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {setEmailOptions} from "../../redux/email/email.actions";
-import {GenerateDocument} from "../../utils/RenderTemplate";
-import {TemplateList} from "../../utils/TemplateConstants";
+import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
+import { Space, Spin } from "antd";
+import React, { useState } from "react";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { setEmailOptions } from "../../redux/email/email.actions";
+import { GenerateDocument } from "../../utils/RenderTemplate";
+import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
- setEmailOptions: (e) => dispatch(setEmailOptions(e)),
+ setEmailOptions: (e) => dispatch(setEmailOptions(e))
});
-export function ChatPrintButton({conversation}) {
- const [loading, setLoading] = useState(false);
+export function ChatPrintButton({ conversation }) {
+ const [loading, setLoading] = useState(false);
- const generateDocument = (type) => {
- setLoading(true);
- GenerateDocument(
- {
- name: TemplateList("messaging").conversation_list.key,
- variables: {id: conversation.id},
- },
- {
- subject: TemplateList("messaging").conversation_list.subject,
- },
- type,
- conversation.id
- ).catch(e => {
- console.warn('Something went wrong generating a document.');
- });
- setLoading(false);
- }
+ const generateDocument = (type) => {
+ setLoading(true);
+ GenerateDocument(
+ {
+ name: TemplateList("messaging").conversation_list.key,
+ variables: { id: conversation.id }
+ },
+ {
+ subject: TemplateList("messaging").conversation_list.subject
+ },
+ type,
+ conversation.id
+ ).catch((e) => {
+ console.warn("Something went wrong generating a document.");
+ });
+ setLoading(false);
+ };
- return (
-
- generateDocument('p')}/>
- generateDocument('e')}/>
- {loading && }
-
- );
+ return (
+
+ generateDocument("p")} />
+ generateDocument("e")} />
+ {loading && }
+
+ );
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);
diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx
index cae683876..918be1b5f 100644
--- a/client/src/components/chat-send-message/chat-send-message.component.jsx
+++ b/client/src/components/chat-send-message/chat-send-message.component.jsx
@@ -1,111 +1,101 @@
-import {LoadingOutlined, SendOutlined} from "@ant-design/icons";
-import {Input, Spin} from "antd";
-import React, {useEffect, useRef, useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {logImEXEvent} from "../../firebase/firebase.utils";
-import {sendMessage, setMessage,} from "../../redux/messaging/messaging.actions";
-import {selectIsSending, selectMessage,} from "../../redux/messaging/messaging.selectors";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
+import { Input, Spin } from "antd";
+import React, { useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { logImEXEvent } from "../../firebase/firebase.utils";
+import { sendMessage, setMessage } from "../../redux/messaging/messaging.actions";
+import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.selectors";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
- isSending: selectIsSending,
- message: selectMessage,
+ bodyshop: selectBodyshop,
+ isSending: selectIsSending,
+ message: selectMessage
});
const mapDispatchToProps = (dispatch) => ({
- sendMessage: (message) => dispatch(sendMessage(message)),
- setMessage: (message) => dispatch(setMessage(message)),
+ sendMessage: (message) => dispatch(sendMessage(message)),
+ setMessage: (message) => dispatch(setMessage(message))
});
-function ChatSendMessageComponent({
- conversation,
- bodyshop,
- sendMessage,
- isSending,
- message,
- setMessage,
- }) {
- const inputArea = useRef(null);
- const [selectedMedia, setSelectedMedia] = useState([]);
- useEffect(() => {
- inputArea.current.focus();
- }, [isSending, setMessage]);
+function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
+ const inputArea = useRef(null);
+ const [selectedMedia, setSelectedMedia] = useState([]);
+ useEffect(() => {
+ inputArea.current.focus();
+ }, [isSending, setMessage]);
- const {t} = useTranslation();
+ const { t } = useTranslation();
- const handleEnter = () => {
- const selectedImages = selectedMedia.filter((i) => i.isSelected);
- if ((message === "" || !message) && selectedImages.length === 0) return;
- logImEXEvent("messaging_send_message");
+ const handleEnter = () => {
+ const selectedImages = selectedMedia.filter((i) => i.isSelected);
+ if ((message === "" || !message) && selectedImages.length === 0) return;
+ logImEXEvent("messaging_send_message");
- if (selectedImages.length < 11) {
- sendMessage({
- to: conversation.phone_num,
- body: message || "",
- messagingServiceSid: bodyshop.messagingservicesid,
- conversationid: conversation.id,
- selectedMedia: selectedImages,
- imexshopid: bodyshop.imexshopid,
- });
- setSelectedMedia(
- selectedMedia.map((i) => {
- return {...i, isSelected: false};
- })
- );
- }
- };
+ if (selectedImages.length < 11) {
+ sendMessage({
+ to: conversation.phone_num,
+ body: message || "",
+ messagingServiceSid: bodyshop.messagingservicesid,
+ conversationid: conversation.id,
+ selectedMedia: selectedImages,
+ imexshopid: bodyshop.imexshopid
+ });
+ setSelectedMedia(
+ selectedMedia.map((i) => {
+ return { ...i, isSelected: false };
+ })
+ );
+ }
+ };
- return (
-
-
-
-
+ return (
+
+
+
+
setMessage(e.target.value)}
- onPressEnter={(event) => {
- event.preventDefault();
- if (!!!event.shiftKey) handleEnter();
- }}
+ className="imex-flex-row__margin imex-flex-row__grow"
+ allowClear
+ autoFocus
+ ref={inputArea}
+ autoSize={{ minRows: 1, maxRows: 4 }}
+ value={message}
+ disabled={isSending}
+ placeholder={t("messaging.labels.typeamessage")}
+ onChange={(e) => setMessage(e.target.value)}
+ onPressEnter={(event) => {
+ event.preventDefault();
+ if (!!!event.shiftKey) handleEnter();
+ }}
/>
-
-
- }
- />
-
- );
+
+
+ }
+ />
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ChatSendMessageComponent);
+export default connect(mapStateToProps, mapDispatchToProps)(ChatSendMessageComponent);
diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx
index 004b28a3c..f40c5c85e 100644
--- a/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx
+++ b/client/src/components/chat-tag-ro/chat-tag-ro.component.jsx
@@ -1,45 +1,35 @@
-import {CloseCircleOutlined, LoadingOutlined} from "@ant-design/icons";
-import {Empty, Select, Space} from "antd";
+import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
+import { Empty, Select, Space } from "antd";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
+import { useTranslation } from "react-i18next";
+import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
-export default function ChatTagRoComponent({
- roOptions,
- loading,
- handleSearch,
- handleInsertTag,
- setOpen,
- }) {
- const {t} = useTranslation();
+export default function ChatTagRoComponent({ roOptions, loading, handleSearch, handleInsertTag, setOpen }) {
+ const { t } = useTranslation();
- return (
-
-
- : }
- >
- {roOptions.map((item, idx) => (
-
- {` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`}
-
- ))}
-
-
- {loading ? : null}
+ return (
+
+
+ : }
+ >
+ {roOptions.map((item, idx) => (
+
+ {` ${item.ro_number || ""} | ${OwnerNameDisplayFunction(item)}`}
+
+ ))}
+
+
+ {loading ? : null}
- {loading ? (
-
- ) : (
- setOpen(false)}/>
- )}
-
- );
+ {loading ? : setOpen(false)} />}
+
+ );
}
diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx
index f2ea134f0..f9b6fa5ad 100644
--- a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx
+++ b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx
@@ -1,66 +1,62 @@
-import {PlusOutlined} from "@ant-design/icons";
-import {useLazyQuery, useMutation} from "@apollo/client";
-import {Tag} from "antd";
+import { PlusOutlined } from "@ant-design/icons";
+import { useLazyQuery, useMutation } from "@apollo/client";
+import { Tag } from "antd";
import _ from "lodash";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {logImEXEvent} from "../../firebase/firebase.utils";
-import {INSERT_CONVERSATION_TAG} from "../../graphql/job-conversations.queries";
-import {SEARCH_FOR_JOBS} from "../../graphql/jobs.queries";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { logImEXEvent } from "../../firebase/firebase.utils";
+import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
+import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
import ChatTagRo from "./chat-tag-ro.component";
-export default function ChatTagRoContainer({conversation}) {
- const {t} = useTranslation();
- const [open, setOpen] = useState(false);
+export default function ChatTagRoContainer({ conversation }) {
+ const { t } = useTranslation();
+ const [open, setOpen] = useState(false);
- const [loadRo, {loading, data}] = useLazyQuery(SEARCH_FOR_JOBS);
+ const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
- const executeSearch = (v) => {
- logImEXEvent("messaging_search_job_tag", {searchTerm: v});
- loadRo(v);
- };
+ const executeSearch = (v) => {
+ logImEXEvent("messaging_search_job_tag", { searchTerm: v });
+ loadRo(v);
+ };
- const debouncedExecuteSearch = _.debounce(executeSearch, 500);
+ const debouncedExecuteSearch = _.debounce(executeSearch, 500);
- const handleSearch = (value) => {
- debouncedExecuteSearch({variables: {search: value}});
- };
+ const handleSearch = (value) => {
+ debouncedExecuteSearch({ variables: { search: value } });
+ };
- const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, {
- variables: {conversationId: conversation.id},
- });
+ const [insertTag] = useMutation(INSERT_CONVERSATION_TAG, {
+ variables: { conversationId: conversation.id }
+ });
- const handleInsertTag = (value, option) => {
- logImEXEvent("messaging_add_job_tag");
- insertTag({variables: {jobId: option.key}});
- setOpen(false);
- };
+ const handleInsertTag = (value, option) => {
+ logImEXEvent("messaging_add_job_tag");
+ insertTag({ variables: { jobId: option.key } });
+ setOpen(false);
+ };
- const existingJobTags =
- conversation &&
- conversation.job_conversations &&
- conversation.job_conversations.map((i) => i.jobid);
+ const existingJobTags =
+ conversation && conversation.job_conversations && conversation.job_conversations.map((i) => i.jobid);
- const roOptions = data
- ? data.search_jobs.filter((job) => !existingJobTags.includes(job.id))
- : [];
+ const roOptions = data ? data.search_jobs.filter((job) => !existingJobTags.includes(job.id)) : [];
- return (
-
- {open ? (
-
- ) : (
-
setOpen(true)}>
-
- {t("messaging.actions.link")}
-
- )}
-
- );
+ return (
+
+ {open ? (
+
+ ) : (
+
setOpen(true)}>
+
+ {t("messaging.actions.link")}
+
+ )}
+
+ );
}
diff --git a/client/src/components/config-form-components/checkbox/checkbox.component.jsx b/client/src/components/config-form-components/checkbox/checkbox.component.jsx
index f3945268d..7c5a6c4be 100644
--- a/client/src/components/config-form-components/checkbox/checkbox.component.jsx
+++ b/client/src/components/config-form-components/checkbox/checkbox.component.jsx
@@ -1,22 +1,22 @@
-import {Checkbox, Form} from "antd";
+import { Checkbox, Form } from "antd";
import React from "react";
-export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
- const {name, label, required} = formItem;
+export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
+ const { name, label, required } = formItem;
- return (
-
-
-
- );
+ return (
+
+
+
+ );
}
diff --git a/client/src/components/config-form-components/config-form-components.component.jsx b/client/src/components/config-form-components/config-form-components.component.jsx
index c2b513052..abcd6a6b5 100644
--- a/client/src/components/config-form-components/config-form-components.component.jsx
+++ b/client/src/components/config-form-components/config-form-components.component.jsx
@@ -1,18 +1,18 @@
import React from "react";
import FormTypes from "./config-form-types";
-export default function ConfirmFormComponents({componentList, readOnly}) {
- return (
-
- {componentList.map((f, idx) => {
- const Comp = FormTypes[f.type];
+export default function ConfirmFormComponents({ componentList, readOnly }) {
+ return (
+
+ {componentList.map((f, idx) => {
+ const Comp = FormTypes[f.type];
- if (!!Comp) {
- return
;
- } else {
- return Error
;
- }
- })}
-
- );
+ if (!!Comp) {
+ return
;
+ } else {
+ return
Error
;
+ }
+ })}
+
+ );
}
diff --git a/client/src/components/config-form-components/config-form-types.js b/client/src/components/config-form-components/config-form-types.js
index fa2121ef8..a59a66d91 100644
--- a/client/src/components/config-form-components/config-form-types.js
+++ b/client/src/components/config-form-components/config-form-types.js
@@ -5,11 +5,11 @@ import Text from "./text/text.component";
import Textarea from "./textarea/textarea.component";
const e = {
- checkbox: CheckboxFormItem,
- slider: Slider,
- text: Text,
- textarea: Textarea,
- rate: Rate,
+ checkbox: CheckboxFormItem,
+ slider: Slider,
+ text: Text,
+ textarea: Textarea,
+ rate: Rate
};
export default e;
diff --git a/client/src/components/config-form-components/rate/rate.component.jsx b/client/src/components/config-form-components/rate/rate.component.jsx
index 2e144b7be..9f7e0189f 100644
--- a/client/src/components/config-form-components/rate/rate.component.jsx
+++ b/client/src/components/config-form-components/rate/rate.component.jsx
@@ -1,21 +1,21 @@
-import {Form, Rate} from "antd";
+import { Form, Rate } from "antd";
import React from "react";
-export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
- const {name, label, required} = formItem;
+export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
+ const { name, label, required } = formItem;
- return (
-
-
-
- );
+ return (
+
+
+
+ );
}
diff --git a/client/src/components/config-form-components/slider/slider.component.jsx b/client/src/components/config-form-components/slider/slider.component.jsx
index 0d8da40f7..1fde2bfa4 100644
--- a/client/src/components/config-form-components/slider/slider.component.jsx
+++ b/client/src/components/config-form-components/slider/slider.component.jsx
@@ -1,21 +1,21 @@
-import {Form, Slider} from "antd";
+import { Form, Slider } from "antd";
import React from "react";
-export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
- const {name, label, required, min, max} = formItem;
+export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
+ const { name, label, required, min, max } = formItem;
- return (
-
-
-
- );
+ return (
+
+
+
+ );
}
diff --git a/client/src/components/config-form-components/text/text.component.jsx b/client/src/components/config-form-components/text/text.component.jsx
index 9364e1c5d..73394e0c0 100644
--- a/client/src/components/config-form-components/text/text.component.jsx
+++ b/client/src/components/config-form-components/text/text.component.jsx
@@ -1,20 +1,20 @@
-import {Form, Input} from "antd";
+import { Form, Input } from "antd";
import React from "react";
-export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
- const {name, label, required} = formItem;
- return (
-
-
-
- );
+export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
+ const { name, label, required } = formItem;
+ return (
+
+
+
+ );
}
diff --git a/client/src/components/config-form-components/textarea/textarea.component.jsx b/client/src/components/config-form-components/textarea/textarea.component.jsx
index f707e4298..c9bb19781 100644
--- a/client/src/components/config-form-components/textarea/textarea.component.jsx
+++ b/client/src/components/config-form-components/textarea/textarea.component.jsx
@@ -1,21 +1,21 @@
-import {Form, Input} from "antd";
+import { Form, Input } from "antd";
import React from "react";
-export default function JobIntakeFormCheckboxComponent({formItem, readOnly}) {
- const {name, label, required, rows} = formItem;
+export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
+ const { name, label, required, rows } = formItem;
- return (
-
-
-
- );
+ return (
+
+
+
+ );
}
diff --git a/client/src/components/conflict/conflict.component.jsx b/client/src/components/conflict/conflict.component.jsx
index f9087b1b7..659e64080 100644
--- a/client/src/components/conflict/conflict.component.jsx
+++ b/client/src/components/conflict/conflict.component.jsx
@@ -1,28 +1,36 @@
import React from "react";
-import {Button, Result} from "antd";
-import {useTranslation} from "react-i18next";
-import InstanceRenderManager from '../../utils/instanceRenderMgr';
+import { Button, Result } from "antd";
+import { useTranslation } from "react-i18next";
+import InstanceRenderManager from "../../utils/instanceRenderMgr";
export default function ConflictComponent() {
- const {t} = useTranslation();
- return (
-
-
- {t("general.labels.instanceconflictext",{app: InstanceRenderManager({imex:'$t(titles.imexonline)', rome: '$t(titles.romeonline)', promanager: '$t(titles.promanager)'})})}
- {
- window.location.reload();
- }}
- >
- {t("general.actions.refresh")}
-
-
- }
- />
-
- );
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t("general.labels.instanceconflictext", {
+ app: InstanceRenderManager({
+ imex: "$t(titles.imexonline)",
+ rome: "$t(titles.romeonline)",
+ promanager: "$t(titles.promanager)"
+ })
+ })}
+
+ {
+ window.location.reload();
+ }}
+ >
+ {t("general.actions.refresh")}
+
+
+ }
+ />
+
+ );
}
diff --git a/client/src/components/contract-cars/contract-cars.component.jsx b/client/src/components/contract-cars/contract-cars.component.jsx
index 9868b0ad9..830dd5531 100644
--- a/client/src/components/contract-cars/contract-cars.component.jsx
+++ b/client/src/components/contract-cars/contract-cars.component.jsx
@@ -1,152 +1,127 @@
-import {Card, Input, Table} from "antd";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {alphaSort} from "../../utils/sorters";
+import { Card, Input, Table } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { alphaSort } from "../../utils/sorters";
-export default function ContractsCarsComponent({
- loading,
- data,
- selectedCarId,
- handleSelect,
- }) {
- const [state, setState] = useState({
- sortedInfo: {},
- filteredInfo: {text: ""},
- search: "",
- });
+export default function ContractsCarsComponent({ loading, data, selectedCarId, handleSelect }) {
+ const [state, setState] = useState({
+ sortedInfo: {},
+ filteredInfo: { text: "" },
+ search: ""
+ });
- const {t} = useTranslation();
+ const { t } = useTranslation();
- const columns = [
- {
- title: t("courtesycars.fields.fleetnumber"),
- dataIndex: "fleetnumber",
- key: "fleetnumber",
- sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
- sortOrder:
- state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order,
- },
- {
- title: t("courtesycars.fields.status"),
- dataIndex: "status",
- key: "status",
- sorter: (a, b) => alphaSort(a.status, b.status),
- sortOrder:
- state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
- render: (text, record) => {t(record.status)}
,
- },
- {
- title: t("courtesycars.fields.readiness"),
- dataIndex: "readiness",
- key: "readiness",
- sorter: (a, b) => alphaSort(a.readiness, b.readiness),
- sortOrder:
- state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
- render: (text, record) => t(record.readiness),
- },
- {
- title: t("courtesycars.fields.year"),
- dataIndex: "year",
- key: "year",
- sorter: (a, b) => alphaSort(a.year, b.year),
- sortOrder:
- state.sortedInfo.columnKey === "year" && state.sortedInfo.order,
- },
- {
- title: t("courtesycars.fields.make"),
- dataIndex: "make",
- key: "make",
- sorter: (a, b) => alphaSort(a.make, b.make),
- sortOrder:
- state.sortedInfo.columnKey === "make" && state.sortedInfo.order,
- },
- {
- title: t("courtesycars.fields.model"),
- dataIndex: "model",
- key: "model",
- sorter: (a, b) => alphaSort(a.model, b.model),
- sortOrder:
- state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
- },
- {
- title: t("courtesycars.fields.color"),
- dataIndex: "color",
- key: "color",
- sorter: (a, b) => alphaSort(a.color, b.color),
- sortOrder:
- state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
- },
- {
- title: t("courtesycars.fields.plate"),
- dataIndex: "plate",
- key: "plate",
- sorter: (a, b) => alphaSort(a.plate, b.plate),
- sortOrder:
- state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
- },
- ];
+ const columns = [
+ {
+ title: t("courtesycars.fields.fleetnumber"),
+ dataIndex: "fleetnumber",
+ key: "fleetnumber",
+ sorter: (a, b) => alphaSort(a.fleetnumber, b.fleetnumber),
+ sortOrder: state.sortedInfo.columnKey === "fleetnumber" && state.sortedInfo.order
+ },
+ {
+ title: t("courtesycars.fields.status"),
+ dataIndex: "status",
+ key: "status",
+ sorter: (a, b) => alphaSort(a.status, b.status),
+ sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
+ render: (text, record) => {t(record.status)}
+ },
+ {
+ title: t("courtesycars.fields.readiness"),
+ dataIndex: "readiness",
+ key: "readiness",
+ sorter: (a, b) => alphaSort(a.readiness, b.readiness),
+ sortOrder: state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
+ render: (text, record) => t(record.readiness)
+ },
+ {
+ title: t("courtesycars.fields.year"),
+ dataIndex: "year",
+ key: "year",
+ sorter: (a, b) => alphaSort(a.year, b.year),
+ sortOrder: state.sortedInfo.columnKey === "year" && state.sortedInfo.order
+ },
+ {
+ title: t("courtesycars.fields.make"),
+ dataIndex: "make",
+ key: "make",
+ sorter: (a, b) => alphaSort(a.make, b.make),
+ sortOrder: state.sortedInfo.columnKey === "make" && state.sortedInfo.order
+ },
+ {
+ title: t("courtesycars.fields.model"),
+ dataIndex: "model",
+ key: "model",
+ sorter: (a, b) => alphaSort(a.model, b.model),
+ sortOrder: state.sortedInfo.columnKey === "model" && state.sortedInfo.order
+ },
+ {
+ title: t("courtesycars.fields.color"),
+ dataIndex: "color",
+ key: "color",
+ sorter: (a, b) => alphaSort(a.color, b.color),
+ sortOrder: state.sortedInfo.columnKey === "color" && state.sortedInfo.order
+ },
+ {
+ title: t("courtesycars.fields.plate"),
+ dataIndex: "plate",
+ key: "plate",
+ sorter: (a, b) => alphaSort(a.plate, b.plate),
+ sortOrder: state.sortedInfo.columnKey === "plate" && state.sortedInfo.order
+ }
+ ];
- const handleTableChange = (pagination, filters, sorter) => {
- setState({...state, filteredInfo: filters, sortedInfo: sorter});
- };
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
- const filteredData =
- state.search === ""
- ? data
- : data.filter(
- (cc) =>
- (cc.fleetnumber || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (cc.status || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (cc.year || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (cc.make || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (cc.model || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (cc.color || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
- );
+ const filteredData =
+ state.search === ""
+ ? data
+ : data.filter(
+ (cc) =>
+ (cc.fleetnumber || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (cc.status || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (cc.year || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (cc.make || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (cc.model || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (cc.color || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
+ );
- return (
- setState({...state, search: e.target.value})}
- />
+ return (
+ setState({ ...state, search: e.target.value })}
+ />
+ }
+ >
+ {
+ return {
+ onClick: (event) => {
+ handleSelect(record);
}
- >
- {
- return {
- onClick: (event) => {
- handleSelect(record);
- },
- };
- }}
- />
-
- );
+ };
+ }}
+ />
+
+ );
}
diff --git a/client/src/components/contract-cars/contract-cars.container.jsx b/client/src/components/contract-cars/contract-cars.container.jsx
index 9d5f23355..24364313d 100644
--- a/client/src/components/contract-cars/contract-cars.container.jsx
+++ b/client/src/components/contract-cars/contract-cars.container.jsx
@@ -1,37 +1,37 @@
-import {useQuery} from "@apollo/client";
+import { useQuery } from "@apollo/client";
import dayjs from "../../utils/day";
import React from "react";
-import {QUERY_AVAILABLE_CC} from "../../graphql/courtesy-car.queries";
+import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
import AlertComponent from "../alert/alert.component";
import ContractCarsComponent from "./contract-cars.component";
-export default function ContractCarsContainer({selectedCarState, form}) {
- const {loading, error, data} = useQuery(QUERY_AVAILABLE_CC, {
- variables: {today: dayjs().format("YYYY-MM-DD")},
- fetchPolicy: "network-only",
- nextFetchPolicy: "network-only",
+export default function ContractCarsContainer({ selectedCarState, form }) {
+ const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, {
+ variables: { today: dayjs().format("YYYY-MM-DD") },
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
+
+ const [selectedCar, setSelectedCar] = selectedCarState;
+
+ const handleSelect = (record) => {
+ setSelectedCar(record);
+
+ form.setFieldsValue({
+ kmstart: record.mileage,
+ dailyrate: record.dailycost,
+ fuelout: record.fuel,
+ damage: record.damage
});
+ };
- const [selectedCar, setSelectedCar] = selectedCarState;
-
- const handleSelect = (record) => {
- setSelectedCar(record);
-
- form.setFieldsValue({
- kmstart: record.mileage,
- dailyrate: record.dailycost,
- fuelout: record.fuel,
- damage: record.damage,
- });
- };
-
- if (error) return ;
- return (
-
- );
+ if (error) return ;
+ return (
+
+ );
}
diff --git a/client/src/components/contract-convert-to-ro/contract-convert-to-ro.component.jsx b/client/src/components/contract-convert-to-ro/contract-convert-to-ro.component.jsx
index b267d0dc6..ee8d01ed6 100644
--- a/client/src/components/contract-convert-to-ro/contract-convert-to-ro.component.jsx
+++ b/client/src/components/contract-convert-to-ro/contract-convert-to-ro.component.jsx
@@ -1,397 +1,381 @@
-import {useMutation} from "@apollo/client";
-import {Button, Form, InputNumber, notification, Popover, Radio, Select, Space,} from "antd";
+import { useMutation } from "@apollo/client";
+import { Button, Form, InputNumber, notification, Popover, Radio, Select, Space } from "antd";
import axios from "axios";
import dayjs from "../../utils/day";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {useNavigate} from "react-router-dom";
-import {createStructuredSelector} from "reselect";
-import {INSERT_NEW_JOB} from "../../graphql/jobs.queries";
-import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
+import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
- //currentUser: selectCurrentUser
- bodyshop: selectBodyshop,
- currentUser: selectCurrentUser,
+ //currentUser: selectCurrentUser
+ bodyshop: selectBodyshop,
+ currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export function ContractConvertToRo({
- bodyshop,
- currentUser,
- contract,
- disabled,
- }) {
- const {t} = useTranslation();
- const [open, setOpen] = useState(false);
- const [loading, setLoading] = useState(false);
- const [insertJob] = useMutation(INSERT_NEW_JOB);
- const history = useNavigate();
+export function ContractConvertToRo({ bodyshop, currentUser, contract, disabled }) {
+ const { t } = useTranslation();
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [insertJob] = useMutation(INSERT_NEW_JOB);
+ const history = useNavigate();
- const handleFinish = async (values) => {
- setLoading(true);
+ const handleFinish = async (values) => {
+ setLoading(true);
- const contractLength = dayjs(contract.actualreturn).diff(
- dayjs(contract.start),
- "day"
- );
- const billingLines = [];
- if (contractLength > 0)
- billingLines.push({
- manual_line: true,
- unq_seq: 1,
- line_no: 1,
- line_ref: 1,
- line_desc: t("contracts.fields.dailyrate"),
- db_price: contract.dailyrate,
- act_price: contract.dailyrate,
- part_qty: contractLength,
- part_type: "CCDR",
- tax_part: true,
- mod_lb_hrs: 0,
- db_ref: "io-ccdr",
- // mod_lbr_ty: "PAL",
- });
- const mileageDiff =
- contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength;
- if (mileageDiff > 0) {
- billingLines.push({
- manual_line: true,
- unq_seq: 2,
- line_no: 2,
- line_ref: 2,
- line_desc: "Fuel Surcharge",
- db_price: contract.excesskmrate,
- act_price: contract.excesskmrate,
- part_type: "CCM",
- part_qty: mileageDiff,
- tax_part: true,
- db_ref: "io-ccm",
- mod_lb_hrs: 0,
- });
+ const contractLength = dayjs(contract.actualreturn).diff(dayjs(contract.start), "day");
+ const billingLines = [];
+ if (contractLength > 0)
+ billingLines.push({
+ manual_line: true,
+ unq_seq: 1,
+ line_no: 1,
+ line_ref: 1,
+ line_desc: t("contracts.fields.dailyrate"),
+ db_price: contract.dailyrate,
+ act_price: contract.dailyrate,
+ part_qty: contractLength,
+ part_type: "CCDR",
+ tax_part: true,
+ mod_lb_hrs: 0,
+ db_ref: "io-ccdr"
+ // mod_lbr_ty: "PAL",
+ });
+ const mileageDiff = contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength;
+ if (mileageDiff > 0) {
+ billingLines.push({
+ manual_line: true,
+ unq_seq: 2,
+ line_no: 2,
+ line_ref: 2,
+ line_desc: "Fuel Surcharge",
+ db_price: contract.excesskmrate,
+ act_price: contract.excesskmrate,
+ part_type: "CCM",
+ part_qty: mileageDiff,
+ tax_part: true,
+ db_ref: "io-ccm",
+ mod_lb_hrs: 0
+ });
+ }
+
+ if (values.refuelqty > 0) {
+ billingLines.push({
+ manual_line: true,
+ unq_seq: 3,
+ line_no: 3,
+ line_ref: 3,
+ line_desc: t("contracts.fields.refuelcharge"),
+ db_price: contract.refuelcharge,
+ act_price: contract.refuelcharge,
+ part_qty: values.refuelqty,
+ part_type: "CCF",
+ tax_part: true,
+ db_ref: "io-ccf",
+ mod_lb_hrs: 0
+ });
+ }
+ if (values.applyCleanupCharge) {
+ billingLines.push({
+ manual_line: true,
+ unq_seq: 4,
+ line_no: 4,
+ line_ref: 4,
+ line_desc: t("contracts.fields.cleanupcharge"),
+ db_price: contract.cleanupcharge,
+ act_price: contract.cleanupcharge,
+ part_qty: 1,
+ part_type: "CCC",
+ tax_part: true,
+ db_ref: "io-ccc",
+ mod_lb_hrs: 0
+ });
+ }
+ if (contract.damagewaiver) {
+ //Add for cleanup fee.
+ billingLines.push({
+ manual_line: true,
+ unq_seq: 5,
+ line_no: 5,
+ line_ref: 5,
+ line_desc: t("contracts.fields.damagewaiver"),
+ db_price: contract.damagewaiver,
+ act_price: contract.damagewaiver,
+ part_type: "CCD",
+ part_qty: 1,
+ tax_part: true,
+ db_ref: "io-ccd",
+ mod_lb_hrs: 0
+ });
+ }
+
+ const newJob = {
+ // converted: true,
+ shopid: bodyshop.id,
+ ownerid: contract.job.ownerid,
+ vehicleid: contract.job.vehicleid,
+ federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
+ state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100,
+ local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
+ ins_co_nm: values.ins_co_nm,
+ class: values.class,
+ converted: true,
+ clm_no: contract.job.clm_no ? `${contract.job.clm_no}-CC` : null,
+ ownr_fn: contract.job.owner.ownr_fn,
+ ownr_ln: contract.job.owner.ownr_ln,
+ ownr_co_nm: contract.job.owner.ownr_co_nm,
+ ownr_ph1: contract.job.owner.ownr_ph1,
+ ownr_ea: contract.job.owner.ownr_ea,
+ v_model_desc: contract.job.vehicle && contract.job.vehicle.v_model_desc,
+ v_model_yr: contract.job.vehicle && contract.job.vehicle.v_model_yr,
+ v_make_desc: contract.job.vehicle && contract.job.vehicle.v_make_desc,
+ v_vin: contract.job.vehicle && contract.job.vehicle.v_vin,
+ status: bodyshop.md_ro_statuses.default_completed,
+ notes: {
+ data: [
+ {
+ text: t("contracts.labels.noteconvertedfrom", {
+ agreementnumber: contract.agreementnumber
+ }),
+ audit: true,
+ created_by: currentUser.email
+ }
+ ]
+ },
+ joblines: {
+ data: billingLines
+ },
+ parts_tax_rates: {
+ PAA: {
+ prt_type: "PAA",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ PAC: {
+ prt_type: "PAC",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ PAL: {
+ prt_type: "PAL",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ PAM: {
+ prt_type: "PAM",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ PAN: {
+ prt_type: "PAN",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ PAR: {
+ prt_type: "PAR",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ PAS: {
+ prt_type: "PAS",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ CCDR: {
+ prt_type: "CCDR",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ CCF: {
+ prt_type: "CCF",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ CCM: {
+ prt_type: "CCM",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ CCC: {
+ prt_type: "CCC",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
+ },
+ CCD: {
+ prt_type: "CCD",
+ prt_discp: 0,
+ prt_mktyp: false,
+ prt_mkupp: 0,
+ prt_tax_in: true,
+ prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
}
-
- if (values.refuelqty > 0) {
- billingLines.push({
- manual_line: true,
- unq_seq: 3,
- line_no: 3,
- line_ref: 3,
- line_desc: t("contracts.fields.refuelcharge"),
- db_price: contract.refuelcharge,
- act_price: contract.refuelcharge,
- part_qty: values.refuelqty,
- part_type: "CCF",
- tax_part: true,
- db_ref: "io-ccf",
- mod_lb_hrs: 0,
- });
- }
- if (values.applyCleanupCharge) {
- billingLines.push({
- manual_line: true,
- unq_seq: 4,
- line_no: 4,
- line_ref: 4,
- line_desc: t("contracts.fields.cleanupcharge"),
- db_price: contract.cleanupcharge,
- act_price: contract.cleanupcharge,
- part_qty: 1,
- part_type: "CCC",
- tax_part: true,
- db_ref: "io-ccc",
- mod_lb_hrs: 0,
- });
- }
- if (contract.damagewaiver) {
- //Add for cleanup fee.
- billingLines.push({
- manual_line: true,
- unq_seq: 5,
- line_no: 5,
- line_ref: 5,
- line_desc: t("contracts.fields.damagewaiver"),
- db_price: contract.damagewaiver,
- act_price: contract.damagewaiver,
- part_type: "CCD",
- part_qty: 1,
- tax_part: true,
- db_ref: "io-ccd",
- mod_lb_hrs: 0,
- });
- }
-
- const newJob = {
- // converted: true,
- shopid: bodyshop.id,
- ownerid: contract.job.ownerid,
- vehicleid: contract.job.vehicleid,
- federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
- state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100,
- local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
- ins_co_nm: values.ins_co_nm,
- class: values.class,
- converted: true,
- clm_no: contract.job.clm_no ? `${contract.job.clm_no}-CC` : null,
- ownr_fn: contract.job.owner.ownr_fn,
- ownr_ln: contract.job.owner.ownr_ln,
- ownr_co_nm: contract.job.owner.ownr_co_nm,
- ownr_ph1: contract.job.owner.ownr_ph1,
- ownr_ea: contract.job.owner.ownr_ea,
- v_model_desc: contract.job.vehicle && contract.job.vehicle.v_model_desc,
- v_model_yr: contract.job.vehicle && contract.job.vehicle.v_model_yr,
- v_make_desc: contract.job.vehicle && contract.job.vehicle.v_make_desc,
- v_vin: contract.job.vehicle && contract.job.vehicle.v_vin,
- status: bodyshop.md_ro_statuses.default_completed,
- notes: {
- data: [
- {
- text: t("contracts.labels.noteconvertedfrom", {
- agreementnumber: contract.agreementnumber,
- }),
- audit: true,
- created_by: currentUser.email,
- },
- ],
- },
- joblines: {
- data: billingLines,
- },
- parts_tax_rates: {
- PAA: {
- prt_type: "PAA",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- PAC: {
- prt_type: "PAC",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- PAL: {
- prt_type: "PAL",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- PAM: {
- prt_type: "PAM",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- PAN: {
- prt_type: "PAN",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- PAR: {
- prt_type: "PAR",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- PAS: {
- prt_type: "PAS",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- CCDR: {
- prt_type: "CCDR",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- CCF: {
- prt_type: "CCF",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- CCM: {
- prt_type: "CCM",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- CCC: {
- prt_type: "CCC",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- CCD: {
- prt_type: "CCD",
- prt_discp: 0,
- prt_mktyp: false,
- prt_mkupp: 0,
- prt_tax_in: true,
- prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
- },
- },
- };
-
- //Calcualte the new job totals.
-
- const newTotals = (
- await axios.post("/job/totals", {
- job: {...newJob, joblines: billingLines},
- })
- ).data;
-
- newJob.clm_total = newTotals.totals.total_repairs.amount / 100;
- newJob.job_totals = newTotals;
-
- const result = await insertJob({
- variables: {job: [newJob]},
- // refetchQueries: ["GET_JOB_BY_PK"],
- // awaitRefetchQueries: true,
- });
-
- if (!!result.errors) {
- notification["error"]({
- message: t("jobs.errors.inserting", {
- message: JSON.stringify(result.errors),
- }),
- });
- } else {
- notification["success"]({
- message: t("jobs.successes.created"),
- onClick: () => {
- history.push(
- `/manage/jobs/${result.data.insert_jobs.returning[0].id}`
- );
- },
- });
- }
-
- setOpen(false);
- setLoading(false);
+ }
};
- const popContent = (
-
-
-
- {bodyshop.md_ins_cos.map((s) => (
-
- {s.name}
-
- ))}
-
-
-
-
- {bodyshop.md_classes.map((s) => (
-
- {s}
-
- ))}
-
-
-
-
- {t("general.labels.yes")}
- {t("general.labels.no")}
-
-
-
-
-
-
-
- {t("contracts.actions.convertoro")}
-
- setOpen(false)}>
- {t("general.actions.close")}
-
-
-
-
- );
+ //Calcualte the new job totals.
- return (
-
-
- setOpen(true)}
- loading={loading}
- disabled={!contract.dailyrate || !contract.actualreturn || disabled}
- >
- {t("contracts.actions.convertoro")}
-
-
-
- );
+ const newTotals = (
+ await axios.post("/job/totals", {
+ job: { ...newJob, joblines: billingLines }
+ })
+ ).data;
+
+ newJob.clm_total = newTotals.totals.total_repairs.amount / 100;
+ newJob.job_totals = newTotals;
+
+ const result = await insertJob({
+ variables: { job: [newJob] }
+ // refetchQueries: ["GET_JOB_BY_PK"],
+ // awaitRefetchQueries: true,
+ });
+
+ if (!!result.errors) {
+ notification["error"]({
+ message: t("jobs.errors.inserting", {
+ message: JSON.stringify(result.errors)
+ })
+ });
+ } else {
+ notification["success"]({
+ message: t("jobs.successes.created"),
+ onClick: () => {
+ history.push(`/manage/jobs/${result.data.insert_jobs.returning[0].id}`);
+ }
+ });
+ }
+
+ setOpen(false);
+ setLoading(false);
+ };
+
+ const popContent = (
+
+
+
+ {bodyshop.md_ins_cos.map((s) => (
+
+ {s.name}
+
+ ))}
+
+
+
+
+ {bodyshop.md_classes.map((s) => (
+
+ {s}
+
+ ))}
+
+
+
+
+ {t("general.labels.yes")}
+ {t("general.labels.no")}
+
+
+
+
+
+
+
+ {t("contracts.actions.convertoro")}
+
+ setOpen(false)}>{t("general.actions.close")}
+
+
+
+ );
+
+ return (
+
+
+ setOpen(true)}
+ loading={loading}
+ disabled={!contract.dailyrate || !contract.actualreturn || disabled}
+ >
+ {t("contracts.actions.convertoro")}
+
+
+
+ );
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(ContractConvertToRo);
+export default connect(mapStateToProps, mapDispatchToProps)(ContractConvertToRo);
diff --git a/client/src/components/contract-courtesy-car-block/contract-courtesy-car-block.component.jsx b/client/src/components/contract-courtesy-car-block/contract-courtesy-car-block.component.jsx
index d95e1b52e..75aaa3c9f 100644
--- a/client/src/components/contract-courtesy-car-block/contract-courtesy-car-block.component.jsx
+++ b/client/src/components/contract-courtesy-car-block/contract-courtesy-car-block.component.jsx
@@ -1,32 +1,26 @@
-import {Card} from "antd";
+import { Card } from "antd";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {Link} from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router-dom";
import DataLabel from "../data-label/data-label.component";
-export default function ContractCourtesyCarBlock({courtesyCar}) {
- const {t} = useTranslation();
- return (
-
-
-
-
- {(courtesyCar && courtesyCar.fleetnumber) || ""}
-
-
- {(courtesyCar && courtesyCar.plate) || ""}
-
-
- {`${(courtesyCar && courtesyCar.year) || ""} ${
- (courtesyCar && courtesyCar.make) || ""
- } ${(courtesyCar && courtesyCar.model) || ""}`}
-
-
-
-
- );
+export default function ContractCourtesyCarBlock({ courtesyCar }) {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+ {(courtesyCar && courtesyCar.fleetnumber) || ""}
+
+ {(courtesyCar && courtesyCar.plate) || ""}
+
+ {`${(courtesyCar && courtesyCar.year) || ""} ${
+ (courtesyCar && courtesyCar.make) || ""
+ } ${(courtesyCar && courtesyCar.model) || ""}`}
+
+
+
+
+ );
}
diff --git a/client/src/components/contract-form/contract-form-job-prefill.component.jsx b/client/src/components/contract-form/contract-form-job-prefill.component.jsx
index 639ba21b4..76f32023f 100644
--- a/client/src/components/contract-form/contract-form-job-prefill.component.jsx
+++ b/client/src/components/contract-form/contract-form-job-prefill.component.jsx
@@ -1,45 +1,43 @@
-import {useLazyQuery} from "@apollo/client";
-import {Button, notification} from "antd";
-import React, {useEffect} from "react";
-import {useTranslation} from "react-i18next";
-import {GET_JOB_FOR_CC_CONTRACT} from "../../graphql/jobs.queries";
+import { useLazyQuery } from "@apollo/client";
+import { Button, notification } from "antd";
+import React, { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { GET_JOB_FOR_CC_CONTRACT } from "../../graphql/jobs.queries";
-export default function ContractCreateJobPrefillComponent({jobId, form}) {
- const [call, {loading, error, data}] = useLazyQuery(
- GET_JOB_FOR_CC_CONTRACT
- );
- const {t} = useTranslation();
+export default function ContractCreateJobPrefillComponent({ jobId, form }) {
+ const [call, { loading, error, data }] = useLazyQuery(GET_JOB_FOR_CC_CONTRACT);
+ const { t } = useTranslation();
- const handleClick = () => {
- call({variables: {id: jobId}});
- };
+ const handleClick = () => {
+ call({ variables: { id: jobId } });
+ };
- useEffect(() => {
- if (data) {
- form.setFieldsValue({
- driver_dlst: data.jobs_by_pk.ownr_ast,
- driver_fn: data.jobs_by_pk.ownr_fn,
- driver_ln: data.jobs_by_pk.ownr_ln,
- driver_addr1: data.jobs_by_pk.ownr_addr1,
- driver_state: data.jobs_by_pk.ownr_st,
- driver_city: data.jobs_by_pk.ownr_city,
- driver_zip: data.jobs_by_pk.ownr_zip,
- driver_ph1: data.jobs_by_pk.ownr_ph1,
- });
- }
- }, [data, form]);
-
- if (error) {
- notification["error"]({
- message: t("contracts.errors.fetchingjobinfo", {
- error: JSON.stringify(error),
- }),
- });
+ useEffect(() => {
+ if (data) {
+ form.setFieldsValue({
+ driver_dlst: data.jobs_by_pk.ownr_ast,
+ driver_fn: data.jobs_by_pk.ownr_fn,
+ driver_ln: data.jobs_by_pk.ownr_ln,
+ driver_addr1: data.jobs_by_pk.ownr_addr1,
+ driver_state: data.jobs_by_pk.ownr_st,
+ driver_city: data.jobs_by_pk.ownr_city,
+ driver_zip: data.jobs_by_pk.ownr_zip,
+ driver_ph1: data.jobs_by_pk.ownr_ph1
+ });
}
+ }, [data, form]);
- return (
-
- {t("contracts.labels.populatefromjob")}
-
- );
+ if (error) {
+ notification["error"]({
+ message: t("contracts.errors.fetchingjobinfo", {
+ error: JSON.stringify(error)
+ })
+ });
+ }
+
+ return (
+
+ {t("contracts.labels.populatefromjob")}
+
+ );
}
diff --git a/client/src/components/contract-form/contract-form.component.jsx b/client/src/components/contract-form/contract-form.component.jsx
index ae983a6c6..eeac43701 100644
--- a/client/src/components/contract-form/contract-form.component.jsx
+++ b/client/src/components/contract-form/contract-form.component.jsx
@@ -1,9 +1,9 @@
-import {WarningFilled} from "@ant-design/icons";
-import {Form, Input, InputNumber, Space} from "antd";
+import { WarningFilled } from "@ant-design/icons";
+import { Form, Input, InputNumber, Space } from "antd";
import dayjs from "../../utils/day";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {DateFormatter} from "../../utils/DateFormatter";
+import { useTranslation } from "react-i18next";
+import { DateFormatter } from "../../utils/DateFormatter";
//import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
@@ -11,361 +11,310 @@ import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
-import InputPhone, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component";
+import InputPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import ContractFormJobPrefill from "./contract-form-job-prefill.component";
-export default function ContractFormComponent({
- form,
- create = false,
- selectedJobState,
- selectedCar,
- }) {
- const {t} = useTranslation();
- return (
-
+ );
}
diff --git a/client/src/components/contract-job-block/contract-job-block.component.jsx b/client/src/components/contract-job-block/contract-job-block.component.jsx
index e0d6af75b..526860b6b 100644
--- a/client/src/components/contract-job-block/contract-job-block.component.jsx
+++ b/client/src/components/contract-job-block/contract-job-block.component.jsx
@@ -1,33 +1,25 @@
-import {Card} from "antd";
+import { Card } from "antd";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {Link} from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router-dom";
import DataLabel from "../data-label/data-label.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
-export default function ContractJobBlock({job}) {
- const {t} = useTranslation();
- return (
-
-
-
-
- {(job && job.ro_number) || ""}
-
-
- {`${(job && job.v_model_yr) || ""} ${
- (job && job.v_make_desc) || ""
- } ${(job && job.v_model_desc) || ""}`}
-
-
-
-
-
-
-
- );
+export default function ContractJobBlock({ job }) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {(job && job.ro_number) || ""}
+
+ {`${(job && job.v_model_yr) || ""} ${(job && job.v_make_desc) || ""} ${(job && job.v_model_desc) || ""}`}
+
+
+
+
+
+
+
+ );
}
diff --git a/client/src/components/contract-jobs/contract-jobs.component.jsx b/client/src/components/contract-jobs/contract-jobs.component.jsx
index 84dfd6d4e..ea71c2538 100644
--- a/client/src/components/contract-jobs/contract-jobs.component.jsx
+++ b/client/src/components/contract-jobs/contract-jobs.component.jsx
@@ -1,201 +1,155 @@
-import {Card, Input, Table} from "antd";
-import React, {useMemo, useState} from "react";
-import {useTranslation} from "react-i18next";
-import {alphaSort} from "../../utils/sorters";
+import { Card, Input, Table } from "antd";
+import React, { useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { alphaSort } from "../../utils/sorters";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
-import {pageLimit} from "../../utils/config";
+import { pageLimit } from "../../utils/config";
-export default function ContractsJobsComponent({
- loading,
- data,
- selectedJob,
- handleSelect,
- }) {
- const [state, setState] = useState({
- sortedInfo: {},
- filteredInfo: {text: ""},
- search: "",
- });
+export default function ContractsJobsComponent({ loading, data, selectedJob, handleSelect }) {
+ const [state, setState] = useState({
+ sortedInfo: {},
+ filteredInfo: { text: "" },
+ search: ""
+ });
- const {t} = useTranslation();
+ const { t } = useTranslation();
- const columns = [
- {
- title: t("jobs.fields.ro_number"),
- dataIndex: "ro_number",
- key: "ro_number",
- width: "8%",
- sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
- sortOrder:
- state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+ const columns = [
+ {
+ title: t("jobs.fields.ro_number"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ width: "8%",
+ sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
+ sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
- render: (text, record) => (
-
- {record.ro_number ? record.ro_number : t("general.labels.na")}
-
- ),
- },
- {
- title: t("jobs.fields.owner"),
- dataIndex: "owner",
- key: "owner",
- ellipsis: true,
- sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
- width: "25%",
- sortOrder:
- state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
- render: (text, record) => ,
- },
- {
- title: t("jobs.fields.status"),
- dataIndex: "status",
- key: "status",
- width: "10%",
- ellipsis: true,
- sorter: (a, b) => alphaSort(a.status, b.status),
- sortOrder:
- state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
- render: (text, record) => {
- return record.status || t("general.labels.na");
- },
- },
+ render: (text, record) => {record.ro_number ? record.ro_number : t("general.labels.na")}
+ },
+ {
+ title: t("jobs.fields.owner"),
+ dataIndex: "owner",
+ key: "owner",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
+ width: "25%",
+ sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
+ render: (text, record) =>
+ },
+ {
+ title: t("jobs.fields.status"),
+ dataIndex: "status",
+ key: "status",
+ width: "10%",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.status, b.status),
+ sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.status || t("general.labels.na");
+ }
+ },
- {
- title: t("jobs.fields.vehicle"),
- dataIndex: "vehicle",
- key: "vehicle",
- width: "15%",
- ellipsis: true,
- render: (text, record) => {
- return record.vehicleid ? (
-
- {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
- record.v_model_desc || ""
- }`}
-
- ) : (
- t("jobs.errors.novehicle")
- );
- },
- },
- {
- title: t("vehicles.fields.plate_no"),
- dataIndex: "plate_no",
- key: "plate_no",
- width: "8%",
- ellipsis: true,
- sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
- sortOrder:
- state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
- render: (text, record) => {
- return record.plate_no ? (
- {record.plate_no}
- ) : (
- t("general.labels.unknown")
- );
- },
- },
- {
- title: t("jobs.fields.clm_no"),
- dataIndex: "clm_no",
- key: "clm_no",
- width: "12%",
- ellipsis: true,
- sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
- sortOrder:
- state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
- render: (text, record) => {
- return record.clm_no ? (
- {record.clm_no}
- ) : (
- t("general.labels.unknown")
- );
- },
- },
- ];
+ {
+ title: t("jobs.fields.vehicle"),
+ dataIndex: "vehicle",
+ key: "vehicle",
+ width: "15%",
+ ellipsis: true,
+ render: (text, record) => {
+ return record.vehicleid ? (
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
+ ) : (
+ t("jobs.errors.novehicle")
+ );
+ }
+ },
+ {
+ title: t("vehicles.fields.plate_no"),
+ dataIndex: "plate_no",
+ key: "plate_no",
+ width: "8%",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
+ sortOrder: state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.plate_no ? {record.plate_no} : t("general.labels.unknown");
+ }
+ },
+ {
+ title: t("jobs.fields.clm_no"),
+ dataIndex: "clm_no",
+ key: "clm_no",
+ width: "12%",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
+ sortOrder: state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.clm_no ? {record.clm_no} : t("general.labels.unknown");
+ }
+ }
+ ];
- const handleTableChange = (pagination, filters, sorter) => {
- setState({...state, filteredInfo: filters, sortedInfo: sorter});
- };
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
- const filteredData =
- state.search === ""
- ? data
- : data.filter(
- (j) =>
- (j.ro_number || "")
- .toString()
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (j.ownr_co_nm || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (j.ownr_fn || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (j.ownr_ln || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (j.clm_no || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (j.v_make_desc || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (j.v_model_desc || "")
- .toLowerCase()
- .includes(state.search.toLowerCase()) ||
- (j.plate_no || "")
- .toLowerCase()
- .includes(state.search.toLowerCase())
- );
+ const filteredData =
+ state.search === ""
+ ? data
+ : data.filter(
+ (j) =>
+ (j.ro_number || "").toString().toLowerCase().includes(state.search.toLowerCase()) ||
+ (j.ownr_co_nm || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (j.ownr_fn || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (j.ownr_ln || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (j.clm_no || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (j.v_make_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (j.v_model_desc || "").toLowerCase().includes(state.search.toLowerCase()) ||
+ (j.plate_no || "").toLowerCase().includes(state.search.toLowerCase())
+ );
- const defaultCurrent = useMemo(() => {
- const page =
- Math.floor(
- (filteredData.findIndex((v) => v.id === selectedJob) || 0) / 10
- ) + 1;
- if (page === 0) return 1;
- return page;
- }, [filteredData, selectedJob]);
+ const defaultCurrent = useMemo(() => {
+ const page = Math.floor((filteredData.findIndex((v) => v.id === selectedJob) || 0) / 10) + 1;
+ if (page === 0) return 1;
+ return page;
+ }, [filteredData, selectedJob]);
- if (loading) return ;
- return (
- setState({...state, search: e.target.value})}
- />
+ if (loading) return ;
+ return (
+ setState({ ...state, search: e.target.value })}
+ />
+ }
+ >
+ {
+ return {
+ onClick: (event) => {
+ handleSelect(record);
}
- >
- {
- return {
- onClick: (event) => {
- handleSelect(record);
- },
- };
- }}
- />
-
- );
+ };
+ }}
+ />
+
+ );
}
diff --git a/client/src/components/contract-jobs/contract-jobs.container.jsx b/client/src/components/contract-jobs/contract-jobs.container.jsx
index 926d89330..1db612a03 100644
--- a/client/src/components/contract-jobs/contract-jobs.container.jsx
+++ b/client/src/components/contract-jobs/contract-jobs.container.jsx
@@ -1,41 +1,41 @@
-import {useQuery} from "@apollo/client";
+import { useQuery } from "@apollo/client";
import React from "react";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {QUERY_ALL_ACTIVE_JOBS} from "../../graphql/jobs.queries";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import ContractJobsComponent from "./contract-jobs.component";
const mapStateToProps = createStructuredSelector({
- //currentUser: selectCurrentUser
- bodyshop: selectBodyshop,
+ //currentUser: selectCurrentUser
+ bodyshop: selectBodyshop
});
-export function ContractJobsContainer({selectedJobState, bodyshop}) {
- const {loading, error, data} = useQuery(QUERY_ALL_ACTIVE_JOBS, {
- variables: {
- statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
- isConverted: true,
- },
- fetchPolicy: "network-only",
- nextFetchPolicy: "network-only",
- });
- const [selectedJob, setSelectedJob] = selectedJobState;
+export function ContractJobsContainer({ selectedJobState, bodyshop }) {
+ const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
+ variables: {
+ statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
+ isConverted: true
+ },
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
+ const [selectedJob, setSelectedJob] = selectedJobState;
- const handleSelect = (record) => {
- setSelectedJob(record.id);
- };
+ const handleSelect = (record) => {
+ setSelectedJob(record.id);
+ };
- if (error) return ;
- return (
-
- );
+ if (error) return ;
+ return (
+
+ );
}
export default connect(mapStateToProps, null)(ContractJobsContainer);
diff --git a/client/src/components/contract-license-decode-button/contract-license-decode-button.component.jsx b/client/src/components/contract-license-decode-button/contract-license-decode-button.component.jsx
index 49da3a5b2..f6bbf7e02 100644
--- a/client/src/components/contract-license-decode-button/contract-license-decode-button.component.jsx
+++ b/client/src/components/contract-license-decode-button/contract-license-decode-button.component.jsx
@@ -1,123 +1,101 @@
-import {Button, Input, Modal, Typography} from "antd";
+import { Button, Input, Modal, Typography } from "antd";
import dayjs from "../../utils/day";
-import React, {useState} from "react";
-import {useTranslation} from "react-i18next";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
import aamva from "../../utils/aamva";
import DataLabel from "../data-label/data-label.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
-import {logImEXEvent} from "../../firebase/firebase.utils";
+import { logImEXEvent } from "../../firebase/firebase.utils";
-export default function ContractLicenseDecodeButton({form}) {
- const {t} = useTranslation();
- const [modalVisible, setModalVisible] = useState(false);
- const [loading, setLoading] = useState(false);
- const [decodedBarcode, setDecodedBarcode] = useState(null);
+export default function ContractLicenseDecodeButton({ form }) {
+ const { t } = useTranslation();
+ const [modalVisible, setModalVisible] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [decodedBarcode, setDecodedBarcode] = useState(null);
- const handleDecode = (e) => {
- logImEXEvent("contract_license_decode");
- setLoading(true);
- const aamvaParse = aamva.parse(e.currentTarget.value);
- setDecodedBarcode(aamvaParse);
- setLoading(false);
+ const handleDecode = (e) => {
+ logImEXEvent("contract_license_decode");
+ setLoading(true);
+ const aamvaParse = aamva.parse(e.currentTarget.value);
+ setDecodedBarcode(aamvaParse);
+ setLoading(false);
+ };
+
+ const handleInsertForm = () => {
+ logImEXEvent("contract_license_decode_fill_form");
+
+ const values = {
+ driver_dlnumber: decodedBarcode.dl,
+ driver_dlexpiry: dayjs(`20${decodedBarcode.expiration_date}${dayjs(decodedBarcode.birthday).format("DD")}`),
+ driver_dlst: decodedBarcode.state,
+ driver_fn: decodedBarcode.name.first,
+ driver_ln: decodedBarcode.name.last,
+ driver_addr1: decodedBarcode.address,
+ driver_city: decodedBarcode.city,
+ driver_state: decodedBarcode.state,
+ driver_zip: decodedBarcode.postal_code,
+ driver_dob: dayjs(decodedBarcode.birthday)
};
- const handleInsertForm = () => {
- logImEXEvent("contract_license_decode_fill_form");
+ form.setFieldsValue(values);
+ setModalVisible(false);
+ setDecodedBarcode(null);
+ };
+ const handleClick = () => {
+ setModalVisible(true);
+ };
+ const handleCancel = () => {
+ setModalVisible(false);
+ };
- const values = {
- driver_dlnumber: decodedBarcode.dl,
- driver_dlexpiry: dayjs(
- `20${decodedBarcode.expiration_date}${dayjs(
- decodedBarcode.birthday
- ).format("DD")}`
- ),
- driver_dlst: decodedBarcode.state,
- driver_fn: decodedBarcode.name.first,
- driver_ln: decodedBarcode.name.last,
- driver_addr1: decodedBarcode.address,
- driver_city: decodedBarcode.city,
- driver_state: decodedBarcode.state,
- driver_zip: decodedBarcode.postal_code,
- driver_dob: dayjs(decodedBarcode.birthday),
- };
-
- form.setFieldsValue(values);
- setModalVisible(false);
- setDecodedBarcode(null);
- };
- const handleClick = () => {
- setModalVisible(true);
- };
- const handleCancel = () => {
- setModalVisible(false);
- };
-
- return (
+ return (
+
+
-
+
+ {
+ if (!loading) setLoading(true);
+ }}
+ onPressEnter={handleDecode}
+ />
+
+
+ {decodedBarcode ? (
+
+
{decodedBarcode.state}
+
{decodedBarcode.dl}
+
{decodedBarcode.name.first}
+
{decodedBarcode.name.last}
+
{decodedBarcode.address}
+
{decodedBarcode.address}
+
+ {dayjs(`20${decodedBarcode.expiration_date}${dayjs(decodedBarcode.birthday).format("DD")}`).format(
+ "MM/DD/YYYY"
+ )}
+
+
+ {dayjs(decodedBarcode.birthday).format("MM/DD/YYYY")}
+
-
- {
- if (!loading) setLoading(true);
- }}
- onPressEnter={handleDecode}
- />
-
-
- {decodedBarcode ? (
-
-
- {decodedBarcode.state}
-
-
- {decodedBarcode.dl}
-
-
- {decodedBarcode.name.first}
-
-
- {decodedBarcode.name.last}
-
-
- {decodedBarcode.address}
-
-
- {decodedBarcode.address}
-
-
- {dayjs(
- `20${decodedBarcode.expiration_date}${dayjs(
- decodedBarcode.birthday
- ).format("DD")}`
- ).format("MM/DD/YYYY")}
-
-
- {dayjs(decodedBarcode.birthday).format("MM/DD/YYYY")}
-
-
-
- {t("contracts.labels.correctdataonform")}
-
-
-
- ) : (
- {t("contracts.labels.waitingforscan")}
- )}
-
+
{t("contracts.labels.correctdataonform")}
-
-
- {t("contracts.actions.decodelicense")}
-
+
+ ) : (
+ {t("contracts.labels.waitingforscan")}
+ )}
+
- );
+
+
{t("contracts.actions.decodelicense")}
+
+ );
}
diff --git a/client/src/components/contract-status-select/contract-status-select.component.jsx b/client/src/components/contract-status-select/contract-status-select.component.jsx
index 72a8a2e5c..8bd048195 100644
--- a/client/src/components/contract-status-select/contract-status-select.component.jsx
+++ b/client/src/components/contract-status-select/contract-status-select.component.jsx
@@ -1,33 +1,31 @@
-import React, {forwardRef, useEffect, useState} from "react";
-import {Select} from "antd";
-import {useTranslation} from "react-i18next";
+import React, { forwardRef, useEffect, useState } from "react";
+import { Select } from "antd";
+import { useTranslation } from "react-i18next";
-const {Option} = Select;
+const { Option } = Select;
-const ContractStatusComponent = ({value, onChange}, ref) => {
- const [option, setOption] = useState(value);
- const {t} = useTranslation();
+const ContractStatusComponent = ({ value, onChange }, ref) => {
+ const [option, setOption] = useState(value);
+ const { t } = useTranslation();
- useEffect(() => {
- if (value !== option && onChange) {
- onChange(option);
- }
- }, [value, option, onChange]);
+ useEffect(() => {
+ if (value !== option && onChange) {
+ onChange(option);
+ }
+ }, [value, option, onChange]);
- return (
-
- {t("contracts.status.new")}
- {t("contracts.status.out")}
-
- {t("contracts.status.returned")}
-
-
- );
+ return (
+
+ {t("contracts.status.new")}
+ {t("contracts.status.out")}
+ {t("contracts.status.returned")}
+
+ );
};
export default forwardRef(ContractStatusComponent);
diff --git a/client/src/components/contracts-find-modal/contracts-find-modal.component.jsx b/client/src/components/contracts-find-modal/contracts-find-modal.component.jsx
index 4bd08369a..52a216e7d 100644
--- a/client/src/components/contracts-find-modal/contracts-find-modal.component.jsx
+++ b/client/src/components/contracts-find-modal/contracts-find-modal.component.jsx
@@ -1,37 +1,37 @@
-import {Form, Input} from "antd";
+import { Form, Input } from "antd";
import React from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {createStructuredSelector} from "reselect";
-import {selectBodyshop} from "../../redux/user/user.selectors";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
+ bodyshop: selectBodyshop
});
export default connect(mapStateToProps, null)(PartsReceiveModalComponent);
-export function PartsReceiveModalComponent({bodyshop, form}) {
- const {t} = useTranslation();
+export function PartsReceiveModalComponent({ bodyshop, form }) {
+ const { t } = useTranslation();
- return (
-
-
-
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+
+
+
+ );
}
diff --git a/client/src/components/contracts-find-modal/contracts-find-modal.container.jsx b/client/src/components/contracts-find-modal/contracts-find-modal.container.jsx
index e7398f6a3..eb3ae6fd4 100644
--- a/client/src/components/contracts-find-modal/contracts-find-modal.container.jsx
+++ b/client/src/components/contracts-find-modal/contracts-find-modal.container.jsx
@@ -1,169 +1,143 @@
-import {useLazyQuery} from "@apollo/client";
-import {Button, Form, Modal, Table} from "antd";
-import React, {useEffect} from "react";
-import {useTranslation} from "react-i18next";
-import {connect} from "react-redux";
-import {Link} from "react-router-dom";
-import {createStructuredSelector} from "reselect";
-import {logImEXEvent} from "../../firebase/firebase.utils";
-import {FIND_CONTRACT} from "../../graphql/cccontracts.queries";
-import {toggleModalVisible} from "../../redux/modals/modals.actions";
-import {selectContractFinder} from "../../redux/modals/modals.selectors";
-import {selectBodyshop} from "../../redux/user/user.selectors";
-import {DateTimeFormatter} from "../../utils/DateFormatter";
+import { useLazyQuery } from "@apollo/client";
+import { Button, Form, Modal, Table } from "antd";
+import React, { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { Link } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { logImEXEvent } from "../../firebase/firebase.utils";
+import { FIND_CONTRACT } from "../../graphql/cccontracts.queries";
+import { toggleModalVisible } from "../../redux/modals/modals.actions";
+import { selectContractFinder } from "../../redux/modals/modals.selectors";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { DateTimeFormatter } from "../../utils/DateFormatter";
import ContractsFindModalComponent from "./contracts-find-modal.component";
import AlertComponent from "../alert/alert.component";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
- contractFinderModal: selectContractFinder,
+ bodyshop: selectBodyshop,
+ contractFinderModal: selectContractFinder
});
const mapDispatchToProps = (dispatch) => ({
- toggleModalVisible: () => dispatch(toggleModalVisible("contractFinder")),
+ toggleModalVisible: () => dispatch(toggleModalVisible("contractFinder"))
});
export function ContractsFindModalContainer({
- contractFinderModal,
- toggleModalVisible,
+ contractFinderModal,
+ toggleModalVisible,
- bodyshop,
- }) {
- const {t} = useTranslation();
+ bodyshop
+}) {
+ const { t } = useTranslation();
- const {open} = contractFinderModal;
+ const { open } = contractFinderModal;
- const [form] = Form.useForm();
+ const [form] = Form.useForm();
- // const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
- const [callSearch, {loading, error, data}] = useLazyQuery(FIND_CONTRACT);
- const handleFinish = async (values) => {
- logImEXEvent("contract_finder_search");
+ // const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
+ const [callSearch, { loading, error, data }] = useLazyQuery(FIND_CONTRACT);
+ const handleFinish = async (values) => {
+ logImEXEvent("contract_finder_search");
- //Execute contract find
+ //Execute contract find
- callSearch({
- variables: {
- plate:
- (values.plate && values.plate !== "" && values.plate) || undefined,
- time: values.time,
+ callSearch({
+ variables: {
+ plate: (values.plate && values.plate !== "" && values.plate) || undefined,
+ time: values.time
+ }
+ });
+ };
+
+ useEffect(() => {
+ if (open) {
+ form.resetFields();
+ }
+ }, [open, form]);
+
+ return (
+ toggleModalVisible()}
+ onOk={() => toggleModalVisible()}
+ destroyOnClose
+ forceRender
+ >
+