Compare commits
2 Commits
feature/ma
...
feature/cy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
345fc48c17 | ||
|
|
b8fe566030 |
@@ -1,4 +1,4 @@
|
||||
<babeledit_project be_version="2.7.1" version="1.2">
|
||||
<babeledit_project version="1.2" be_version="2.7.1">
|
||||
<!--
|
||||
|
||||
BabelEdit project file
|
||||
@@ -4509,27 +4509,6 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>enforce_conversion_csr</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>enforce_referral</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -42098,48 +42077,6 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>returns_grouped_by_vendor_detailed</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>returns_grouped_by_vendor_summary</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>schedule</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -42402,27 +42339,6 @@
|
||||
<folder_node>
|
||||
<name>labels</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>atssummary</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>employeevacation</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// craco.config.js
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const CracoLessPlugin = require("craco-less");
|
||||
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
|
||||
|
||||
module.exports = {
|
||||
@@ -19,6 +20,37 @@ module.exports = {
|
||||
ignore: ["node_modules", "webpack.config.js"],
|
||||
},
|
||||
},
|
||||
{
|
||||
plugin: CracoLessPlugin,
|
||||
options: {
|
||||
lessLoaderOptions: {
|
||||
lessOptions: {
|
||||
modifyVars: {
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? { "@primary-color": "#a51d1d" }
|
||||
: {
|
||||
//"@primary-color": "#1DA57A"
|
||||
}),
|
||||
// "@primary-color": " #1890ff", // primary color for all components
|
||||
// "@link-color": "#1890ff", // link color
|
||||
// "@success-color": "#52c41a", // success state color
|
||||
// "@warning-color": "#faad14", // warning state color
|
||||
// "@error-color": "#f5222d", // error state color
|
||||
// "@font-size-base": "14px", // major text font size
|
||||
// " @heading-color": "rgba(0, 0, 0, 0.85)", // heading text color
|
||||
// "@text-color": "rgba(0, 0, 0, 0.65)", // major text color
|
||||
// "@text-color-secondary": "rgba(0, 0, 0, 0.45)", // secondary text color
|
||||
// "@disabled-color": "rgba(0, 0, 0, 0.25)", // disable state color
|
||||
// "@border-radius-base": "2px", // major border radius
|
||||
// "@border-color-base": "#d9d9d9", // major border color
|
||||
// "@box-shadow-base":
|
||||
// "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers }",
|
||||
},
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webpack: {
|
||||
configure: (webpackConfig) => ({
|
||||
|
||||
9
client/cypress.config.js
Normal file
9
client/cypress.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const { defineConfig } = require("cypress");
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"experimentalStudio": true,
|
||||
"env": {
|
||||
"FIREBASE_USERNAME": "cypress@imex.test",
|
||||
"FIREBASE_PASSWORD": "cypress"
|
||||
}
|
||||
}
|
||||
18
client/cypress/e2e/1-auth/login.cy.js
Normal file
18
client/cypress/e2e/1-auth/login.cy.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context("Actions", () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
// https://on.cypress.io/interacting-with-elements
|
||||
|
||||
it("Attempt a failed login.", () => {
|
||||
cy.visit("http://localhost:3000");
|
||||
cy.get(".ant-btn").contains("Sign In").click();
|
||||
cy.get("#email").type("fake@email.com");
|
||||
cy.get("#password").type("fakepassword");
|
||||
cy.get(".ant-btn").contains("Login").click();
|
||||
cy.should("have.text", "user");
|
||||
});
|
||||
|
||||
it("Attempt a failed login.", () => {});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"id": 8739,
|
||||
"name": "Jane",
|
||||
"email": "jane@example.com"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1,23 +0,0 @@
|
||||
/// <reference types="Cypress" />
|
||||
const { FIREBASE_USERNAME, FIREBASE_PASSWORcD } = Cypress.env();
|
||||
describe("Renders the General Page", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/");
|
||||
});
|
||||
it("Renders Correctly", () => {});
|
||||
it("Has the Slogan", () => {
|
||||
cy.findByText("A whole x22new kind of shop management system.").should(
|
||||
"exist"
|
||||
);
|
||||
/* ==== Generated with Cypress Studio ==== */
|
||||
cy.get(
|
||||
".ant-menu-item-active > .ant-menu-title-content > .header0-item-block"
|
||||
).click();
|
||||
cy.get("#email").clear();
|
||||
cy.get("#email").type("patrick@imex.dev");
|
||||
cy.get("#password").clear();
|
||||
cy.get("#password").type("patrick123{enter}");
|
||||
cy.get(".ant-form > .ant-btn").click();
|
||||
/* ==== End Cypress Studio ==== */
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
// Welcome to Cypress!
|
||||
//
|
||||
// This spec file contains a variety of sample tests
|
||||
// for a todo list app that are designed to demonstrate
|
||||
// the power of writing tests in Cypress.
|
||||
//
|
||||
// To learn more about how Cypress works and
|
||||
// what makes it such an awesome testing tool,
|
||||
// please read our getting started guide:
|
||||
// https://on.cypress.io/introduction-to-cypress
|
||||
|
||||
describe('example to-do app', () => {
|
||||
beforeEach(() => {
|
||||
// Cypress starts out with a blank slate for each test
|
||||
// so we must tell it to visit our website with the `cy.visit()` command.
|
||||
// Since we want to visit the same URL at the start of all our tests,
|
||||
// we include it in our beforeEach function so that it runs before each test
|
||||
cy.visit('https://example.cypress.io/todo')
|
||||
})
|
||||
|
||||
it('displays two todo items by default', () => {
|
||||
// We use the `cy.get()` command to get all elements that match the selector.
|
||||
// Then, we use `should` to assert that there are two matched items,
|
||||
// which are the two default items.
|
||||
cy.get('.todo-list li').should('have.length', 2)
|
||||
|
||||
// We can go even further and check that the default todos each contain
|
||||
// the correct text. We use the `first` and `last` functions
|
||||
// to get just the first and last matched elements individually,
|
||||
// and then perform an assertion with `should`.
|
||||
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
|
||||
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
|
||||
})
|
||||
|
||||
it('can add new todo items', () => {
|
||||
// We'll store our item text in a variable so we can reuse it
|
||||
const newItem = 'Feed the cat'
|
||||
|
||||
// Let's get the input element and use the `type` command to
|
||||
// input our new list item. After typing the content of our item,
|
||||
// we need to type the enter key as well in order to submit the input.
|
||||
// This input has a data-test attribute so we'll use that to select the
|
||||
// element in accordance with best practices:
|
||||
// https://on.cypress.io/selecting-elements
|
||||
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
|
||||
|
||||
// Now that we've typed our new item, let's check that it actually was added to the list.
|
||||
// Since it's the newest item, it should exist as the last element in the list.
|
||||
// In addition, with the two default items, we should have a total of 3 elements in the list.
|
||||
// Since assertions yield the element that was asserted on,
|
||||
// we can chain both of these assertions together into a single statement.
|
||||
cy.get('.todo-list li')
|
||||
.should('have.length', 3)
|
||||
.last()
|
||||
.should('have.text', newItem)
|
||||
})
|
||||
|
||||
it('can check off an item as completed', () => {
|
||||
// In addition to using the `get` command to get an element by selector,
|
||||
// we can also use the `contains` command to get an element by its contents.
|
||||
// However, this will yield the <label>, which is lowest-level element that contains the text.
|
||||
// In order to check the item, we'll find the <input> element for this <label>
|
||||
// by traversing up the dom to the parent element. From there, we can `find`
|
||||
// the child checkbox <input> element and use the `check` command to check it.
|
||||
cy.contains('Pay electric bill')
|
||||
.parent()
|
||||
.find('input[type=checkbox]')
|
||||
.check()
|
||||
|
||||
// Now that we've checked the button, we can go ahead and make sure
|
||||
// that the list element is now marked as completed.
|
||||
// Again we'll use `contains` to find the <label> element and then use the `parents` command
|
||||
// to traverse multiple levels up the dom until we find the corresponding <li> element.
|
||||
// Once we get that element, we can assert that it has the completed class.
|
||||
cy.contains('Pay electric bill')
|
||||
.parents('li')
|
||||
.should('have.class', 'completed')
|
||||
})
|
||||
|
||||
context('with a checked task', () => {
|
||||
beforeEach(() => {
|
||||
// We'll take the command we used above to check off an element
|
||||
// Since we want to perform multiple tests that start with checking
|
||||
// one element, we put it in the beforeEach hook
|
||||
// so that it runs at the start of every test.
|
||||
cy.contains('Pay electric bill')
|
||||
.parent()
|
||||
.find('input[type=checkbox]')
|
||||
.check()
|
||||
})
|
||||
|
||||
it('can filter for uncompleted tasks', () => {
|
||||
// We'll click on the "active" button in order to
|
||||
// display only incomplete items
|
||||
cy.contains('Active').click()
|
||||
|
||||
// After filtering, we can assert that there is only the one
|
||||
// incomplete item in the list.
|
||||
cy.get('.todo-list li')
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('have.text', 'Walk the dog')
|
||||
|
||||
// For good measure, let's also assert that the task we checked off
|
||||
// does not exist on the page.
|
||||
cy.contains('Pay electric bill').should('not.exist')
|
||||
})
|
||||
|
||||
it('can filter for completed tasks', () => {
|
||||
// We can perform similar steps as the test above to ensure
|
||||
// that only completed tasks are shown
|
||||
cy.contains('Completed').click()
|
||||
|
||||
cy.get('.todo-list li')
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('have.text', 'Pay electric bill')
|
||||
|
||||
cy.contains('Walk the dog').should('not.exist')
|
||||
})
|
||||
|
||||
it('can delete all completed tasks', () => {
|
||||
// First, let's click the "Clear completed" button
|
||||
// `contains` is actually serving two purposes here.
|
||||
// First, it's ensuring that the button exists within the dom.
|
||||
// This button only appears when at least one task is checked
|
||||
// so this command is implicitly verifying that it does exist.
|
||||
// Second, it selects the button so we can click it.
|
||||
cy.contains('Clear completed').click()
|
||||
|
||||
// Then we can make sure that there is only one element
|
||||
// in the list and our element does not exist
|
||||
cy.get('.todo-list li')
|
||||
.should('have.length', 1)
|
||||
.should('not.have.text', 'Pay electric bill')
|
||||
|
||||
// Finally, make sure that the clear button no longer exists.
|
||||
cy.contains('Clear completed').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,299 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Actions', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/actions')
|
||||
})
|
||||
|
||||
// https://on.cypress.io/interacting-with-elements
|
||||
|
||||
it('.type() - type into a DOM element', () => {
|
||||
// https://on.cypress.io/type
|
||||
cy.get('.action-email')
|
||||
.type('fake@email.com').should('have.value', 'fake@email.com')
|
||||
|
||||
// .type() with special character sequences
|
||||
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
|
||||
.type('{del}{selectall}{backspace}')
|
||||
|
||||
// .type() with key modifiers
|
||||
.type('{alt}{option}') //these are equivalent
|
||||
.type('{ctrl}{control}') //these are equivalent
|
||||
.type('{meta}{command}{cmd}') //these are equivalent
|
||||
.type('{shift}')
|
||||
|
||||
// Delay each keypress by 0.1 sec
|
||||
.type('slow.typing@email.com', { delay: 100 })
|
||||
.should('have.value', 'slow.typing@email.com')
|
||||
|
||||
cy.get('.action-disabled')
|
||||
// Ignore error checking prior to type
|
||||
// like whether the input is visible or disabled
|
||||
.type('disabled error checking', { force: true })
|
||||
.should('have.value', 'disabled error checking')
|
||||
})
|
||||
|
||||
it('.focus() - focus on a DOM element', () => {
|
||||
// https://on.cypress.io/focus
|
||||
cy.get('.action-focus').focus()
|
||||
.should('have.class', 'focus')
|
||||
.prev().should('have.attr', 'style', 'color: orange;')
|
||||
})
|
||||
|
||||
it('.blur() - blur off a DOM element', () => {
|
||||
// https://on.cypress.io/blur
|
||||
cy.get('.action-blur').type('About to blur').blur()
|
||||
.should('have.class', 'error')
|
||||
.prev().should('have.attr', 'style', 'color: red;')
|
||||
})
|
||||
|
||||
it('.clear() - clears an input or textarea element', () => {
|
||||
// https://on.cypress.io/clear
|
||||
cy.get('.action-clear').type('Clear this text')
|
||||
.should('have.value', 'Clear this text')
|
||||
.clear()
|
||||
.should('have.value', '')
|
||||
})
|
||||
|
||||
it('.submit() - submit a form', () => {
|
||||
// https://on.cypress.io/submit
|
||||
cy.get('.action-form')
|
||||
.find('[type="text"]').type('HALFOFF')
|
||||
|
||||
cy.get('.action-form').submit()
|
||||
.next().should('contain', 'Your form has been submitted!')
|
||||
})
|
||||
|
||||
it('.click() - click on a DOM element', () => {
|
||||
// https://on.cypress.io/click
|
||||
cy.get('.action-btn').click()
|
||||
|
||||
// You can click on 9 specific positions of an element:
|
||||
// -----------------------------------
|
||||
// | topLeft top topRight |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// | left center right |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// | bottomLeft bottom bottomRight |
|
||||
// -----------------------------------
|
||||
|
||||
// clicking in the center of the element is the default
|
||||
cy.get('#action-canvas').click()
|
||||
|
||||
cy.get('#action-canvas').click('topLeft')
|
||||
cy.get('#action-canvas').click('top')
|
||||
cy.get('#action-canvas').click('topRight')
|
||||
cy.get('#action-canvas').click('left')
|
||||
cy.get('#action-canvas').click('right')
|
||||
cy.get('#action-canvas').click('bottomLeft')
|
||||
cy.get('#action-canvas').click('bottom')
|
||||
cy.get('#action-canvas').click('bottomRight')
|
||||
|
||||
// .click() accepts an x and y coordinate
|
||||
// that controls where the click occurs :)
|
||||
|
||||
cy.get('#action-canvas')
|
||||
.click(80, 75) // click 80px on x coord and 75px on y coord
|
||||
.click(170, 75)
|
||||
.click(80, 165)
|
||||
.click(100, 185)
|
||||
.click(125, 190)
|
||||
.click(150, 185)
|
||||
.click(170, 165)
|
||||
|
||||
// click multiple elements by passing multiple: true
|
||||
cy.get('.action-labels>.label').click({ multiple: true })
|
||||
|
||||
// Ignore error checking prior to clicking
|
||||
cy.get('.action-opacity>.btn').click({ force: true })
|
||||
})
|
||||
|
||||
it('.dblclick() - double click on a DOM element', () => {
|
||||
// https://on.cypress.io/dblclick
|
||||
|
||||
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
||||
// that hides the div and shows an input on double click
|
||||
cy.get('.action-div').dblclick().should('not.be.visible')
|
||||
cy.get('.action-input-hidden').should('be.visible')
|
||||
})
|
||||
|
||||
it('.rightclick() - right click on a DOM element', () => {
|
||||
// https://on.cypress.io/rightclick
|
||||
|
||||
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
||||
// that hides the div and shows an input on right click
|
||||
cy.get('.rightclick-action-div').rightclick().should('not.be.visible')
|
||||
cy.get('.rightclick-action-input-hidden').should('be.visible')
|
||||
})
|
||||
|
||||
it('.check() - check a checkbox or radio element', () => {
|
||||
// https://on.cypress.io/check
|
||||
|
||||
// By default, .check() will check all
|
||||
// matching checkbox or radio elements in succession, one after another
|
||||
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
|
||||
.check().should('be.checked')
|
||||
|
||||
cy.get('.action-radios [type="radio"]').not('[disabled]')
|
||||
.check().should('be.checked')
|
||||
|
||||
// .check() accepts a value argument
|
||||
cy.get('.action-radios [type="radio"]')
|
||||
.check('radio1').should('be.checked')
|
||||
|
||||
// .check() accepts an array of values
|
||||
cy.get('.action-multiple-checkboxes [type="checkbox"]')
|
||||
.check(['checkbox1', 'checkbox2']).should('be.checked')
|
||||
|
||||
// Ignore error checking prior to checking
|
||||
cy.get('.action-checkboxes [disabled]')
|
||||
.check({ force: true }).should('be.checked')
|
||||
|
||||
cy.get('.action-radios [type="radio"]')
|
||||
.check('radio3', { force: true }).should('be.checked')
|
||||
})
|
||||
|
||||
it('.uncheck() - uncheck a checkbox element', () => {
|
||||
// https://on.cypress.io/uncheck
|
||||
|
||||
// By default, .uncheck() will uncheck all matching
|
||||
// checkbox elements in succession, one after another
|
||||
cy.get('.action-check [type="checkbox"]')
|
||||
.not('[disabled]')
|
||||
.uncheck().should('not.be.checked')
|
||||
|
||||
// .uncheck() accepts a value argument
|
||||
cy.get('.action-check [type="checkbox"]')
|
||||
.check('checkbox1')
|
||||
.uncheck('checkbox1').should('not.be.checked')
|
||||
|
||||
// .uncheck() accepts an array of values
|
||||
cy.get('.action-check [type="checkbox"]')
|
||||
.check(['checkbox1', 'checkbox3'])
|
||||
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
|
||||
|
||||
// Ignore error checking prior to unchecking
|
||||
cy.get('.action-check [disabled]')
|
||||
.uncheck({ force: true }).should('not.be.checked')
|
||||
})
|
||||
|
||||
it('.select() - select an option in a <select> element', () => {
|
||||
// https://on.cypress.io/select
|
||||
|
||||
// at first, no option should be selected
|
||||
cy.get('.action-select')
|
||||
.should('have.value', '--Select a fruit--')
|
||||
|
||||
// Select option(s) with matching text content
|
||||
cy.get('.action-select').select('apples')
|
||||
// confirm the apples were selected
|
||||
// note that each value starts with "fr-" in our HTML
|
||||
cy.get('.action-select').should('have.value', 'fr-apples')
|
||||
|
||||
cy.get('.action-select-multiple')
|
||||
.select(['apples', 'oranges', 'bananas'])
|
||||
// when getting multiple values, invoke "val" method first
|
||||
.invoke('val')
|
||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
||||
|
||||
// Select option(s) with matching value
|
||||
cy.get('.action-select').select('fr-bananas')
|
||||
// can attach an assertion right away to the element
|
||||
.should('have.value', 'fr-bananas')
|
||||
|
||||
cy.get('.action-select-multiple')
|
||||
.select(['fr-apples', 'fr-oranges', 'fr-bananas'])
|
||||
.invoke('val')
|
||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
||||
|
||||
// assert the selected values include oranges
|
||||
cy.get('.action-select-multiple')
|
||||
.invoke('val').should('include', 'fr-oranges')
|
||||
})
|
||||
|
||||
it('.scrollIntoView() - scroll an element into view', () => {
|
||||
// https://on.cypress.io/scrollintoview
|
||||
|
||||
// normally all of these buttons are hidden,
|
||||
// because they're not within
|
||||
// the viewable area of their parent
|
||||
// (we need to scroll to see them)
|
||||
cy.get('#scroll-horizontal button')
|
||||
.should('not.be.visible')
|
||||
|
||||
// scroll the button into view, as if the user had scrolled
|
||||
cy.get('#scroll-horizontal button').scrollIntoView()
|
||||
.should('be.visible')
|
||||
|
||||
cy.get('#scroll-vertical button')
|
||||
.should('not.be.visible')
|
||||
|
||||
// Cypress handles the scroll direction needed
|
||||
cy.get('#scroll-vertical button').scrollIntoView()
|
||||
.should('be.visible')
|
||||
|
||||
cy.get('#scroll-both button')
|
||||
.should('not.be.visible')
|
||||
|
||||
// Cypress knows to scroll to the right and down
|
||||
cy.get('#scroll-both button').scrollIntoView()
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('.trigger() - trigger an event on a DOM element', () => {
|
||||
// https://on.cypress.io/trigger
|
||||
|
||||
// To interact with a range input (slider)
|
||||
// we need to set its value & trigger the
|
||||
// event to signal it changed
|
||||
|
||||
// Here, we invoke jQuery's val() method to set
|
||||
// the value and trigger the 'change' event
|
||||
cy.get('.trigger-input-range')
|
||||
.invoke('val', 25)
|
||||
.trigger('change')
|
||||
.get('input[type=range]').siblings('p')
|
||||
.should('have.text', '25')
|
||||
})
|
||||
|
||||
it('cy.scrollTo() - scroll the window or element to a position', () => {
|
||||
// https://on.cypress.io/scrollto
|
||||
|
||||
// You can scroll to 9 specific positions of an element:
|
||||
// -----------------------------------
|
||||
// | topLeft top topRight |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// | left center right |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// | bottomLeft bottom bottomRight |
|
||||
// -----------------------------------
|
||||
|
||||
// if you chain .scrollTo() off of cy, we will
|
||||
// scroll the entire window
|
||||
cy.scrollTo('bottom')
|
||||
|
||||
cy.get('#scrollable-horizontal').scrollTo('right')
|
||||
|
||||
// or you can scroll to a specific coordinate:
|
||||
// (x axis, y axis) in pixels
|
||||
cy.get('#scrollable-vertical').scrollTo(250, 250)
|
||||
|
||||
// or you can scroll to a specific percentage
|
||||
// of the (width, height) of the element
|
||||
cy.get('#scrollable-both').scrollTo('75%', '25%')
|
||||
|
||||
// control the easing of the scroll (default is 'swing')
|
||||
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
|
||||
|
||||
// control the duration of the scroll (in ms)
|
||||
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
|
||||
})
|
||||
})
|
||||
@@ -1,39 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Aliasing', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/aliasing')
|
||||
})
|
||||
|
||||
it('.as() - alias a DOM element for later use', () => {
|
||||
// https://on.cypress.io/as
|
||||
|
||||
// Alias a DOM element for use later
|
||||
// We don't have to traverse to the element
|
||||
// later in our code, we reference it with @
|
||||
|
||||
cy.get('.as-table').find('tbody>tr')
|
||||
.first().find('td').first()
|
||||
.find('button').as('firstBtn')
|
||||
|
||||
// when we reference the alias, we place an
|
||||
// @ in front of its name
|
||||
cy.get('@firstBtn').click()
|
||||
|
||||
cy.get('@firstBtn')
|
||||
.should('have.class', 'btn-success')
|
||||
.and('contain', 'Changed')
|
||||
})
|
||||
|
||||
it('.as() - alias a route for later use', () => {
|
||||
// Alias the route to wait for its response
|
||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
||||
|
||||
// we have code that gets a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-btn').click()
|
||||
|
||||
// https://on.cypress.io/wait
|
||||
cy.wait('@getComment').its('response.statusCode').should('eq', 200)
|
||||
})
|
||||
})
|
||||
@@ -1,177 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Assertions', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/assertions')
|
||||
})
|
||||
|
||||
describe('Implicit Assertions', () => {
|
||||
it('.should() - make an assertion about the current subject', () => {
|
||||
// https://on.cypress.io/should
|
||||
cy.get('.assertion-table')
|
||||
.find('tbody tr:last')
|
||||
.should('have.class', 'success')
|
||||
.find('td')
|
||||
.first()
|
||||
// checking the text of the <td> element in various ways
|
||||
.should('have.text', 'Column content')
|
||||
.should('contain', 'Column content')
|
||||
.should('have.html', 'Column content')
|
||||
// chai-jquery uses "is()" to check if element matches selector
|
||||
.should('match', 'td')
|
||||
// to match text content against a regular expression
|
||||
// first need to invoke jQuery method text()
|
||||
// and then match using regular expression
|
||||
.invoke('text')
|
||||
.should('match', /column content/i)
|
||||
|
||||
// a better way to check element's text content against a regular expression
|
||||
// is to use "cy.contains"
|
||||
// https://on.cypress.io/contains
|
||||
cy.get('.assertion-table')
|
||||
.find('tbody tr:last')
|
||||
// finds first <td> element with text content matching regular expression
|
||||
.contains('td', /column content/i)
|
||||
.should('be.visible')
|
||||
|
||||
// for more information about asserting element's text
|
||||
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
||||
})
|
||||
|
||||
it('.and() - chain multiple assertions together', () => {
|
||||
// https://on.cypress.io/and
|
||||
cy.get('.assertions-link')
|
||||
.should('have.class', 'active')
|
||||
.and('have.attr', 'href')
|
||||
.and('include', 'cypress.io')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Explicit Assertions', () => {
|
||||
// https://on.cypress.io/assertions
|
||||
it('expect - make an assertion about a specified subject', () => {
|
||||
// We can use Chai's BDD style assertions
|
||||
expect(true).to.be.true
|
||||
const o = { foo: 'bar' }
|
||||
|
||||
expect(o).to.equal(o)
|
||||
expect(o).to.deep.equal({ foo: 'bar' })
|
||||
// matching text using regular expression
|
||||
expect('FooBar').to.match(/bar$/i)
|
||||
})
|
||||
|
||||
it('pass your own callback function to should()', () => {
|
||||
// Pass a function to should that can have any number
|
||||
// of explicit assertions within it.
|
||||
// The ".should(cb)" function will be retried
|
||||
// automatically until it passes all your explicit assertions or times out.
|
||||
cy.get('.assertions-p')
|
||||
.find('p')
|
||||
.should(($p) => {
|
||||
// https://on.cypress.io/$
|
||||
// return an array of texts from all of the p's
|
||||
// @ts-ignore TS6133 unused variable
|
||||
const texts = $p.map((i, el) => Cypress.$(el).text())
|
||||
|
||||
// jquery map returns jquery object
|
||||
// and .get() convert this to simple array
|
||||
const paragraphs = texts.get()
|
||||
|
||||
// array should have length of 3
|
||||
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
|
||||
|
||||
// use second argument to expect(...) to provide clear
|
||||
// message with each assertion
|
||||
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
|
||||
'Some text from first p',
|
||||
'More text from second p',
|
||||
'And even more text from third p',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('finds element by class name regex', () => {
|
||||
cy.get('.docs-header')
|
||||
.find('div')
|
||||
// .should(cb) callback function will be retried
|
||||
.should(($div) => {
|
||||
expect($div).to.have.length(1)
|
||||
|
||||
const className = $div[0].className
|
||||
|
||||
expect(className).to.match(/heading-/)
|
||||
})
|
||||
// .then(cb) callback is not retried,
|
||||
// it either passes or fails
|
||||
.then(($div) => {
|
||||
expect($div, 'text content').to.have.text('Introduction')
|
||||
})
|
||||
})
|
||||
|
||||
it('can throw any error', () => {
|
||||
cy.get('.docs-header')
|
||||
.find('div')
|
||||
.should(($div) => {
|
||||
if ($div.length !== 1) {
|
||||
// you can throw your own errors
|
||||
throw new Error('Did not find 1 element')
|
||||
}
|
||||
|
||||
const className = $div[0].className
|
||||
|
||||
if (!className.match(/heading-/)) {
|
||||
throw new Error(`Could not find class "heading-" in ${className}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('matches unknown text between two elements', () => {
|
||||
/**
|
||||
* Text from the first element.
|
||||
* @type {string}
|
||||
*/
|
||||
let text
|
||||
|
||||
/**
|
||||
* Normalizes passed text,
|
||||
* useful before comparing text with spaces and different capitalization.
|
||||
* @param {string} s Text to normalize
|
||||
*/
|
||||
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
|
||||
|
||||
cy.get('.two-elements')
|
||||
.find('.first')
|
||||
.then(($first) => {
|
||||
// save text from the first element
|
||||
text = normalizeText($first.text())
|
||||
})
|
||||
|
||||
cy.get('.two-elements')
|
||||
.find('.second')
|
||||
.should(($div) => {
|
||||
// we can massage text before comparing
|
||||
const secondText = normalizeText($div.text())
|
||||
|
||||
expect(secondText, 'second text').to.equal(text)
|
||||
})
|
||||
})
|
||||
|
||||
it('assert - assert shape of an object', () => {
|
||||
const person = {
|
||||
name: 'Joe',
|
||||
age: 20,
|
||||
}
|
||||
|
||||
assert.isObject(person, 'value is object')
|
||||
})
|
||||
|
||||
it('retries the should callback until assertions pass', () => {
|
||||
cy.get('#random-number')
|
||||
.should(($div) => {
|
||||
const n = parseFloat($div.text())
|
||||
|
||||
expect(n).to.be.gte(1).and.be.lte(10)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,97 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Connectors', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/connectors')
|
||||
})
|
||||
|
||||
it('.each() - iterate over an array of elements', () => {
|
||||
// https://on.cypress.io/each
|
||||
cy.get('.connectors-each-ul>li')
|
||||
.each(($el, index, $list) => {
|
||||
console.log($el, index, $list)
|
||||
})
|
||||
})
|
||||
|
||||
it('.its() - get properties on the current subject', () => {
|
||||
// https://on.cypress.io/its
|
||||
cy.get('.connectors-its-ul>li')
|
||||
// calls the 'length' property yielding that value
|
||||
.its('length')
|
||||
.should('be.gt', 2)
|
||||
})
|
||||
|
||||
it('.invoke() - invoke a function on the current subject', () => {
|
||||
// our div is hidden in our script.js
|
||||
// $('.connectors-div').hide()
|
||||
|
||||
// https://on.cypress.io/invoke
|
||||
cy.get('.connectors-div').should('be.hidden')
|
||||
// call the jquery method 'show' on the 'div.container'
|
||||
.invoke('show')
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('.spread() - spread an array as individual args to callback function', () => {
|
||||
// https://on.cypress.io/spread
|
||||
const arr = ['foo', 'bar', 'baz']
|
||||
|
||||
cy.wrap(arr).spread((foo, bar, baz) => {
|
||||
expect(foo).to.eq('foo')
|
||||
expect(bar).to.eq('bar')
|
||||
expect(baz).to.eq('baz')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.then()', () => {
|
||||
it('invokes a callback function with the current subject', () => {
|
||||
// https://on.cypress.io/then
|
||||
cy.get('.connectors-list > li')
|
||||
.then(($lis) => {
|
||||
expect($lis, '3 items').to.have.length(3)
|
||||
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
|
||||
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
|
||||
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
|
||||
})
|
||||
})
|
||||
|
||||
it('yields the returned value to the next command', () => {
|
||||
cy.wrap(1)
|
||||
.then((num) => {
|
||||
expect(num).to.equal(1)
|
||||
|
||||
return 2
|
||||
})
|
||||
.then((num) => {
|
||||
expect(num).to.equal(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('yields the original subject without return', () => {
|
||||
cy.wrap(1)
|
||||
.then((num) => {
|
||||
expect(num).to.equal(1)
|
||||
// note that nothing is returned from this callback
|
||||
})
|
||||
.then((num) => {
|
||||
// this callback receives the original unchanged value 1
|
||||
expect(num).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('yields the value yielded by the last Cypress command inside', () => {
|
||||
cy.wrap(1)
|
||||
.then((num) => {
|
||||
expect(num).to.equal(1)
|
||||
// note how we run a Cypress command
|
||||
// the result yielded by this Cypress command
|
||||
// will be passed to the second ".then"
|
||||
cy.wrap(2)
|
||||
})
|
||||
.then((num) => {
|
||||
// this callback receives the value yielded by "cy.wrap(2)"
|
||||
expect(num).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,77 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Cookies', () => {
|
||||
beforeEach(() => {
|
||||
Cypress.Cookies.debug(true)
|
||||
|
||||
cy.visit('https://example.cypress.io/commands/cookies')
|
||||
|
||||
// clear cookies again after visiting to remove
|
||||
// any 3rd party cookies picked up such as cloudflare
|
||||
cy.clearCookies()
|
||||
})
|
||||
|
||||
it('cy.getCookie() - get a browser cookie', () => {
|
||||
// https://on.cypress.io/getcookie
|
||||
cy.get('#getCookie .set-a-cookie').click()
|
||||
|
||||
// cy.getCookie() yields a cookie object
|
||||
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
||||
})
|
||||
|
||||
it('cy.getCookies() - get browser cookies', () => {
|
||||
// https://on.cypress.io/getcookies
|
||||
cy.getCookies().should('be.empty')
|
||||
|
||||
cy.get('#getCookies .set-a-cookie').click()
|
||||
|
||||
// cy.getCookies() yields an array of cookies
|
||||
cy.getCookies().should('have.length', 1).should((cookies) => {
|
||||
// each cookie has these properties
|
||||
expect(cookies[0]).to.have.property('name', 'token')
|
||||
expect(cookies[0]).to.have.property('value', '123ABC')
|
||||
expect(cookies[0]).to.have.property('httpOnly', false)
|
||||
expect(cookies[0]).to.have.property('secure', false)
|
||||
expect(cookies[0]).to.have.property('domain')
|
||||
expect(cookies[0]).to.have.property('path')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.setCookie() - set a browser cookie', () => {
|
||||
// https://on.cypress.io/setcookie
|
||||
cy.getCookies().should('be.empty')
|
||||
|
||||
cy.setCookie('foo', 'bar')
|
||||
|
||||
// cy.getCookie() yields a cookie object
|
||||
cy.getCookie('foo').should('have.property', 'value', 'bar')
|
||||
})
|
||||
|
||||
it('cy.clearCookie() - clear a browser cookie', () => {
|
||||
// https://on.cypress.io/clearcookie
|
||||
cy.getCookie('token').should('be.null')
|
||||
|
||||
cy.get('#clearCookie .set-a-cookie').click()
|
||||
|
||||
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
||||
|
||||
// cy.clearCookies() yields null
|
||||
cy.clearCookie('token').should('be.null')
|
||||
|
||||
cy.getCookie('token').should('be.null')
|
||||
})
|
||||
|
||||
it('cy.clearCookies() - clear browser cookies', () => {
|
||||
// https://on.cypress.io/clearcookies
|
||||
cy.getCookies().should('be.empty')
|
||||
|
||||
cy.get('#clearCookies .set-a-cookie').click()
|
||||
|
||||
cy.getCookies().should('have.length', 1)
|
||||
|
||||
// cy.clearCookies() yields null
|
||||
cy.clearCookies()
|
||||
|
||||
cy.getCookies().should('be.empty')
|
||||
})
|
||||
})
|
||||
@@ -1,202 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Cypress.Commands', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
// https://on.cypress.io/custom-commands
|
||||
|
||||
it('.add() - create a custom command', () => {
|
||||
Cypress.Commands.add('console', {
|
||||
prevSubject: true,
|
||||
}, (subject, method) => {
|
||||
// the previous subject is automatically received
|
||||
// and the commands arguments are shifted
|
||||
|
||||
// allow us to change the console method used
|
||||
method = method || 'log'
|
||||
|
||||
// log the subject to the console
|
||||
// @ts-ignore TS7017
|
||||
console[method]('The subject is', subject)
|
||||
|
||||
// whatever we return becomes the new subject
|
||||
// we don't want to change the subject so
|
||||
// we return whatever was passed in
|
||||
return subject
|
||||
})
|
||||
|
||||
// @ts-ignore TS2339
|
||||
cy.get('button').console('info').then(($button) => {
|
||||
// subject is still $button
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.Cookies', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
// https://on.cypress.io/cookies
|
||||
it('.debug() - enable or disable debugging', () => {
|
||||
Cypress.Cookies.debug(true)
|
||||
|
||||
// Cypress will now log in the console when
|
||||
// cookies are set or cleared
|
||||
cy.setCookie('fakeCookie', '123ABC')
|
||||
cy.clearCookie('fakeCookie')
|
||||
cy.setCookie('fakeCookie', '123ABC')
|
||||
cy.clearCookie('fakeCookie')
|
||||
cy.setCookie('fakeCookie', '123ABC')
|
||||
})
|
||||
|
||||
it('.preserveOnce() - preserve cookies by key', () => {
|
||||
// normally cookies are reset after each test
|
||||
cy.getCookie('fakeCookie').should('not.be.ok')
|
||||
|
||||
// preserving a cookie will not clear it when
|
||||
// the next test starts
|
||||
cy.setCookie('lastCookie', '789XYZ')
|
||||
Cypress.Cookies.preserveOnce('lastCookie')
|
||||
})
|
||||
|
||||
it('.defaults() - set defaults for all cookies', () => {
|
||||
// now any cookie with the name 'session_id' will
|
||||
// not be cleared before each new test runs
|
||||
Cypress.Cookies.defaults({
|
||||
preserve: 'session_id',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.arch', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get CPU architecture name of underlying OS', () => {
|
||||
// https://on.cypress.io/arch
|
||||
expect(Cypress.arch).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.config()', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get and set configuration options', () => {
|
||||
// https://on.cypress.io/config
|
||||
let myConfig = Cypress.config()
|
||||
|
||||
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
|
||||
expect(myConfig).to.have.property('baseUrl', null)
|
||||
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
|
||||
expect(myConfig).to.have.property('requestTimeout', 5000)
|
||||
expect(myConfig).to.have.property('responseTimeout', 30000)
|
||||
expect(myConfig).to.have.property('viewportHeight', 660)
|
||||
expect(myConfig).to.have.property('viewportWidth', 1000)
|
||||
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
|
||||
expect(myConfig).to.have.property('waitForAnimations', true)
|
||||
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
|
||||
|
||||
// this will change the config for the rest of your tests!
|
||||
Cypress.config('pageLoadTimeout', 20000)
|
||||
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
|
||||
|
||||
Cypress.config('pageLoadTimeout', 60000)
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.dom', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
// https://on.cypress.io/dom
|
||||
it('.isHidden() - determine if a DOM element is hidden', () => {
|
||||
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
|
||||
let visibleP = Cypress.$('.dom-p p.visible').get(0)
|
||||
|
||||
// our first paragraph has css class 'hidden'
|
||||
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
|
||||
expect(Cypress.dom.isHidden(visibleP)).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.env()', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
// We can set environment variables for highly dynamic values
|
||||
|
||||
// https://on.cypress.io/environment-variables
|
||||
it('Get environment variables', () => {
|
||||
// https://on.cypress.io/env
|
||||
// set multiple environment variables
|
||||
Cypress.env({
|
||||
host: 'veronica.dev.local',
|
||||
api_server: 'http://localhost:8888/v1/',
|
||||
})
|
||||
|
||||
// get environment variable
|
||||
expect(Cypress.env('host')).to.eq('veronica.dev.local')
|
||||
|
||||
// set environment variable
|
||||
Cypress.env('api_server', 'http://localhost:8888/v2/')
|
||||
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
|
||||
|
||||
// get all environment variable
|
||||
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
|
||||
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.log', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Control what is printed to the Command Log', () => {
|
||||
// https://on.cypress.io/cypress-log
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.platform', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get underlying OS name', () => {
|
||||
// https://on.cypress.io/platform
|
||||
expect(Cypress.platform).to.be.exist
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.version', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get current version of Cypress being run', () => {
|
||||
// https://on.cypress.io/version
|
||||
expect(Cypress.version).to.be.exist
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.spec', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get current spec information', () => {
|
||||
// https://on.cypress.io/spec
|
||||
// wrap the object so we can inspect it easily by clicking in the command log
|
||||
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute'])
|
||||
})
|
||||
})
|
||||
@@ -1,88 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
/// JSON fixture file can be loaded directly using
|
||||
// the built-in JavaScript bundler
|
||||
// @ts-ignore
|
||||
const requiredExample = require('../../fixtures/example')
|
||||
|
||||
context('Files', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/files')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// load example.json fixture file and store
|
||||
// in the test context object
|
||||
cy.fixture('example.json').as('example')
|
||||
})
|
||||
|
||||
it('cy.fixture() - load a fixture', () => {
|
||||
// https://on.cypress.io/fixture
|
||||
|
||||
// Instead of writing a response inline you can
|
||||
// use a fixture file's content.
|
||||
|
||||
// when application makes an Ajax request matching "GET **/comments/*"
|
||||
// Cypress will intercept it and reply with the object in `example.json` fixture
|
||||
cy.intercept('GET', '**/comments/*', { fixture: 'example.json' }).as('getComment')
|
||||
|
||||
// we have code that gets a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.fixture-btn').click()
|
||||
|
||||
cy.wait('@getComment').its('response.body')
|
||||
.should('have.property', 'name')
|
||||
.and('include', 'Using fixtures to represent data')
|
||||
})
|
||||
|
||||
it('cy.fixture() or require - load a fixture', function () {
|
||||
// we are inside the "function () { ... }"
|
||||
// callback and can use test context object "this"
|
||||
// "this.example" was loaded in "beforeEach" function callback
|
||||
expect(this.example, 'fixture in the test context')
|
||||
.to.deep.equal(requiredExample)
|
||||
|
||||
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
||||
cy.wrap(this.example)
|
||||
.should('deep.equal', requiredExample)
|
||||
})
|
||||
|
||||
it('cy.readFile() - read file contents', () => {
|
||||
// https://on.cypress.io/readfile
|
||||
|
||||
// You can read a file and yield its contents
|
||||
// The filePath is relative to your project's root.
|
||||
cy.readFile('cypress.json').then((json) => {
|
||||
expect(json).to.be.an('object')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.writeFile() - write to a file', () => {
|
||||
// https://on.cypress.io/writefile
|
||||
|
||||
// You can write to a file
|
||||
|
||||
// Use a response from a request to automatically
|
||||
// generate a fixture file for use later
|
||||
cy.request('https://jsonplaceholder.cypress.io/users')
|
||||
.then((response) => {
|
||||
cy.writeFile('cypress/fixtures/users.json', response.body)
|
||||
})
|
||||
|
||||
cy.fixture('users').should((users) => {
|
||||
expect(users[0].name).to.exist
|
||||
})
|
||||
|
||||
// JavaScript arrays and objects are stringified
|
||||
// and formatted into text.
|
||||
cy.writeFile('cypress/fixtures/profile.json', {
|
||||
id: 8739,
|
||||
name: 'Jane',
|
||||
email: 'jane@example.com',
|
||||
})
|
||||
|
||||
cy.fixture('profile').should((profile) => {
|
||||
expect(profile.name).to.eq('Jane')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Local Storage', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/local-storage')
|
||||
})
|
||||
// Although local storage is automatically cleared
|
||||
// in between tests to maintain a clean state
|
||||
// sometimes we need to clear the local storage manually
|
||||
|
||||
it('cy.clearLocalStorage() - clear all data in local storage', () => {
|
||||
// https://on.cypress.io/clearlocalstorage
|
||||
cy.get('.ls-btn').click().should(() => {
|
||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
|
||||
// clearLocalStorage() yields the localStorage object
|
||||
cy.clearLocalStorage().should((ls) => {
|
||||
expect(ls.getItem('prop1')).to.be.null
|
||||
expect(ls.getItem('prop2')).to.be.null
|
||||
expect(ls.getItem('prop3')).to.be.null
|
||||
})
|
||||
|
||||
cy.get('.ls-btn').click().should(() => {
|
||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
|
||||
// Clear key matching string in Local Storage
|
||||
cy.clearLocalStorage('prop1').should((ls) => {
|
||||
expect(ls.getItem('prop1')).to.be.null
|
||||
expect(ls.getItem('prop2')).to.eq('blue')
|
||||
expect(ls.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
|
||||
cy.get('.ls-btn').click().should(() => {
|
||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
|
||||
// Clear keys matching regex in Local Storage
|
||||
cy.clearLocalStorage(/prop1|2/).should((ls) => {
|
||||
expect(ls.getItem('prop1')).to.be.null
|
||||
expect(ls.getItem('prop2')).to.be.null
|
||||
expect(ls.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Location', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/location')
|
||||
})
|
||||
|
||||
it('cy.hash() - get the current URL hash', () => {
|
||||
// https://on.cypress.io/hash
|
||||
cy.hash().should('be.empty')
|
||||
})
|
||||
|
||||
it('cy.location() - get window.location', () => {
|
||||
// https://on.cypress.io/location
|
||||
cy.location().should((location) => {
|
||||
expect(location.hash).to.be.empty
|
||||
expect(location.href).to.eq('https://example.cypress.io/commands/location')
|
||||
expect(location.host).to.eq('example.cypress.io')
|
||||
expect(location.hostname).to.eq('example.cypress.io')
|
||||
expect(location.origin).to.eq('https://example.cypress.io')
|
||||
expect(location.pathname).to.eq('/commands/location')
|
||||
expect(location.port).to.eq('')
|
||||
expect(location.protocol).to.eq('https:')
|
||||
expect(location.search).to.be.empty
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.url() - get the current URL', () => {
|
||||
// https://on.cypress.io/url
|
||||
cy.url().should('eq', 'https://example.cypress.io/commands/location')
|
||||
})
|
||||
})
|
||||
@@ -1,104 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Misc', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/misc')
|
||||
})
|
||||
|
||||
it('.end() - end the command chain', () => {
|
||||
// https://on.cypress.io/end
|
||||
|
||||
// cy.end is useful when you want to end a chain of commands
|
||||
// and force Cypress to re-query from the root element
|
||||
cy.get('.misc-table').within(() => {
|
||||
// ends the current chain and yields null
|
||||
cy.contains('Cheryl').click().end()
|
||||
|
||||
// queries the entire table again
|
||||
cy.contains('Charles').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.exec() - execute a system command', () => {
|
||||
// execute a system command.
|
||||
// so you can take actions necessary for
|
||||
// your test outside the scope of Cypress.
|
||||
// https://on.cypress.io/exec
|
||||
|
||||
// we can use Cypress.platform string to
|
||||
// select appropriate command
|
||||
// https://on.cypress/io/platform
|
||||
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
|
||||
|
||||
// on CircleCI Windows build machines we have a failure to run bash shell
|
||||
// https://github.com/cypress-io/cypress/issues/5169
|
||||
// so skip some of the tests by passing flag "--env circle=true"
|
||||
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
|
||||
|
||||
if (isCircleOnWindows) {
|
||||
cy.log('Skipping test on CircleCI')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// cy.exec problem on Shippable CI
|
||||
// https://github.com/cypress-io/cypress/issues/6718
|
||||
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
|
||||
|
||||
if (isShippable) {
|
||||
cy.log('Skipping test on ShippableCI')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cy.exec('echo Jane Lane')
|
||||
.its('stdout').should('contain', 'Jane Lane')
|
||||
|
||||
if (Cypress.platform === 'win32') {
|
||||
cy.exec('print cypress.json')
|
||||
.its('stderr').should('be.empty')
|
||||
} else {
|
||||
cy.exec('cat cypress.json')
|
||||
.its('stderr').should('be.empty')
|
||||
|
||||
cy.exec('pwd')
|
||||
.its('code').should('eq', 0)
|
||||
}
|
||||
})
|
||||
|
||||
it('cy.focused() - get the DOM element that has focus', () => {
|
||||
// https://on.cypress.io/focused
|
||||
cy.get('.misc-form').find('#name').click()
|
||||
cy.focused().should('have.id', 'name')
|
||||
|
||||
cy.get('.misc-form').find('#description').click()
|
||||
cy.focused().should('have.id', 'description')
|
||||
})
|
||||
|
||||
context('Cypress.Screenshot', function () {
|
||||
it('cy.screenshot() - take a screenshot', () => {
|
||||
// https://on.cypress.io/screenshot
|
||||
cy.screenshot('my-image')
|
||||
})
|
||||
|
||||
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
|
||||
Cypress.Screenshot.defaults({
|
||||
blackout: ['.foo'],
|
||||
capture: 'viewport',
|
||||
clip: { x: 0, y: 0, width: 200, height: 200 },
|
||||
scale: false,
|
||||
disableTimersAndAnimations: true,
|
||||
screenshotOnRunFailure: true,
|
||||
onBeforeScreenshot () { },
|
||||
onAfterScreenshot () { },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.wrap() - wrap an object', () => {
|
||||
// https://on.cypress.io/wrap
|
||||
cy.wrap({ foo: 'bar' })
|
||||
.should('have.property', 'foo')
|
||||
.and('include', 'bar')
|
||||
})
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Navigation', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
cy.get('.navbar-nav').contains('Commands').click()
|
||||
cy.get('.dropdown-menu').contains('Navigation').click()
|
||||
})
|
||||
|
||||
it('cy.go() - go back or forward in the browser\'s history', () => {
|
||||
// https://on.cypress.io/go
|
||||
|
||||
cy.location('pathname').should('include', 'navigation')
|
||||
|
||||
cy.go('back')
|
||||
cy.location('pathname').should('not.include', 'navigation')
|
||||
|
||||
cy.go('forward')
|
||||
cy.location('pathname').should('include', 'navigation')
|
||||
|
||||
// clicking back
|
||||
cy.go(-1)
|
||||
cy.location('pathname').should('not.include', 'navigation')
|
||||
|
||||
// clicking forward
|
||||
cy.go(1)
|
||||
cy.location('pathname').should('include', 'navigation')
|
||||
})
|
||||
|
||||
it('cy.reload() - reload the page', () => {
|
||||
// https://on.cypress.io/reload
|
||||
cy.reload()
|
||||
|
||||
// reload the page without using the cache
|
||||
cy.reload(true)
|
||||
})
|
||||
|
||||
it('cy.visit() - visit a remote url', () => {
|
||||
// https://on.cypress.io/visit
|
||||
|
||||
// Visit any sub-domain of your current domain
|
||||
|
||||
// Pass options to the visit
|
||||
cy.visit('https://example.cypress.io/commands/navigation', {
|
||||
timeout: 50000, // increase total time for the visit to resolve
|
||||
onBeforeLoad (contentWindow) {
|
||||
// contentWindow is the remote page's window object
|
||||
expect(typeof contentWindow === 'object').to.be.true
|
||||
},
|
||||
onLoad (contentWindow) {
|
||||
// contentWindow is the remote page's window object
|
||||
expect(typeof contentWindow === 'object').to.be.true
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,163 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Network Requests', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/network-requests')
|
||||
})
|
||||
|
||||
// Manage HTTP requests in your app
|
||||
|
||||
it('cy.request() - make an XHR request', () => {
|
||||
// https://on.cypress.io/request
|
||||
cy.request('https://jsonplaceholder.cypress.io/comments')
|
||||
.should((response) => {
|
||||
expect(response.status).to.eq(200)
|
||||
// the server sometimes gets an extra comment posted from another machine
|
||||
// which gets returned as 1 extra object
|
||||
expect(response.body).to.have.property('length').and.be.oneOf([500, 501])
|
||||
expect(response).to.have.property('headers')
|
||||
expect(response).to.have.property('duration')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.request() - verify response using BDD syntax', () => {
|
||||
cy.request('https://jsonplaceholder.cypress.io/comments')
|
||||
.then((response) => {
|
||||
// https://on.cypress.io/assertions
|
||||
expect(response).property('status').to.equal(200)
|
||||
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501])
|
||||
expect(response).to.include.keys('headers', 'duration')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.request() with query parameters', () => {
|
||||
// will execute request
|
||||
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
||||
cy.request({
|
||||
url: 'https://jsonplaceholder.cypress.io/comments',
|
||||
qs: {
|
||||
postId: 1,
|
||||
id: 3,
|
||||
},
|
||||
})
|
||||
.its('body')
|
||||
.should('be.an', 'array')
|
||||
.and('have.length', 1)
|
||||
.its('0') // yields first element of the array
|
||||
.should('contain', {
|
||||
postId: 1,
|
||||
id: 3,
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.request() - pass result to the second request', () => {
|
||||
// first, let's find out the userId of the first user we have
|
||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
||||
.its('body') // yields the response object
|
||||
.its('0') // yields the first element of the returned list
|
||||
// the above two commands its('body').its('0')
|
||||
// can be written as its('body.0')
|
||||
// if you do not care about TypeScript checks
|
||||
.then((user) => {
|
||||
expect(user).property('id').to.be.a('number')
|
||||
// make a new post on behalf of the user
|
||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
||||
userId: user.id,
|
||||
title: 'Cypress Test Runner',
|
||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
||||
})
|
||||
})
|
||||
// note that the value here is the returned value of the 2nd request
|
||||
// which is the new post object
|
||||
.then((response) => {
|
||||
expect(response).property('status').to.equal(201) // new entity created
|
||||
expect(response).property('body').to.contain({
|
||||
title: 'Cypress Test Runner',
|
||||
})
|
||||
|
||||
// we don't know the exact post id - only that it will be > 100
|
||||
// since JSONPlaceholder has built-in 100 posts
|
||||
expect(response.body).property('id').to.be.a('number')
|
||||
.and.to.be.gt(100)
|
||||
|
||||
// we don't know the user id here - since it was in above closure
|
||||
// so in this test just confirm that the property is there
|
||||
expect(response.body).property('userId').to.be.a('number')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.request() - save response in the shared test context', () => {
|
||||
// https://on.cypress.io/variables-and-aliases
|
||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
||||
.its('body').its('0') // yields the first element of the returned list
|
||||
.as('user') // saves the object in the test context
|
||||
.then(function () {
|
||||
// NOTE 👀
|
||||
// By the time this callback runs the "as('user')" command
|
||||
// has saved the user object in the test context.
|
||||
// To access the test context we need to use
|
||||
// the "function () { ... }" callback form,
|
||||
// otherwise "this" points at a wrong or undefined object!
|
||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
||||
userId: this.user.id,
|
||||
title: 'Cypress Test Runner',
|
||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
||||
})
|
||||
.its('body').as('post') // save the new post from the response
|
||||
})
|
||||
.then(function () {
|
||||
// When this callback runs, both "cy.request" API commands have finished
|
||||
// and the test context has "user" and "post" objects set.
|
||||
// Let's verify them.
|
||||
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.intercept() - route responses to matching requests', () => {
|
||||
// https://on.cypress.io/intercept
|
||||
|
||||
let message = 'whoa, this comment does not exist'
|
||||
|
||||
// Listen to GET to comments/1
|
||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
||||
|
||||
// we have code that gets a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-btn').click()
|
||||
|
||||
// https://on.cypress.io/wait
|
||||
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||
|
||||
// Listen to POST to comments
|
||||
cy.intercept('POST', '**/comments').as('postComment')
|
||||
|
||||
// we have code that posts a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-post').click()
|
||||
cy.wait('@postComment').should(({ request, response }) => {
|
||||
expect(request.body).to.include('email')
|
||||
expect(request.headers).to.have.property('content-type')
|
||||
expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()')
|
||||
})
|
||||
|
||||
// Stub a response to PUT comments/ ****
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '**/comments/*',
|
||||
}, {
|
||||
statusCode: 404,
|
||||
body: { error: message },
|
||||
headers: { 'access-control-allow-origin': '*' },
|
||||
delayMs: 500,
|
||||
}).as('putComment')
|
||||
|
||||
// we have code that puts a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-put').click()
|
||||
|
||||
cy.wait('@putComment')
|
||||
|
||||
// our 404 statusCode logic in scripts.js executed
|
||||
cy.get('.network-put-comment').should('contain', message)
|
||||
})
|
||||
})
|
||||
@@ -1,114 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Querying', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/querying')
|
||||
})
|
||||
|
||||
// The most commonly used query is 'cy.get()', you can
|
||||
// think of this like the '$' in jQuery
|
||||
|
||||
it('cy.get() - query DOM elements', () => {
|
||||
// https://on.cypress.io/get
|
||||
|
||||
cy.get('#query-btn').should('contain', 'Button')
|
||||
|
||||
cy.get('.query-btn').should('contain', 'Button')
|
||||
|
||||
cy.get('#querying .well>button:first').should('contain', 'Button')
|
||||
// ↲
|
||||
// Use CSS selectors just like jQuery
|
||||
|
||||
cy.get('[data-test-id="test-example"]').should('have.class', 'example')
|
||||
|
||||
// 'cy.get()' yields jQuery object, you can get its attribute
|
||||
// by invoking `.attr()` method
|
||||
cy.get('[data-test-id="test-example"]')
|
||||
.invoke('attr', 'data-test-id')
|
||||
.should('equal', 'test-example')
|
||||
|
||||
// or you can get element's CSS property
|
||||
cy.get('[data-test-id="test-example"]')
|
||||
.invoke('css', 'position')
|
||||
.should('equal', 'static')
|
||||
|
||||
// or use assertions directly during 'cy.get()'
|
||||
// https://on.cypress.io/assertions
|
||||
cy.get('[data-test-id="test-example"]')
|
||||
.should('have.attr', 'data-test-id', 'test-example')
|
||||
.and('have.css', 'position', 'static')
|
||||
})
|
||||
|
||||
it('cy.contains() - query DOM elements with matching content', () => {
|
||||
// https://on.cypress.io/contains
|
||||
cy.get('.query-list')
|
||||
.contains('bananas')
|
||||
.should('have.class', 'third')
|
||||
|
||||
// we can pass a regexp to `.contains()`
|
||||
cy.get('.query-list')
|
||||
.contains(/^b\w+/)
|
||||
.should('have.class', 'third')
|
||||
|
||||
cy.get('.query-list')
|
||||
.contains('apples')
|
||||
.should('have.class', 'first')
|
||||
|
||||
// passing a selector to contains will
|
||||
// yield the selector containing the text
|
||||
cy.get('#querying')
|
||||
.contains('ul', 'oranges')
|
||||
.should('have.class', 'query-list')
|
||||
|
||||
cy.get('.query-button')
|
||||
.contains('Save Form')
|
||||
.should('have.class', 'btn')
|
||||
})
|
||||
|
||||
it('.within() - query DOM elements within a specific element', () => {
|
||||
// https://on.cypress.io/within
|
||||
cy.get('.query-form').within(() => {
|
||||
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
|
||||
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.root() - query the root DOM element', () => {
|
||||
// https://on.cypress.io/root
|
||||
|
||||
// By default, root is the document
|
||||
cy.root().should('match', 'html')
|
||||
|
||||
cy.get('.query-ul').within(() => {
|
||||
// In this within, the root is now the ul DOM element
|
||||
cy.root().should('have.class', 'query-ul')
|
||||
})
|
||||
})
|
||||
|
||||
it('best practices - selecting elements', () => {
|
||||
// https://on.cypress.io/best-practices#Selecting-Elements
|
||||
cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
|
||||
// Worst - too generic, no context
|
||||
cy.get('button').click()
|
||||
|
||||
// Bad. Coupled to styling. Highly subject to change.
|
||||
cy.get('.btn.btn-large').click()
|
||||
|
||||
// Average. Coupled to the `name` attribute which has HTML semantics.
|
||||
cy.get('[name=submission]').click()
|
||||
|
||||
// Better. But still coupled to styling or JS event listeners.
|
||||
cy.get('#main').click()
|
||||
|
||||
// Slightly better. Uses an ID but also ensures the element
|
||||
// has an ARIA role attribute
|
||||
cy.get('#main[role=button]').click()
|
||||
|
||||
// Much better. But still coupled to text content that may change.
|
||||
cy.contains('Submit').click()
|
||||
|
||||
// Best. Insulated from all changes.
|
||||
cy.get('[data-cy=submit]').click()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,205 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// remove no check once Cypress.sinon is typed
|
||||
// https://github.com/cypress-io/cypress/issues/6720
|
||||
|
||||
context('Spies, Stubs, and Clock', () => {
|
||||
it('cy.spy() - wrap a method in a spy', () => {
|
||||
// https://on.cypress.io/spy
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
|
||||
const obj = {
|
||||
foo () {},
|
||||
}
|
||||
|
||||
const spy = cy.spy(obj, 'foo').as('anyArgs')
|
||||
|
||||
obj.foo()
|
||||
|
||||
expect(spy).to.be.called
|
||||
})
|
||||
|
||||
it('cy.spy() retries until assertions pass', () => {
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
|
||||
const obj = {
|
||||
/**
|
||||
* Prints the argument passed
|
||||
* @param x {any}
|
||||
*/
|
||||
foo (x) {
|
||||
console.log('obj.foo called with', x)
|
||||
},
|
||||
}
|
||||
|
||||
cy.spy(obj, 'foo').as('foo')
|
||||
|
||||
setTimeout(() => {
|
||||
obj.foo('first')
|
||||
}, 500)
|
||||
|
||||
setTimeout(() => {
|
||||
obj.foo('second')
|
||||
}, 2500)
|
||||
|
||||
cy.get('@foo').should('have.been.calledTwice')
|
||||
})
|
||||
|
||||
it('cy.stub() - create a stub and/or replace a function with stub', () => {
|
||||
// https://on.cypress.io/stub
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
|
||||
const obj = {
|
||||
/**
|
||||
* prints both arguments to the console
|
||||
* @param a {string}
|
||||
* @param b {string}
|
||||
*/
|
||||
foo (a, b) {
|
||||
console.log('a', a, 'b', b)
|
||||
},
|
||||
}
|
||||
|
||||
const stub = cy.stub(obj, 'foo').as('foo')
|
||||
|
||||
obj.foo('foo', 'bar')
|
||||
|
||||
expect(stub).to.be.called
|
||||
})
|
||||
|
||||
it('cy.clock() - control time in the browser', () => {
|
||||
// https://on.cypress.io/clock
|
||||
|
||||
// create the date in UTC so its always the same
|
||||
// no matter what local timezone the browser is running in
|
||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
||||
|
||||
cy.clock(now)
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
cy.get('#clock-div').click()
|
||||
.should('have.text', '1489449600')
|
||||
})
|
||||
|
||||
it('cy.tick() - move time in the browser', () => {
|
||||
// https://on.cypress.io/tick
|
||||
|
||||
// create the date in UTC so its always the same
|
||||
// no matter what local timezone the browser is running in
|
||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
||||
|
||||
cy.clock(now)
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
cy.get('#tick-div').click()
|
||||
.should('have.text', '1489449600')
|
||||
|
||||
cy.tick(10000) // 10 seconds passed
|
||||
cy.get('#tick-div').click()
|
||||
.should('have.text', '1489449610')
|
||||
})
|
||||
|
||||
it('cy.stub() matches depending on arguments', () => {
|
||||
// see all possible matchers at
|
||||
// https://sinonjs.org/releases/latest/matchers/
|
||||
const greeter = {
|
||||
/**
|
||||
* Greets a person
|
||||
* @param {string} name
|
||||
*/
|
||||
greet (name) {
|
||||
return `Hello, ${name}!`
|
||||
},
|
||||
}
|
||||
|
||||
cy.stub(greeter, 'greet')
|
||||
.callThrough() // if you want non-matched calls to call the real method
|
||||
.withArgs(Cypress.sinon.match.string).returns('Hi')
|
||||
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name'))
|
||||
|
||||
expect(greeter.greet('World')).to.equal('Hi')
|
||||
// @ts-ignore
|
||||
expect(() => greeter.greet(42)).to.throw('Invalid name')
|
||||
expect(greeter.greet).to.have.been.calledTwice
|
||||
|
||||
// non-matched calls goes the actual method
|
||||
// @ts-ignore
|
||||
expect(greeter.greet()).to.equal('Hello, undefined!')
|
||||
})
|
||||
|
||||
it('matches call arguments using Sinon matchers', () => {
|
||||
// see all possible matchers at
|
||||
// https://sinonjs.org/releases/latest/matchers/
|
||||
const calculator = {
|
||||
/**
|
||||
* returns the sum of two arguments
|
||||
* @param a {number}
|
||||
* @param b {number}
|
||||
*/
|
||||
add (a, b) {
|
||||
return a + b
|
||||
},
|
||||
}
|
||||
|
||||
const spy = cy.spy(calculator, 'add').as('add')
|
||||
|
||||
expect(calculator.add(2, 3)).to.equal(5)
|
||||
|
||||
// if we want to assert the exact values used during the call
|
||||
expect(spy).to.be.calledWith(2, 3)
|
||||
|
||||
// let's confirm "add" method was called with two numbers
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number)
|
||||
|
||||
// alternatively, provide the value to match
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3))
|
||||
|
||||
// match any value
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
|
||||
|
||||
// match any value from a list
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
|
||||
|
||||
/**
|
||||
* Returns true if the given number is event
|
||||
* @param {number} x
|
||||
*/
|
||||
const isEven = (x) => x % 2 === 0
|
||||
|
||||
// expect the value to pass a custom predicate function
|
||||
// the second argument to "sinon.match(predicate, message)" is
|
||||
// shown if the predicate does not pass and assertion fails
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3)
|
||||
|
||||
/**
|
||||
* Returns a function that checks if a given number is larger than the limit
|
||||
* @param {number} limit
|
||||
* @returns {(x: number) => boolean}
|
||||
*/
|
||||
const isGreaterThan = (limit) => (x) => x > limit
|
||||
|
||||
/**
|
||||
* Returns a function that checks if a given number is less than the limit
|
||||
* @param {number} limit
|
||||
* @returns {(x: number) => boolean}
|
||||
*/
|
||||
const isLessThan = (limit) => (x) => x < limit
|
||||
|
||||
// you can combine several matchers using "and", "or"
|
||||
expect(spy).to.be.calledWith(
|
||||
Cypress.sinon.match.number,
|
||||
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')),
|
||||
)
|
||||
|
||||
expect(spy).to.be.calledWith(
|
||||
Cypress.sinon.match.number,
|
||||
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)),
|
||||
)
|
||||
|
||||
// matchers can be used from BDD assertions
|
||||
cy.get('@add').should('have.been.calledWith',
|
||||
Cypress.sinon.match.number, Cypress.sinon.match(3))
|
||||
|
||||
// you can alias matchers for shorter test code
|
||||
const { match: M } = Cypress.sinon
|
||||
|
||||
cy.get('@add').should('have.been.calledWith', M.number, M(3))
|
||||
})
|
||||
})
|
||||
@@ -1,121 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Traversal', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/traversal')
|
||||
})
|
||||
|
||||
it('.children() - get child DOM elements', () => {
|
||||
// https://on.cypress.io/children
|
||||
cy.get('.traversal-breadcrumb')
|
||||
.children('.active')
|
||||
.should('contain', 'Data')
|
||||
})
|
||||
|
||||
it('.closest() - get closest ancestor DOM element', () => {
|
||||
// https://on.cypress.io/closest
|
||||
cy.get('.traversal-badge')
|
||||
.closest('ul')
|
||||
.should('have.class', 'list-group')
|
||||
})
|
||||
|
||||
it('.eq() - get a DOM element at a specific index', () => {
|
||||
// https://on.cypress.io/eq
|
||||
cy.get('.traversal-list>li')
|
||||
.eq(1).should('contain', 'siamese')
|
||||
})
|
||||
|
||||
it('.filter() - get DOM elements that match the selector', () => {
|
||||
// https://on.cypress.io/filter
|
||||
cy.get('.traversal-nav>li')
|
||||
.filter('.active').should('contain', 'About')
|
||||
})
|
||||
|
||||
it('.find() - get descendant DOM elements of the selector', () => {
|
||||
// https://on.cypress.io/find
|
||||
cy.get('.traversal-pagination')
|
||||
.find('li').find('a')
|
||||
.should('have.length', 7)
|
||||
})
|
||||
|
||||
it('.first() - get first DOM element', () => {
|
||||
// https://on.cypress.io/first
|
||||
cy.get('.traversal-table td')
|
||||
.first().should('contain', '1')
|
||||
})
|
||||
|
||||
it('.last() - get last DOM element', () => {
|
||||
// https://on.cypress.io/last
|
||||
cy.get('.traversal-buttons .btn')
|
||||
.last().should('contain', 'Submit')
|
||||
})
|
||||
|
||||
it('.next() - get next sibling DOM element', () => {
|
||||
// https://on.cypress.io/next
|
||||
cy.get('.traversal-ul')
|
||||
.contains('apples').next().should('contain', 'oranges')
|
||||
})
|
||||
|
||||
it('.nextAll() - get all next sibling DOM elements', () => {
|
||||
// https://on.cypress.io/nextall
|
||||
cy.get('.traversal-next-all')
|
||||
.contains('oranges')
|
||||
.nextAll().should('have.length', 3)
|
||||
})
|
||||
|
||||
it('.nextUntil() - get next sibling DOM elements until next el', () => {
|
||||
// https://on.cypress.io/nextuntil
|
||||
cy.get('#veggies')
|
||||
.nextUntil('#nuts').should('have.length', 3)
|
||||
})
|
||||
|
||||
it('.not() - remove DOM elements from set of DOM elements', () => {
|
||||
// https://on.cypress.io/not
|
||||
cy.get('.traversal-disabled .btn')
|
||||
.not('[disabled]').should('not.contain', 'Disabled')
|
||||
})
|
||||
|
||||
it('.parent() - get parent DOM element from DOM elements', () => {
|
||||
// https://on.cypress.io/parent
|
||||
cy.get('.traversal-mark')
|
||||
.parent().should('contain', 'Morbi leo risus')
|
||||
})
|
||||
|
||||
it('.parents() - get parent DOM elements from DOM elements', () => {
|
||||
// https://on.cypress.io/parents
|
||||
cy.get('.traversal-cite')
|
||||
.parents().should('match', 'blockquote')
|
||||
})
|
||||
|
||||
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
|
||||
// https://on.cypress.io/parentsuntil
|
||||
cy.get('.clothes-nav')
|
||||
.find('.active')
|
||||
.parentsUntil('.clothes-nav')
|
||||
.should('have.length', 2)
|
||||
})
|
||||
|
||||
it('.prev() - get previous sibling DOM element', () => {
|
||||
// https://on.cypress.io/prev
|
||||
cy.get('.birds').find('.active')
|
||||
.prev().should('contain', 'Lorikeets')
|
||||
})
|
||||
|
||||
it('.prevAll() - get all previous sibling DOM elements', () => {
|
||||
// https://on.cypress.io/prevall
|
||||
cy.get('.fruits-list').find('.third')
|
||||
.prevAll().should('have.length', 2)
|
||||
})
|
||||
|
||||
it('.prevUntil() - get all previous sibling DOM elements until el', () => {
|
||||
// https://on.cypress.io/prevuntil
|
||||
cy.get('.foods-list').find('#nuts')
|
||||
.prevUntil('#veggies').should('have.length', 3)
|
||||
})
|
||||
|
||||
it('.siblings() - get all sibling DOM elements', () => {
|
||||
// https://on.cypress.io/siblings
|
||||
cy.get('.traversal-pills .active')
|
||||
.siblings().should('have.length', 2)
|
||||
})
|
||||
})
|
||||
@@ -1,110 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Utilities', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/utilities')
|
||||
})
|
||||
|
||||
it('Cypress._ - call a lodash method', () => {
|
||||
// https://on.cypress.io/_
|
||||
cy.request('https://jsonplaceholder.cypress.io/users')
|
||||
.then((response) => {
|
||||
let ids = Cypress._.chain(response.body).map('id').take(3).value()
|
||||
|
||||
expect(ids).to.deep.eq([1, 2, 3])
|
||||
})
|
||||
})
|
||||
|
||||
it('Cypress.$ - call a jQuery method', () => {
|
||||
// https://on.cypress.io/$
|
||||
let $li = Cypress.$('.utility-jquery li:first')
|
||||
|
||||
cy.wrap($li)
|
||||
.should('not.have.class', 'active')
|
||||
.click()
|
||||
.should('have.class', 'active')
|
||||
})
|
||||
|
||||
it('Cypress.Blob - blob utilities and base64 string conversion', () => {
|
||||
// https://on.cypress.io/blob
|
||||
cy.get('.utility-blob').then(($div) => {
|
||||
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
||||
// get the dataUrl string for the javascript-logo
|
||||
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
|
||||
.then((dataUrl) => {
|
||||
// create an <img> element and set its src to the dataUrl
|
||||
let img = Cypress.$('<img />', { src: dataUrl })
|
||||
|
||||
// need to explicitly return cy here since we are initially returning
|
||||
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
||||
// append the image
|
||||
$div.append(img)
|
||||
|
||||
cy.get('.utility-blob img').click()
|
||||
.should('have.attr', 'src', dataUrl)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Cypress.minimatch - test out glob patterns against strings', () => {
|
||||
// https://on.cypress.io/minimatch
|
||||
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', {
|
||||
matchBase: true,
|
||||
})
|
||||
|
||||
expect(matching, 'matching wildcard').to.be.true
|
||||
|
||||
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
|
||||
matchBase: true,
|
||||
})
|
||||
|
||||
expect(matching, 'comments').to.be.false
|
||||
|
||||
// ** matches against all downstream path segments
|
||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', {
|
||||
matchBase: true,
|
||||
})
|
||||
|
||||
expect(matching, 'comments').to.be.true
|
||||
|
||||
// whereas * matches only the next path segment
|
||||
|
||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
|
||||
matchBase: false,
|
||||
})
|
||||
|
||||
expect(matching, 'comments').to.be.false
|
||||
})
|
||||
|
||||
it('Cypress.Promise - instantiate a bluebird promise', () => {
|
||||
// https://on.cypress.io/promise
|
||||
let waited = false
|
||||
|
||||
/**
|
||||
* @return Bluebird<string>
|
||||
*/
|
||||
function waitOneSecond () {
|
||||
// return a promise that resolves after 1 second
|
||||
// @ts-ignore TS2351 (new Cypress.Promise)
|
||||
return new Cypress.Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
// set waited to true
|
||||
waited = true
|
||||
|
||||
// resolve with 'foo' string
|
||||
resolve('foo')
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
cy.then(() => {
|
||||
// return a promise to cy.then() that
|
||||
// is awaited until it resolves
|
||||
// @ts-ignore TS7006
|
||||
return waitOneSecond().then((str) => {
|
||||
expect(str).to.eq('foo')
|
||||
expect(waited).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,59 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Viewport', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/viewport')
|
||||
})
|
||||
|
||||
it('cy.viewport() - set the viewport size and dimension', () => {
|
||||
// https://on.cypress.io/viewport
|
||||
|
||||
cy.get('#navbar').should('be.visible')
|
||||
cy.viewport(320, 480)
|
||||
|
||||
// the navbar should have collapse since our screen is smaller
|
||||
cy.get('#navbar').should('not.be.visible')
|
||||
cy.get('.navbar-toggle').should('be.visible').click()
|
||||
cy.get('.nav').find('a').should('be.visible')
|
||||
|
||||
// lets see what our app looks like on a super large screen
|
||||
cy.viewport(2999, 2999)
|
||||
|
||||
// cy.viewport() accepts a set of preset sizes
|
||||
// to easily set the screen to a device's width and height
|
||||
|
||||
// We added a cy.wait() between each viewport change so you can see
|
||||
// the change otherwise it is a little too fast to see :)
|
||||
|
||||
cy.viewport('macbook-15')
|
||||
cy.wait(200)
|
||||
cy.viewport('macbook-13')
|
||||
cy.wait(200)
|
||||
cy.viewport('macbook-11')
|
||||
cy.wait(200)
|
||||
cy.viewport('ipad-2')
|
||||
cy.wait(200)
|
||||
cy.viewport('ipad-mini')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-6+')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-6')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-5')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-4')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-3')
|
||||
cy.wait(200)
|
||||
|
||||
// cy.viewport() accepts an orientation for all presets
|
||||
// the default orientation is 'portrait'
|
||||
cy.viewport('ipad-2', 'portrait')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-4', 'landscape')
|
||||
cy.wait(200)
|
||||
|
||||
// The viewport will be reset back to the default dimensions
|
||||
// in between tests (the default can be set in cypress.json)
|
||||
})
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Waiting', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/waiting')
|
||||
})
|
||||
// BE CAREFUL of adding unnecessary wait times.
|
||||
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
||||
|
||||
// https://on.cypress.io/wait
|
||||
it('cy.wait() - wait for a specific amount of time', () => {
|
||||
cy.get('.wait-input1').type('Wait 1000ms after typing')
|
||||
cy.wait(1000)
|
||||
cy.get('.wait-input2').type('Wait 1000ms after typing')
|
||||
cy.wait(1000)
|
||||
cy.get('.wait-input3').type('Wait 1000ms after typing')
|
||||
cy.wait(1000)
|
||||
})
|
||||
|
||||
it('cy.wait() - wait for a specific route', () => {
|
||||
// Listen to GET to comments/1
|
||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
||||
|
||||
// we have code that gets a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-btn').click()
|
||||
|
||||
// wait for GET comments/1
|
||||
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||
})
|
||||
})
|
||||
@@ -1,22 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Window', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/window')
|
||||
})
|
||||
|
||||
it('cy.window() - get the global window object', () => {
|
||||
// https://on.cypress.io/window
|
||||
cy.window().should('have.property', 'top')
|
||||
})
|
||||
|
||||
it('cy.document() - get the document object', () => {
|
||||
// https://on.cypress.io/document
|
||||
cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
|
||||
})
|
||||
|
||||
it('cy.title() - get the title', () => {
|
||||
// https://on.cypress.io/title
|
||||
cy.title().should('include', 'Kitchen Sink')
|
||||
})
|
||||
})
|
||||
@@ -1,22 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
@@ -22,6 +22,4 @@
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
import "@testing-library/cypress/add-commands";
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
@@ -1,5 +1,5 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// This example support/e2e.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
@@ -17,4 +17,4 @@
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
// require('./commands')
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": ["**/*.*"]
|
||||
}
|
||||
@@ -4,55 +4,60 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/pro-layout": "^7.6.1",
|
||||
"@apollo/client": "^3.6.9",
|
||||
"@asseinfo/react-kanban": "^2.2.0",
|
||||
"@craco/craco": "^7.0.0",
|
||||
"@craco/craco": "^6.4.5",
|
||||
"@fingerprintjs/fingerprintjs": "^3.3.3",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@sentry/react": "^7.28.1",
|
||||
"@sentry/tracing": "^7.28.1",
|
||||
"@sentry/react": "^7.7.0",
|
||||
"@sentry/tracing": "^7.7.0",
|
||||
"@splitsoftware/splitio-react": "^1.6.0",
|
||||
"@stripe/react-stripe-js": "^1.9.0",
|
||||
"@stripe/stripe-js": "^1.32.0",
|
||||
"@tanem/react-nprogress": "^5.0.8",
|
||||
"antd": "5.1.5",
|
||||
"antd": "^4.22.3",
|
||||
"apollo-link-logger": "^2.0.0",
|
||||
"axios": "^1.2.3",
|
||||
"axios": "^0.27.2",
|
||||
"craco-less": "^1.20.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"enquire-js": "^0.2.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^9.9.1",
|
||||
"graphql": "^16.5.0",
|
||||
"i18next": "^22.4.6",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next": "^21.8.14",
|
||||
"i18next-browser-languagedetector": "^6.1.4",
|
||||
"jsoneditor": "^9.9.0",
|
||||
"jsreport-browser-client-dist": "^1.3.0",
|
||||
"libphonenumber-js": "^1.10.9",
|
||||
"logrocket": "^3.0.1",
|
||||
"markerjs2": "^2.22.0",
|
||||
"moment-business-days": "^1.2.0",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"normalize-url": "^8.0.0",
|
||||
"phone": "^3.1.32",
|
||||
"normalize-url": "^7.0.3",
|
||||
"phone": "^3.1.23",
|
||||
"preval.macro": "^5.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^8.1.0",
|
||||
"query-string": "^7.1.1",
|
||||
"rc-queue-anim": "^2.0.0",
|
||||
"rc-scroll-anim": "^2.7.6",
|
||||
"react": "^18.2.0",
|
||||
"react-big-calendar": "^1.5.2",
|
||||
"react": "^17.0.2",
|
||||
"react-big-calendar": "^1.5.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-drag-listview": "^0.2.1",
|
||||
"react-grid-gallery": "^0.5.5",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-i18next": "^12.1.1",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-number-format": "^5.1.2",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-i18next": "^11.18.1",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-number-format": "^4.9.3",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-resizable": "^3.0.4",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-sublime-video": "^0.2.5",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"recharts": "^2.1.12",
|
||||
"redux": "^4.2.0",
|
||||
@@ -64,7 +69,7 @@
|
||||
"socket.io-client": "^4.5.1",
|
||||
"styled-components": "^5.3.5",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"web-vitals": "^3.1.0",
|
||||
"web-vitals": "^2.1.4",
|
||||
"workbox-background-sync": "^6.5.3",
|
||||
"workbox-broadcast-update": "^6.5.3",
|
||||
"workbox-cacheable-response": "^6.5.3",
|
||||
@@ -81,14 +86,12 @@
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "craco start",
|
||||
"startrs": "react-scripts start",
|
||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
||||
"build:test": "env-cmd -f .env.test yarn run build",
|
||||
"build-deploy:test": "yarn run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
|
||||
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
||||
"test": "cypress open",
|
||||
"eject": "react-scripts eject",
|
||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -113,9 +116,8 @@
|
||||
"react-error-overlay": "6.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/webpack-plugin": "^1.20.0",
|
||||
"@testing-library/cypress": "^9.0.0",
|
||||
"cypress": "^12.3.0",
|
||||
"@sentry/webpack-plugin": "^1.19.0",
|
||||
"cypress": "^12.2.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"react-error-overlay": "6.0.11",
|
||||
"redux-logger": "^3.0.6",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -147,9 +147,4 @@
|
||||
//Update row highlighting on production board.
|
||||
.ant-table-tbody > tr.ant-table-row:hover > td {
|
||||
background: #eaeaea !important;
|
||||
}
|
||||
|
||||
.job-line-manual{
|
||||
color: tomato;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import React from "react";
|
||||
import {
|
||||
PaymentRequestButtonElement,
|
||||
useStripe,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
@@ -15,6 +19,49 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
function Test({ bodyshop, setEmailOptions }) {
|
||||
const stripe = useStripe();
|
||||
|
||||
const [paymentRequest, setPaymentRequest] = useState(null);
|
||||
useEffect(() => {
|
||||
if (stripe) {
|
||||
const pr = stripe.paymentRequest({
|
||||
country: "CA",
|
||||
displayItems: [{ label: "Deductible", amount: 1099 }],
|
||||
currency: "cad",
|
||||
total: {
|
||||
label: "Demo total",
|
||||
amount: 1099,
|
||||
},
|
||||
requestPayerName: true,
|
||||
requestPayerEmail: true,
|
||||
});
|
||||
|
||||
// Check the availability of the Payment Request API.
|
||||
pr.canMakePayment().then((result) => {
|
||||
if (result) {
|
||||
setPaymentRequest(pr);
|
||||
} else {
|
||||
// var details = {
|
||||
// total: { label: "", amount: { currency: "CAD", value: "0.00" } },
|
||||
// };
|
||||
new PaymentRequest(
|
||||
[{ supportedMethods: ["basic-card"] }],
|
||||
{}
|
||||
// details
|
||||
).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [stripe]);
|
||||
|
||||
if (paymentRequest) {
|
||||
return (
|
||||
<div style={{ height: "300px" }}>
|
||||
<PaymentRequestButtonElement options={{ paymentRequest }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
|
||||
@@ -107,6 +107,11 @@ export function AccountingPayablesTableComponent({
|
||||
dataIndex: "transactionid",
|
||||
key: "transactionid",
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.stripeid"),
|
||||
dataIndex: "stripeid",
|
||||
key: "stripeid",
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
|
||||
@@ -61,7 +61,7 @@ export function AllocationsAssignmentComponent({
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popContent} open={visibility}>
|
||||
<Popover content={popContent} visible={visibility}>
|
||||
<Button onClick={() => setVisibility(true)}>
|
||||
{t("allocations.actions.assign")}
|
||||
</Button>
|
||||
|
||||
@@ -59,7 +59,7 @@ export default connect(
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popContent} open={visibility}>
|
||||
<Popover content={popContent} visible={visibility}>
|
||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||
{t("allocations.actions.assign")}
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { Button, Form, Popconfirm, Space } from "antd";
|
||||
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
|
||||
import moment from "moment";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
@@ -179,7 +178,7 @@ export function BillDetailEditcontainer({
|
||||
<BillDetailEditReturn data={data} />
|
||||
|
||||
<Popconfirm
|
||||
open={visible}
|
||||
visible={visible}
|
||||
onConfirm={() => form.submit()}
|
||||
onCancel={() => setVisible(false)}
|
||||
okButtonProps={{ loading: updateLoading }}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function BillDetailEditReturn({
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={visible}
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(false)}
|
||||
destroyOnClose
|
||||
title={t("bills.actions.return")}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function BillDetailEditcontainer() {
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
destroyOnClose
|
||||
open={search.billid}
|
||||
visible={search.billid}
|
||||
>
|
||||
<BillDetailEditComponent />
|
||||
</Drawer>
|
||||
|
||||
@@ -344,7 +344,7 @@ function BillEnterModalContainer({
|
||||
<Modal
|
||||
title={t("bills.labels.new")}
|
||||
width={"98%"}
|
||||
open={billEnterModal.visible}
|
||||
visible={billEnterModal.visible}
|
||||
okText={t("general.actions.save")}
|
||||
keyboard="false"
|
||||
onOk={() => form.submit()}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function ContractsFindModalContainer({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
visible={visible}
|
||||
width="70%"
|
||||
title={t("payments.labels.findermodal")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
content={popContent}
|
||||
open={visibility}
|
||||
visible={visibility}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||
|
||||
@@ -88,8 +88,8 @@ export function ChatMediaSelector({
|
||||
}
|
||||
title={t("messaging.labels.selectmedia")}
|
||||
trigger="click"
|
||||
open={visible}
|
||||
onOpenChange={handleVisibleChange}
|
||||
visible={visible}
|
||||
onVisibleChange={handleVisibleChange}
|
||||
>
|
||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||
<PictureFilled style={{ margin: "0 .5rem" }} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -16,15 +16,19 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
|
||||
const menu = bodyshop.md_messaging_presets.map((i, idx) => ({
|
||||
label: i.label,
|
||||
key: idx,
|
||||
onClick: () => setMessage(i.text),
|
||||
}));
|
||||
const menu = (
|
||||
<Menu>
|
||||
{bodyshop.md_messaging_presets.map((i, idx) => (
|
||||
<Menu.Item onClick={() => setMessage(i.text)} key={idx}>
|
||||
{i.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown trigger={["click"]} menu={{ items: menu }} placement="top">
|
||||
<Dropdown trigger={["click"]} overlay={menu}>
|
||||
<PlusCircleOutlined />
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -390,7 +390,7 @@ export function ContractConvertToRo({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popover content={popContent} open={visible}>
|
||||
<Popover content={popContent} visible={visible}>
|
||||
<Button
|
||||
onClick={() => setVisible(true)}
|
||||
loading={loading}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function ContractLicenseDecodeButton({ form }) {
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
open={modalVisible}
|
||||
visible={modalVisible}
|
||||
okText={t("contracts.actions.senddltoform")}
|
||||
onOk={handleInsertForm}
|
||||
okButtonProps={{ disabled: !!!decodedBarcode }}
|
||||
|
||||
@@ -59,7 +59,7 @@ export function ContractsFindModalContainer({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
visible={visible}
|
||||
width="70%"
|
||||
title={t("contracts.labels.findermodal")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -13,14 +13,25 @@ const mapStateToProps = createStructuredSelector({
|
||||
export function ContractsRatesChangeButton({ disabled, form, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const menu = bodyshop.md_ccc_rates.map((rate, idx) => ({
|
||||
onClick: () => form.setFieldsValue(rate),
|
||||
key: idx,
|
||||
label: rate.label,
|
||||
}));
|
||||
const handleClick = ({ item, key, keyPath }) => {
|
||||
const { label, ...rate } = item.props.value;
|
||||
form.setFieldsValue(rate);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<div>
|
||||
<Menu onClick={handleClick}>
|
||||
{bodyshop.md_ccc_rates.map((rate, idx) => (
|
||||
<Menu.Item value={rate} key={idx}>
|
||||
{rate.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: menu }} disabled={disabled}>
|
||||
<Dropdown overlay={menu} disabled={disabled}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href=" #"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { WarningFilled } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { Button, Form, Input, InputNumber, Space } from "antd";
|
||||
import { Button, Form, Input, InputNumber, PageHeader, Space } from "antd";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -64,7 +64,7 @@ export function CCReturnModalContainer({
|
||||
return (
|
||||
<Modal
|
||||
title={t("courtesycars.labels.return")}
|
||||
open={visible}
|
||||
visible={visible}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
width={"90%"}
|
||||
okText={t("general.actions.save")}
|
||||
|
||||
@@ -3,19 +3,21 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
Input, Space,
|
||||
Input,
|
||||
Menu,
|
||||
Space,
|
||||
Table,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import moment from "moment";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
@@ -177,14 +179,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: t(
|
||||
"printcenter.courtesycarcontract.courtesy_car_inventory"
|
||||
),
|
||||
key: "cc_inv",
|
||||
onClick: () =>
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
onClick={() =>
|
||||
GenerateDocument(
|
||||
{
|
||||
name: TemplateList("courtesycar").courtesy_car_inventory
|
||||
@@ -195,10 +193,13 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
},
|
||||
{},
|
||||
"p"
|
||||
),
|
||||
},
|
||||
],
|
||||
}}
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("printcenter.courtesycarcontract.courtesy_car_inventory")}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button>{t("general.labels.print")}</Button>
|
||||
</Dropdown>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { Button, Dropdown, Menu, notification, Space } from "antd";
|
||||
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
|
||||
import i18next from "i18next";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
@@ -118,15 +117,17 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
);
|
||||
const existingLayoutKeys = state.items.map((i) => i.i);
|
||||
const addComponentOverlay = (
|
||||
<Menu
|
||||
onClick={handleAddComponent}
|
||||
items={Object.keys(componentList).map((key) => ({
|
||||
key: key,
|
||||
value: key,
|
||||
disabled: existingLayoutKeys.includes(key),
|
||||
label: componentList[key].label,
|
||||
}))}
|
||||
></Menu>
|
||||
<Menu onClick={handleAddComponent}>
|
||||
{Object.keys(componentList).map((key) => (
|
||||
<Menu.Item
|
||||
key={key}
|
||||
value={key}
|
||||
disabled={existingLayoutKeys.includes(key)}
|
||||
>
|
||||
{componentList[key].label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
@@ -282,12 +283,8 @@ const createDashboardQuery = (state) => {
|
||||
monthly_sales: jobs(where: {_and: [
|
||||
{ voided: {_eq: false}},
|
||||
{date_invoiced: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.startOf("day")
|
||||
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}]}) {
|
||||
.startOf("month").startOf('day').toISOString()}"}}, {date_invoiced: {_lte: "${moment()
|
||||
.endOf("month").endOf('day').toISOString()}"}}]}) {
|
||||
id
|
||||
ro_number
|
||||
date_invoiced
|
||||
|
||||
@@ -51,7 +51,7 @@ export function DmsCdkVehicles({ bodyshop, form, socket, job }) {
|
||||
<>
|
||||
<Modal
|
||||
width={"90%"}
|
||||
open={visible}
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(false)}
|
||||
onOk={() => {
|
||||
form.setFieldsValue({
|
||||
|
||||
@@ -6,10 +6,12 @@ import {
|
||||
Dropdown,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber, Select,
|
||||
InputNumber,
|
||||
Menu,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Typography
|
||||
Typography,
|
||||
} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import moment from "moment";
|
||||
@@ -275,32 +277,36 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
<div>
|
||||
{t("jobs.fields.dms.payer.controlnumber")}{" "}
|
||||
<Dropdown
|
||||
menu={{
|
||||
items:
|
||||
bodyshop.cdk_configuration.controllist &&
|
||||
bodyshop.cdk_configuration.controllist.map(
|
||||
(key, idx) => ({
|
||||
key: idx,
|
||||
label: key.name,
|
||||
onClick: () => {
|
||||
form.setFieldsValue({
|
||||
payers: form
|
||||
.getFieldValue("payers")
|
||||
.map((row, mapIndex) => {
|
||||
if (index !== mapIndex)
|
||||
return row;
|
||||
overlay={
|
||||
<Menu>
|
||||
{bodyshop.cdk_configuration.controllist &&
|
||||
bodyshop.cdk_configuration.controllist.map(
|
||||
(key, idx) => (
|
||||
<Menu.Item
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
form.setFieldsValue({
|
||||
payers: form
|
||||
.getFieldValue("payers")
|
||||
.map((row, mapIndex) => {
|
||||
if (index !== mapIndex)
|
||||
return row;
|
||||
|
||||
return {
|
||||
...row,
|
||||
controlnumber:
|
||||
key.controlnumber,
|
||||
};
|
||||
}),
|
||||
});
|
||||
},
|
||||
})
|
||||
),
|
||||
}}
|
||||
return {
|
||||
...row,
|
||||
controlnumber:
|
||||
key.controlnumber,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{key.name}
|
||||
</Menu.Item>
|
||||
)
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<a href=" #" onClick={(e) => e.preventDefault()}>
|
||||
<DownOutlined />
|
||||
|
||||
@@ -57,23 +57,20 @@ export function EmailOverlayComponent({
|
||||
|
||||
const menu = (
|
||||
<div>
|
||||
<Menu
|
||||
onClick={handleClick}
|
||||
items={[
|
||||
...bodyshop.employees
|
||||
.filter((e) => e.user_email)
|
||||
.map((e, idx) => ({
|
||||
value: e.user_email,
|
||||
key: idx,
|
||||
label: `${e.first_name} ${e.last_name}`,
|
||||
})),
|
||||
...bodyshop.md_to_emails.map((e, idx) => ({
|
||||
value: e.emails,
|
||||
key: idx + "group",
|
||||
label: e.label,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
<Menu onClick={handleClick}>
|
||||
{bodyshop.employees
|
||||
.filter((e) => e.user_email)
|
||||
.map((e, idx) => (
|
||||
<Menu.Item value={e.user_email} key={idx}>
|
||||
{`${e.first_name} ${e.last_name}`}
|
||||
</Menu.Item>
|
||||
))}
|
||||
{bodyshop.md_to_emails.map((e, idx) => (
|
||||
<Menu.Item value={e.emails} key={idx + "group"}>
|
||||
{e.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ export function EmailOverlayContainer({
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose={true}
|
||||
open={modalVisible}
|
||||
visible={modalVisible}
|
||||
maskClosable={false}
|
||||
width={"80%"}
|
||||
onOk={() => form.submit()}
|
||||
|
||||
@@ -94,7 +94,7 @@ const FormInputNUmberCalculator = (
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popover content={popContent} open={history.length > 0}>
|
||||
<Popover content={popContent} visible={history.length > 0}>
|
||||
<InputNumber
|
||||
ref={ref}
|
||||
value={value}
|
||||
|
||||
@@ -106,428 +106,304 @@ function Header({
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
subMenuCloseDelay={0.3}
|
||||
items={[
|
||||
{
|
||||
key: "home",
|
||||
icon: <HomeFilled />,
|
||||
label: <Link to="/manage">{t("menus.header.home")}</Link>,
|
||||
},
|
||||
{
|
||||
key: "schedule",
|
||||
icon: <Icon component={FaCalendarAlt} />,
|
||||
label: (
|
||||
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "jobssubmenu",
|
||||
icon: <Icon component={FaCarCrash} />,
|
||||
label: t("menus.header.jobs"),
|
||||
children: [
|
||||
{
|
||||
key: "activejobs",
|
||||
icon: <FileFilled />,
|
||||
label: (
|
||||
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "readyjobs",
|
||||
icon: <CheckCircleOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/jobs/ready">
|
||||
{t("menus.header.readyjobs")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "parts-queue",
|
||||
icon: <ToolFilled />,
|
||||
label: (
|
||||
<Link to="/manage/partsqueue">
|
||||
{t("menus.header.parts-queue")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "availablejobs",
|
||||
icon: <ImportOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/available">
|
||||
{t("menus.header.availablejobs")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "newjob",
|
||||
icon: <FileAddOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||
),
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "alljobs",
|
||||
icon: <UnorderedListOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||
),
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "productionlist",
|
||||
icon: <ScheduleOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/production/list">
|
||||
{t("menus.header.productionlist")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "productionboard",
|
||||
icon: <Icon component={BsKanban} />,
|
||||
label: (
|
||||
<Link to="/manage/production/board">
|
||||
{t("menus.header.productionboard")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "scoreboard",
|
||||
icon: <LineChartOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/scoreboard">
|
||||
{t("menus.header.scoreboard")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "customers",
|
||||
icon: <UserOutlined />,
|
||||
label: t("menus.header.customers"),
|
||||
children: [
|
||||
{
|
||||
key: "owners",
|
||||
icon: <TeamOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/owners">{t("menus.header.owners")}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "vehicles",
|
||||
icon: <CarFilled />,
|
||||
label: (
|
||||
<Link to="/manage/vehicles">
|
||||
{t("menus.header.vehicles")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "ccs",
|
||||
icon: <CarFilled />,
|
||||
label: t("menus.header.courtesycars"),
|
||||
children: [
|
||||
{
|
||||
key: "courtesycarsall",
|
||||
icon: <CarFilled />,
|
||||
label: (
|
||||
<Link to="/manage/courtesycars">
|
||||
{t("menus.header.courtesycars-all")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "contracts",
|
||||
icon: <FileFilled />,
|
||||
label: (
|
||||
<Link to="/manage/courtesycars/contracts">
|
||||
{t("menus.header.courtesycars-contracts")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "newcontract",
|
||||
icon: <FileAddFilled />,
|
||||
label: (
|
||||
<Link to="/manage/courtesycars/contracts/new">
|
||||
{t("menus.header.courtesycars-newcontract")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "accounting",
|
||||
icon: <DollarCircleFilled />,
|
||||
label: t("menus.header.accounting"),
|
||||
children: [
|
||||
{
|
||||
key: "bills",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
label: (
|
||||
<Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "enterbills",
|
||||
icon: <Icon component={GiPayMoney} />,
|
||||
onClick: () => {
|
||||
setBillEnterContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
},
|
||||
label: t("menus.header.enterbills"),
|
||||
},
|
||||
...(Simple_Inventory.treatment === "on"
|
||||
? [
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "inventory",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
label: (
|
||||
<Link to="/manage/inventory">
|
||||
{t("menus.header.inventory")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "allpayments",
|
||||
icon: <BankFilled />,
|
||||
label: (
|
||||
<Link to="/manage/payments">
|
||||
{t("menus.header.allpayments")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "enterpayments",
|
||||
onClick: () => {
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: null,
|
||||
});
|
||||
},
|
||||
icon: <Icon component={FaCreditCard} />,
|
||||
label: t("menus.header.enterpayment"),
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "timetickets",
|
||||
icon: <FieldTimeOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/timetickets">
|
||||
{t("menus.header.timetickets")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "entertimetickets",
|
||||
icon: <Icon component={GiPlayerTime} />,
|
||||
onClick: () => {
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
},
|
||||
label: t("menus.header.entertimeticket"),
|
||||
},
|
||||
{ type: "divider" },
|
||||
{
|
||||
key: "accountingexport",
|
||||
icon: <ExportOutlined />,
|
||||
|
||||
label: t("menus.header.export"),
|
||||
children: [
|
||||
{
|
||||
key: "receivables",
|
||||
label: (
|
||||
<Link to="/manage/accounting/receivables">
|
||||
{t("menus.header.accounting-receivables")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
...(!(
|
||||
(bodyshop && bodyshop.cdk_dealerid) ||
|
||||
(bodyshop && bodyshop.pbs_serialnumber)
|
||||
) || DmsAp.treatment === "on"
|
||||
? [
|
||||
{
|
||||
key: "payables",
|
||||
label: (
|
||||
<Link to="/manage/accounting/payables">
|
||||
{t("menus.header.accounting-payables")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(!(
|
||||
(bodyshop && bodyshop.cdk_dealerid) ||
|
||||
(bodyshop && bodyshop.pbs_serialnumber)
|
||||
)
|
||||
? [
|
||||
{
|
||||
key: "payments",
|
||||
label: (
|
||||
<Link to="/manage/accounting/payments">
|
||||
{t("menus.header.accounting-payments")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "export-logs",
|
||||
label: (
|
||||
<Link to="/manage/accounting/exportlogs">
|
||||
{t("menus.header.export-logs")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
key: "phonebook",
|
||||
icon: <PhoneOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "temporarydocs",
|
||||
icon: <PaperClipOutlined />,
|
||||
label: (
|
||||
<Link to="/manage/temporarydocs">
|
||||
{t("menus.header.temporarydocs")}
|
||||
>
|
||||
<Menu.Item key="home" icon={<HomeFilled />}>
|
||||
<Link to="/manage">{t("menus.header.home")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="schedule" icon={<Icon component={FaCalendarAlt} />}>
|
||||
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
key="jobssubmenu"
|
||||
icon={<Icon component={FaCarCrash} />}
|
||||
title={t("menus.header.jobs")}
|
||||
>
|
||||
<Menu.Item key="activejobs" icon={<FileFilled />}>
|
||||
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="readyjobs" icon={<CheckCircleOutlined />}>
|
||||
<Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="parts-queue" icon={<ToolFilled />}>
|
||||
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="availablejobs" icon={<ImportOutlined />}>
|
||||
<Link to="/manage/available">
|
||||
{t("menus.header.availablejobs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="newjob" icon={<FileAddOutlined />}>
|
||||
<Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div1" />
|
||||
<Menu.Item key="alljobs" icon={<UnorderedListOutlined />}>
|
||||
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div2" />
|
||||
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
|
||||
<Link to="/manage/production/list">
|
||||
{t("menus.header.productionlist")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="productionboard" icon={<Icon component={BsKanban} />}>
|
||||
<Link to="/manage/production/board">
|
||||
{t("menus.header.productionboard")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div3" />
|
||||
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
|
||||
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="customers"
|
||||
icon={<UserOutlined />}
|
||||
title={t("menus.header.customers")}
|
||||
>
|
||||
<Menu.Item key="owners" icon={<TeamOutlined />}>
|
||||
<Link to="/manage/owners">{t("menus.header.owners")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="vehicles" icon={<CarFilled />}>
|
||||
<Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="ccs"
|
||||
icon={<CarFilled />}
|
||||
title={t("menus.header.courtesycars")}
|
||||
>
|
||||
<Menu.Item key="courtesycarsall" icon={<CarFilled />}>
|
||||
<Link to="/manage/courtesycars">
|
||||
{t("menus.header.courtesycars-all")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="contracts" icon={<FileFilled />}>
|
||||
<Link to="/manage/courtesycars/contracts">
|
||||
{t("menus.header.courtesycars-contracts")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="newcontract" icon={<FileAddFilled />}>
|
||||
<Link to="/manage/courtesycars/contracts/new">
|
||||
{t("menus.header.courtesycars-newcontract")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="accounting"
|
||||
icon={<DollarCircleFilled />}
|
||||
title={t("menus.header.accounting")}
|
||||
>
|
||||
<Menu.Item
|
||||
key="bills"
|
||||
icon={<Icon component={FaFileInvoiceDollar} />}
|
||||
>
|
||||
<Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterbills"
|
||||
icon={<Icon component={GiPayMoney} />}
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("menus.header.enterbills")}
|
||||
</Menu.Item>
|
||||
{Simple_Inventory.treatment === "on" && (
|
||||
<>
|
||||
<Menu.Divider key="div4" />
|
||||
<Menu.Item
|
||||
key="inventory"
|
||||
icon={<Icon component={FaFileInvoiceDollar} />}
|
||||
>
|
||||
<Link to="/manage/inventory">
|
||||
{t("menus.header.inventory")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
<Menu.Divider key="div7" />
|
||||
<Menu.Item key="allpayments" icon={<BankFilled />}>
|
||||
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterpayments"
|
||||
onClick={() => {
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: null,
|
||||
});
|
||||
}}
|
||||
icon={<Icon component={FaCreditCard} />}
|
||||
>
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div5" />
|
||||
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
||||
<Link to="/manage/timetickets">
|
||||
{t("menus.header.timetickets")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="entertimetickets"
|
||||
icon={<Icon component={GiPlayerTime} />}
|
||||
onClick={() => {
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("menus.header.entertimeticket")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div6" />
|
||||
<Menu.SubMenu
|
||||
key="accountingexport"
|
||||
title={t("menus.header.export")}
|
||||
icon={<ExportOutlined />}
|
||||
>
|
||||
<Menu.Item key="receivables">
|
||||
<Link to="/manage/accounting/receivables">
|
||||
{t("menus.header.accounting-receivables")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
</Menu.Item>
|
||||
{(!(
|
||||
(bodyshop && bodyshop.cdk_dealerid) ||
|
||||
(bodyshop && bodyshop.pbs_serialnumber)
|
||||
) ||
|
||||
DmsAp.treatment === "on") && (
|
||||
<Menu.Item key="payables">
|
||||
<Link to="/manage/accounting/payables">
|
||||
{t("menus.header.accounting-payables")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{!(
|
||||
(bodyshop && bodyshop.cdk_dealerid) ||
|
||||
(bodyshop && bodyshop.pbs_serialnumber)
|
||||
) && (
|
||||
<Menu.Item key="payments">
|
||||
<Link to="/manage/accounting/payments">
|
||||
{t("menus.header.accounting-payments")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key="export-logs">
|
||||
<Link to="/manage/accounting/exportlogs">
|
||||
{t("menus.header.export-logs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu.SubMenu>
|
||||
<Menu.Item key="phonebook" icon={<PhoneOutlined />}>
|
||||
<Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="temporarydocs" icon={<PaperClipOutlined />}>
|
||||
<Link to="/manage/temporarydocs">
|
||||
{t("menus.header.temporarydocs")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu
|
||||
key="shopsubmenu"
|
||||
title={t("menus.header.shop")}
|
||||
icon={<SettingOutlined />}
|
||||
>
|
||||
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
|
||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="dashboard" icon={<DashboardFilled />}>
|
||||
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="reportcenter"
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => {
|
||||
setReportCenterContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("menus.header.reportcenter")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="shop-vendors"
|
||||
icon={<Icon component={IoBusinessOutline} />}
|
||||
>
|
||||
<Link to="/manage/shop/vendors">
|
||||
{t("menus.header.shop_vendors")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="shop-csi" icon={<Icon component={RiSurveyLine} />}>
|
||||
<Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
key="user"
|
||||
title={
|
||||
currentUser.displayName ||
|
||||
currentUser.email ||
|
||||
t("general.labels.unknown")
|
||||
}
|
||||
>
|
||||
<Menu.Item key="signout" danger onClick={() => signOutStart()}>
|
||||
{t("user.actions.signout")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="help"
|
||||
onClick={() => {
|
||||
window.open("https://help.imex.online/", "_blank");
|
||||
}}
|
||||
icon={<Icon component={QuestionCircleFilled} />}
|
||||
>
|
||||
{t("menus.header.help")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="rescue"
|
||||
onClick={() => {
|
||||
window.open("https://imexrescue.com/", "_blank");
|
||||
}}
|
||||
>
|
||||
{t("menus.header.rescueme")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="shiftclock">
|
||||
<Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="profile">
|
||||
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||
</Menu.Item>
|
||||
{
|
||||
key: "shopsubmenu",
|
||||
icon: <SettingOutlined />,
|
||||
label: t("menus.header.shop"),
|
||||
children: [
|
||||
{
|
||||
key: "shop",
|
||||
icon: <Icon component={GiSettingsKnobs} />,
|
||||
label: (
|
||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "dashboard",
|
||||
icon: <DashboardFilled />,
|
||||
label: (
|
||||
<Link to="/manage/dashboard">
|
||||
{t("menus.header.dashboard")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "reportcenter",
|
||||
icon: <BarChartOutlined />,
|
||||
onClick: () => {
|
||||
setReportCenterContext({
|
||||
actions: {},
|
||||
context: {},
|
||||
});
|
||||
},
|
||||
label: t("menus.header.reportcenter"),
|
||||
},
|
||||
{
|
||||
key: "shop-vendors",
|
||||
icon: <Icon component={IoBusinessOutline} />,
|
||||
label: (
|
||||
<Link to="/manage/shop/vendors">
|
||||
{t("menus.header.shop_vendors")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "shop-csi",
|
||||
icon: <Icon component={RiSurveyLine} />,
|
||||
label: (
|
||||
<Link to="/manage/shop/csi">
|
||||
{t("menus.header.shop_csi")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "user",
|
||||
label:
|
||||
currentUser.displayName ||
|
||||
currentUser.email ||
|
||||
t("general.labels.unknown"),
|
||||
children: [
|
||||
{
|
||||
key: "signout",
|
||||
danger: true,
|
||||
onClick: () => signOutStart(),
|
||||
label: t("user.actions.signout"),
|
||||
},
|
||||
{
|
||||
key: "help",
|
||||
icon: <Icon component={QuestionCircleFilled} />,
|
||||
onClick: () => {
|
||||
window.open("https://help.imex.online/", "_blank");
|
||||
},
|
||||
label: t("menus.header.help"),
|
||||
},
|
||||
{
|
||||
key: "rescue",
|
||||
onClick: () => {
|
||||
window.open("https://imexrescue.com/", "_blank");
|
||||
},
|
||||
label: t("menus.header.rescueme"),
|
||||
},
|
||||
{
|
||||
key: "shiftclock",
|
||||
label: (
|
||||
<Link to="/manage/shiftclock">
|
||||
{t("menus.header.shiftclock")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "profile",
|
||||
label: (
|
||||
<Link to="/manage/profile">
|
||||
{t("menus.currentuser.profile")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "recent",
|
||||
label: <ClockCircleFilled />,
|
||||
children: recentItems.map((i, idx) => ({
|
||||
key: idx,
|
||||
label: <Link to={i.url}>{i.label}</Link>,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
></Menu>
|
||||
// <Menu.SubMenu
|
||||
// key="langselecter"
|
||||
// title={
|
||||
// <span>
|
||||
// <GlobalOutlined />
|
||||
// <span>{t("menus.currentuser.languageselector")}</span>
|
||||
// </span>
|
||||
// }
|
||||
// >
|
||||
// <Menu.Item actiontype="lang-select" key="en-US">
|
||||
// {t("general.languages.english")}
|
||||
// </Menu.Item>
|
||||
// <Menu.Item actiontype="lang-select" key="fr-CA">
|
||||
// {t("general.languages.french")}
|
||||
// </Menu.Item>
|
||||
// <Menu.Item actiontype="lang-select" key="es-MX">
|
||||
// {t("general.languages.spanish")}
|
||||
// </Menu.Item>
|
||||
// </Menu.SubMenu>
|
||||
}
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu key="recent" title={<ClockCircleFilled />}>
|
||||
{recentItems.map((i, idx) => (
|
||||
<Menu.Item key={idx}>
|
||||
<Link to={i.url}>{i.label}</Link>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
</Layout.Header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export function InventoryUpsertModalContainer({
|
||||
? t("inventory.actions.edit")
|
||||
: t("inventory.actions.new")
|
||||
}
|
||||
open={visible}
|
||||
visible={visible}
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => {
|
||||
form.submit();
|
||||
|
||||
@@ -96,7 +96,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId, job }) {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button>
|
||||
<Modal open={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
|
||||
<Modal visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Dropdown, notification } from "antd";
|
||||
import React from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { Dropdown, Menu, notification } from "antd";
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -37,19 +37,18 @@ export function JobAltTransportChange({ bodyshop, job }) {
|
||||
});
|
||||
}
|
||||
};
|
||||
const menu = [
|
||||
...(bodyshop.appt_alt_transport &&
|
||||
bodyshop.appt_alt_transport.map((alt) => ({
|
||||
key: alt,
|
||||
label: alt,
|
||||
value: alt,
|
||||
onClick,
|
||||
}))),
|
||||
{ type: "divider" },
|
||||
{ key: "null", label: t("general.actions.clear"), onClick },
|
||||
];
|
||||
const menu = (
|
||||
<Menu selectedKeys={[job && job.alt_transport]} onClick={onClick}>
|
||||
{bodyshop.appt_alt_transport &&
|
||||
bodyshop.appt_alt_transport.map((alt) => (
|
||||
<Menu.Item key={alt}>{alt}</Menu.Item>
|
||||
))}
|
||||
<Menu.Divider />
|
||||
<Menu.Item key={"null"}>{t("general.actions.clear")}</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<Dropdown menu={{ items: menu }}>
|
||||
<Dropdown overlay={menu}>
|
||||
<a href=" #" onClick={(e) => e.preventDefault()}>
|
||||
<DownOutlined />
|
||||
</a>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Dropdown, notification } from "antd";
|
||||
import React from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||
import { Dropdown, Menu, notification } from "antd";
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -44,23 +44,21 @@ export function ScheduleEventColor({ bodyshop, event }) {
|
||||
bodyshop.appt_colors.filter((color) => color.color.hex === event.color)[0]
|
||||
?.label;
|
||||
|
||||
const menu = {
|
||||
items: [
|
||||
...(bodyshop.appt_colors &&
|
||||
bodyshop.appt_colors.map((color) => ({
|
||||
style: { color: color.color.hex },
|
||||
key: color.color.hex,
|
||||
label: color.label,
|
||||
}))),
|
||||
{ type: "divider" },
|
||||
{ key: "null", value: t("general.actions.clear") },
|
||||
],
|
||||
selectedKeys: [event.color],
|
||||
onClick: onClick,
|
||||
};
|
||||
const menu = (
|
||||
<Menu selectedKeys={[event.color]} onClick={onClick}>
|
||||
{bodyshop.appt_colors &&
|
||||
bodyshop.appt_colors.map((color) => (
|
||||
<Menu.Item style={{ color: color.color.hex }} key={color.color.hex}>
|
||||
{color.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Menu.Divider />
|
||||
<Menu.Item key={"null"}>{t("general.actions.clear")}</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu}>
|
||||
<Dropdown overlay={menu}>
|
||||
<a href=" #" onClick={(e) => e.preventDefault()}>
|
||||
{selectedColor}
|
||||
<DownOutlined />
|
||||
|
||||
@@ -2,9 +2,11 @@ import { AlertFilled } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Dropdown, notification,
|
||||
Dropdown,
|
||||
Menu,
|
||||
notification,
|
||||
Popover,
|
||||
Space
|
||||
Space,
|
||||
} from "antd";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import moment from "moment";
|
||||
@@ -16,7 +18,7 @@ import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
openChatByPhone,
|
||||
setMessage
|
||||
setMessage,
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
@@ -25,11 +27,11 @@ import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
||||
import ScheduleAtChange from "./job-at-change.component";
|
||||
import ScheduleEventColor from "./schedule-event.color.component";
|
||||
import ScheduleEventNote from "./schedule-event.note.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -147,12 +149,10 @@ export function ScheduleEventComponent({
|
||||
) : null}
|
||||
{event.job ? (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
disabled: event.arrived,
|
||||
label: t("general.labels.email"),
|
||||
onClick: () => {
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
const Template = TemplateList("job").appointment_reminder;
|
||||
GenerateDocument(
|
||||
{
|
||||
@@ -166,12 +166,13 @@ export function ScheduleEventComponent({
|
||||
"e",
|
||||
event.job && event.job.id
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("general.labels.sms"),
|
||||
disabled: event.arrived || !bodyshop.messagingservicesid,
|
||||
onClick: () => {
|
||||
}}
|
||||
disabled={event.arrived}
|
||||
>
|
||||
{t("general.labels.email")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
|
||||
if (p && p.isValid()) {
|
||||
openChatByPhone({
|
||||
@@ -191,10 +192,13 @@ export function ScheduleEventComponent({
|
||||
message: t("messaging.error.invalidphone"),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
}}
|
||||
disabled={event.arrived || !bodyshop.messagingservicesid}
|
||||
>
|
||||
{t("general.labels.sms")}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button>{t("appointments.actions.sendreminder")}</Button>
|
||||
</Dropdown>
|
||||
@@ -245,7 +249,7 @@ export function ScheduleEventComponent({
|
||||
const RegularEvent = event.isintake ? (
|
||||
<Space
|
||||
wrap
|
||||
size="small"
|
||||
size='small'
|
||||
style={{
|
||||
backgroundColor:
|
||||
event.color && event.color.hex ? event.color.hex : event.color,
|
||||
@@ -283,8 +287,8 @@ export function ScheduleEventComponent({
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={visible}
|
||||
onOpenChange={(vis) => !event.vacation && setVisible(vis)}
|
||||
visible={visible}
|
||||
onVisibleChange={(vis) => !event.vacation && setVisible(vis)}
|
||||
trigger="click"
|
||||
content={event.block ? blockContent : popoverContent}
|
||||
style={{
|
||||
|
||||
@@ -39,7 +39,7 @@ export function JobCostingModalContainer({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={visible}
|
||||
visible={visible}
|
||||
title={t("jobs.labels.jobcosting")}
|
||||
onOk={() => {
|
||||
toggleModalVisible();
|
||||
|
||||
@@ -76,7 +76,7 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={!!selected}
|
||||
visible={!!selected}
|
||||
destroyOnClose
|
||||
width={drawerPercentage}
|
||||
placement="right"
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import {
|
||||
DeleteFilled,
|
||||
EditFilled,
|
||||
FilterFilled,
|
||||
HomeOutlined,
|
||||
MinusCircleTwoTone,
|
||||
PlusCircleTwoTone,
|
||||
SyncOutlined,
|
||||
WarningFilled,
|
||||
EditFilled,
|
||||
PlusCircleTwoTone,
|
||||
MinusCircleTwoTone,
|
||||
HomeOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { Button, Dropdown, Input, Space, Table, Tag } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu,
|
||||
PageHeader,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
} from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -30,13 +38,13 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
||||
// import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
|
||||
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
|
||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||
import _ from "lodash";
|
||||
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
||||
import JobLinesExpander from "./job-lines-expander.component";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import moment from "moment";
|
||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -95,9 +103,6 @@ export function JobLinesComponent({
|
||||
fixed: "left",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
onCell: (record) => ({
|
||||
className: record.manual_line && "job-line-manual",
|
||||
}),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
@@ -399,17 +404,16 @@ export function JobLinesComponent({
|
||||
}
|
||||
};
|
||||
|
||||
const markMenu = {
|
||||
onClick: handleMark,
|
||||
items: [
|
||||
{ key: "PAA", label: t("joblines.fields.part_types.PAA") },
|
||||
{ key: "PAN", label: t("joblines.fields.part_types.PAN") },
|
||||
{ key: "PAL", label: t("joblines.fields.part_types.PAL") },
|
||||
{ key: "PAS", label: t("joblines.fields.part_types.PAS") },
|
||||
{ type: "divider" },
|
||||
{ key: "clear", label: t("general.labels.clear") },
|
||||
],
|
||||
};
|
||||
const markMenu = (
|
||||
<Menu onClick={handleMark}>
|
||||
<Menu.Item key="PAA">{t("joblines.fields.part_types.PAA")}</Menu.Item>
|
||||
<Menu.Item key="PAN">{t("joblines.fields.part_types.PAN")}</Menu.Item>
|
||||
<Menu.Item key="PAL">{t("joblines.fields.part_types.PAL")}</Menu.Item>
|
||||
<Menu.Item key="PAS">{t("joblines.fields.part_types.PAS")}</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="clear">{t("general.labels.clear")}</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -539,7 +543,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
<FilterFilled /> {t("jobs.actions.filterpartsonly")}
|
||||
</Button>
|
||||
<Dropdown menu={markMenu} trigger={["click"]}>
|
||||
<Dropdown overlay={markMenu} trigger={["click"]}>
|
||||
<Button>{t("jobs.actions.mark")}</Button>
|
||||
</Dropdown>
|
||||
<Button
|
||||
|
||||
@@ -87,7 +87,7 @@ export function JobEmployeeAssignments({
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover destroyTooltipOnHide content={popContent} open={visibility}>
|
||||
<Popover destroyTooltipOnHide content={popContent} visible={visibility}>
|
||||
<Spin spinning={loading}>
|
||||
<DataLabel label={t("jobs.fields.employee_body")}>
|
||||
{body ? (
|
||||
|
||||
@@ -225,7 +225,7 @@ export function JobLineConvertToLabor({
|
||||
<Popover
|
||||
disabled={jobline.convertedtolbr}
|
||||
content={overlay}
|
||||
open={visibility}
|
||||
visible={visibility}
|
||||
placement="bottom"
|
||||
>
|
||||
<Tooltip title={t("joblines.actions.converttolabor")}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -21,17 +21,19 @@ export function JoblinePresetButton({ bodyshop, form }) {
|
||||
form.setFieldsValue(item);
|
||||
};
|
||||
|
||||
const menu = {
|
||||
items: bodyshop.md_jobline_presets.map((i, idx) => ({
|
||||
onClick: () => handleSelect(i),
|
||||
key: idx,
|
||||
label: i.label,
|
||||
})),
|
||||
};
|
||||
const menu = (
|
||||
<Menu>
|
||||
{bodyshop.md_jobline_presets.map((i, idx) => (
|
||||
<Menu.Item onClick={() => handleSelect(i)} key={idx}>
|
||||
{i.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown trigger={["click"]} menu={menu}>
|
||||
<Dropdown trigger={["click"]} overlay={menu}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href="# "
|
||||
|
||||
@@ -49,7 +49,7 @@ export function JobLinesUpsertModalComponent({
|
||||
: t("joblines.labels.new")
|
||||
}
|
||||
forceRender
|
||||
open={visible}
|
||||
visible={visible}
|
||||
width="60%"
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => form.submit()}
|
||||
|
||||
@@ -25,6 +25,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||
});
|
||||
|
||||
const stripeTestEnv = process.env.REACT_APP_STRIPE_PUBLIC_KEY; //.includes("test");
|
||||
|
||||
export function JobPayments({
|
||||
job,
|
||||
jobRO,
|
||||
@@ -92,6 +94,23 @@ export function JobPayments({
|
||||
state.sortedInfo.columnKey === "transactionid" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.stripeid"),
|
||||
dataIndex: "stripeid",
|
||||
key: "stripeid",
|
||||
render: (text, record) =>
|
||||
record.stripeid ? (
|
||||
<a
|
||||
href={
|
||||
stripeTestEnv
|
||||
? `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/test/payments/${record.stripeid}`
|
||||
: `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/payments/${record.stripeid}`
|
||||
}
|
||||
>
|
||||
{record.stripeid}
|
||||
</a>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
|
||||
@@ -42,7 +42,7 @@ function JobReconciliationModalContainer({
|
||||
<Modal
|
||||
title={t("jobs.labels.reconciliationheader")}
|
||||
width={"95%"}
|
||||
open={visible}
|
||||
visible={visible}
|
||||
okText={t("general.actions.close")}
|
||||
onOk={handleCancel}
|
||||
onCancel={handleCancel}
|
||||
|
||||
@@ -166,16 +166,6 @@ export default function ScoreboardAddButton({
|
||||
painthrs: 0,
|
||||
}
|
||||
);
|
||||
|
||||
//Add Labor Adjustments
|
||||
v.painthrs = v.painthrs + (job.lbr_adjustments.LAR || 0);
|
||||
v.bodyhrs =
|
||||
v.bodyhrs +
|
||||
Object.keys(job.lbr_adjustments)
|
||||
.filter((key) => key !== "LAR")
|
||||
.reduce((acc, val) => {
|
||||
return acc + job.lbr_adjustments[val];
|
||||
}, 0);
|
||||
form.setFieldsValue({
|
||||
date: new moment(),
|
||||
bodyhrs: Math.round(v.bodyhrs * 10) / 10,
|
||||
@@ -186,7 +176,7 @@ export default function ScoreboardAddButton({
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover content={overlay} open={visibility} placement="bottom">
|
||||
<Popover content={overlay} visible={visibility} placement="bottom">
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Dropdown, notification } from "antd";
|
||||
import { Button, Dropdown, Menu, notification } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -40,18 +40,20 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
|
||||
});
|
||||
};
|
||||
|
||||
const statusmenu = {
|
||||
onClick: (e) => {
|
||||
updateJobStatus(e.key);
|
||||
},
|
||||
items: bodyshop.md_ro_statuses.statuses.map((item) => ({
|
||||
key: item,
|
||||
label: item,
|
||||
})),
|
||||
};
|
||||
const statusmenu = (
|
||||
<Menu
|
||||
onClick={(e) => {
|
||||
updateJobStatus(e.key);
|
||||
}}
|
||||
>
|
||||
{bodyshop.md_ro_statuses.statuses.map((item) => (
|
||||
<Menu.Item key={item}>{item}</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={statusmenu} trigger={["click"]} key="changestatus">
|
||||
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
|
||||
<Button shape="round">
|
||||
<span>{job.status}</span>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Dropdown, notification } from "antd";
|
||||
import { Button, Dropdown, Menu, notification } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -81,32 +81,29 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
}
|
||||
}, [job, setAvailableStatuses, bodyshop]);
|
||||
|
||||
const statusmenu = {
|
||||
onClick: (e) => {
|
||||
updateJobStatus(e.key);
|
||||
},
|
||||
items: [
|
||||
...availableStatuses.map((item) => ({
|
||||
key: item,
|
||||
label: item,
|
||||
})),
|
||||
...(job.converted
|
||||
? [
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
...otherStages.map((item, idx) => ({
|
||||
key: item,
|
||||
label: item,
|
||||
})),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
const statusmenu = (
|
||||
<Menu
|
||||
onClick={(e) => {
|
||||
updateJobStatus(e.key);
|
||||
}}
|
||||
>
|
||||
{availableStatuses.map((item) => (
|
||||
<Menu.Item key={item}>{item}</Menu.Item>
|
||||
))}
|
||||
{job.converted && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
{otherStages.map((item, idx) => (
|
||||
<Menu.Item key={item}>{item}</Menu.Item>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={statusmenu}
|
||||
overlay={statusmenu}
|
||||
trigger={["click"]}
|
||||
key="changestatus"
|
||||
disabled={jobRO || !job.converted}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Dropdown } from "antd";
|
||||
import { Button, Dropdown, Menu } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -62,17 +62,18 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
||||
);
|
||||
};
|
||||
|
||||
const overlay = {
|
||||
onClick: handleMenuClick,
|
||||
items: bodyshop.md_responsibility_centers.dms_defaults.map((mapping) => ({
|
||||
label: mapping.name,
|
||||
key: mapping.name,
|
||||
disabled: disabled,
|
||||
})),
|
||||
};
|
||||
const overlay = (bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
|
||||
<Menu onClick={handleMenuClick}>
|
||||
{bodyshop.md_responsibility_centers.dms_defaults.map((mapping) => (
|
||||
<Menu.Item disabled={disabled} key={mapping.name}>
|
||||
{mapping.name}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber ? (
|
||||
<Dropdown menu={overlay}>
|
||||
<Dropdown overlay={overlay}>
|
||||
<Button disabled={disabled}>{t("jobs.actions.dmsautoallocate")}</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Space,
|
||||
Switch,
|
||||
} from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -19,6 +18,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import axios from "axios";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -83,11 +83,7 @@ export function JobsConvertButton({
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onFinish={handleConvert}
|
||||
initialValues={{
|
||||
driveable: true,
|
||||
towin: false,
|
||||
employee_csr: job.employee_csr,
|
||||
}}
|
||||
initialValues={{ driveable: true, towin: false }}
|
||||
>
|
||||
<Form.Item
|
||||
name={["ins_co_nm"]}
|
||||
@@ -155,41 +151,6 @@ export function JobsConvertButton({
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{bodyshop.enforce_conversion_csr && (
|
||||
<Form.Item
|
||||
name={"employee_csr"}
|
||||
label={t("jobs.fields.employee_csr")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_conversion_csr,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: 200 }}
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
option.props.children
|
||||
.toLowerCase()
|
||||
.indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{bodyshop.employees
|
||||
.filter((emp) => emp.active)
|
||||
.map((emp) => (
|
||||
<Select.Option
|
||||
value={emp.id}
|
||||
key={emp.id}
|
||||
name={`${emp.first_name} ${emp.last_name}`}
|
||||
>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
label={t("jobs.fields.ca_gst_registrant")}
|
||||
name="ca_gst_registrant"
|
||||
@@ -226,21 +187,14 @@ export function JobsConvertButton({
|
||||
if (job.converted) return <></>;
|
||||
|
||||
return (
|
||||
<Popover open={visible} content={popMenu}>
|
||||
<Popover visible={visible} content={popMenu}>
|
||||
<Button
|
||||
key="convert"
|
||||
type="danger"
|
||||
// style={{ display: job.converted ? "none" : "" }}
|
||||
disabled={job.converted || jobRO}
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue({
|
||||
driveable: true,
|
||||
towin: false,
|
||||
employee_csr: job.employee_csr,
|
||||
});
|
||||
}}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
{t("jobs.actions.convert")}
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -15,17 +15,20 @@ export function JobsDetailChangeEstimator({ disabled, form, bodyshop }) {
|
||||
form.setFieldsValue(est);
|
||||
};
|
||||
|
||||
const menu = {
|
||||
onClick: handleClick,
|
||||
items: bodyshop.md_estimators.map((est, idx) => ({
|
||||
value: est,
|
||||
key: idx,
|
||||
label: `${est.est_ct_fn || ""} ${est.est_ct_ln || ""}`.trim(),
|
||||
})),
|
||||
};
|
||||
const menu = (
|
||||
<div>
|
||||
<Menu onClick={handleClick}>
|
||||
{bodyshop.md_estimators.map((est, idx) => (
|
||||
<Menu.Item value={est} key={idx}>
|
||||
{`${est.est_ct_fn} ${est.est_ct_ln}`}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu} disabled={disabled}>
|
||||
<Dropdown overlay={menu} disabled={disabled}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href=" #"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -15,21 +15,23 @@ export function JobsDetailChangeFilehandler({ disabled, form, bodyshop }) {
|
||||
form.setFieldsValue(est);
|
||||
};
|
||||
|
||||
const menu = {
|
||||
onClick: handleClick,
|
||||
style: {
|
||||
columnCount: Math.floor(bodyshop.md_filehandlers.length / 10) + 1,
|
||||
},
|
||||
items: bodyshop.md_filehandlers.map((est, idx) => ({
|
||||
value: est,
|
||||
key: idx,
|
||||
style: { breakInside: "avoid" },
|
||||
label: `${est.ins_ct_fn || ""} ${est.ins_ct_ln || ""}`.trim(),
|
||||
})),
|
||||
};
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
columnCount: Math.floor(bodyshop.md_filehandlers.length / 10) + 1,
|
||||
}}
|
||||
>
|
||||
{bodyshop.md_filehandlers.map((est, idx) => (
|
||||
<Menu.Item value={est} key={idx} style={{ breakInside: "avoid" }}>
|
||||
{`${est.ins_ct_fn} ${est.ins_ct_ln}`}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu} disabled={disabled}>
|
||||
<Dropdown overlay={menu} disabled={disabled}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href=" #"
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
Menu,
|
||||
notification,
|
||||
Popover,
|
||||
Select,
|
||||
@@ -12,11 +13,25 @@ import {
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
|
||||
export default function JobsDetailHeaderAddEvent({ bodyshop, jobid }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsDetailHeaderAddEvent);
|
||||
|
||||
export function JobsDetailHeaderAddEvent({ bodyshop, jobid, ...props }) {
|
||||
const { t } = useTranslation();
|
||||
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
||||
|
||||
@@ -138,12 +153,11 @@ export default function JobsDetailHeaderAddEvent({ bodyshop, jobid }) {
|
||||
setVisibility(true);
|
||||
};
|
||||
|
||||
return {
|
||||
key: "addmanualevent",
|
||||
label: (
|
||||
<Popover content={overlay} open={visibility} onClick={handleClick}>
|
||||
return (
|
||||
<Popover content={overlay} visible={visibility}>
|
||||
<Menu.Item {...props} onClick={handleClick}>
|
||||
{t("appointments.labels.manualevent")}
|
||||
</Popover>
|
||||
),
|
||||
};
|
||||
</Menu.Item>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { Button, Dropdown, notification, Popconfirm } from "antd";
|
||||
import { Button, Dropdown, Menu, notification, Popconfirm } from "antd";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -13,7 +13,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent";
|
||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||
@@ -105,14 +105,13 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
};
|
||||
|
||||
const statusmenu = {
|
||||
items: [
|
||||
{
|
||||
key: "schedule",
|
||||
disabled: !jobInPreProduction || !job.converted || jobRO,
|
||||
label: t("jobs.actions.schedule"),
|
||||
onClick: () => {
|
||||
const statusmenu = (
|
||||
<Menu key="popovermenu">
|
||||
<Menu.Item
|
||||
disabled={!jobInPreProduction || !job.converted || jobRO}
|
||||
onClick={() => {
|
||||
logImEXEvent("job_header_schedule");
|
||||
|
||||
setScheduleContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
@@ -121,228 +120,200 @@ export function JobsDetailHeaderActions({
|
||||
alt_transport: job.alt_transport,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "cancelappts",
|
||||
disabled: job.status !== bodyshop.md_ro_statuses.default_scheduled,
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("general.labels.areyousure")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
|
||||
onConfirm={async () => {
|
||||
const jobUpdate = await cancelAllAppointments({
|
||||
variables: {
|
||||
jobid: job.id,
|
||||
job: {
|
||||
date_scheduled: null,
|
||||
scheduled_in: null,
|
||||
scheduled_completion: null,
|
||||
status: bodyshop.md_ro_statuses.default_imported,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.schedule")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
|
||||
>
|
||||
<Popconfirm
|
||||
title={t("general.labels.areyousure")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
|
||||
onConfirm={async () => {
|
||||
const jobUpdate = await cancelAllAppointments({
|
||||
variables: {
|
||||
jobid: job.id,
|
||||
job: {
|
||||
date_scheduled: null,
|
||||
scheduled_in: null,
|
||||
scheduled_completion: null,
|
||||
status: bodyshop.md_ro_statuses.default_imported,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!jobUpdate.errors) {
|
||||
notification["success"]({
|
||||
message: t("appointments.successes.canceled"),
|
||||
});
|
||||
if (!jobUpdate.errors) {
|
||||
notification["success"]({
|
||||
message: t("appointments.successes.canceled"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.cancelallappointments")}
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "intake",
|
||||
disabled:
|
||||
return;
|
||||
}
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.cancelallappointments")}
|
||||
</Popconfirm>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={
|
||||
!!job.intakechecklist ||
|
||||
!jobInPreProduction ||
|
||||
!job.converted ||
|
||||
jobRO,
|
||||
label:
|
||||
!!job.intakechecklist ||
|
||||
!jobInPreProduction ||
|
||||
!job.converted ||
|
||||
jobRO ? (
|
||||
t("jobs.actions.intake")
|
||||
) : (
|
||||
<Link to={`/manage/jobs/${job.id}/intake`}>
|
||||
{t("jobs.actions.intake")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
disabled: !jobInProduction || jobRO,
|
||||
key: "deliver",
|
||||
label: !jobInProduction ? (
|
||||
jobRO
|
||||
}
|
||||
>
|
||||
{!!job.intakechecklist ||
|
||||
!jobInPreProduction ||
|
||||
!job.converted ||
|
||||
jobRO ? (
|
||||
t("jobs.actions.intake")
|
||||
) : (
|
||||
<Link to={`/manage/jobs/${job.id}/intake`}>
|
||||
{t("jobs.actions.intake")}
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled={!jobInProduction || jobRO}>
|
||||
{!jobInProduction ? (
|
||||
t("jobs.actions.deliver")
|
||||
) : (
|
||||
<Link to={`/manage/jobs/${job.id}/deliver`}>
|
||||
{t("jobs.actions.deliver")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "viewchecklist",
|
||||
disabled: !job.converted,
|
||||
label: (
|
||||
<Link to={`/manage/jobs/${job.id}/checklist`}>
|
||||
{t("jobs.actions.viewchecklist")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "entertimetickets",
|
||||
disabled:
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled={!job.converted}>
|
||||
<Link to={`/manage/jobs/${job.id}/checklist`}>
|
||||
{t("jobs.actions.viewchecklist")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="entertimetickets"
|
||||
disabled={
|
||||
!job.converted ||
|
||||
(!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||
onClick: () => {
|
||||
(!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced)
|
||||
}
|
||||
onClick={() => {
|
||||
logImEXEvent("job_header_enter_time_ticekts");
|
||||
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
context: { jobId: job.id },
|
||||
});
|
||||
},
|
||||
label: t("timetickets.actions.enter"),
|
||||
},
|
||||
{
|
||||
key: "enterpayments",
|
||||
disabled: !job.converted,
|
||||
label: t("menus.header.enterpayment"),
|
||||
onClick: () => {
|
||||
}}
|
||||
>
|
||||
{t("timetickets.actions.enter")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="enterpayments"
|
||||
disabled={!job.converted}
|
||||
onClick={() => {
|
||||
logImEXEvent("job_header_enter_payment");
|
||||
|
||||
setPaymentContext({
|
||||
actions: {},
|
||||
context: { jobid: job.id },
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "cccontract",
|
||||
disabled: jobRO || !job.converted,
|
||||
label: (
|
||||
<Link
|
||||
to={{
|
||||
pathname: "/manage/courtesycars/contracts/new",
|
||||
state: { jobId: job.id },
|
||||
}}
|
||||
>
|
||||
{t("menus.jobsactions.newcccontract")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
...(job.inproduction
|
||||
? [
|
||||
{
|
||||
key: "removetoproduction",
|
||||
disabled: !job.converted,
|
||||
onClick: () => AddToProduction(client, job.id, refetch, true),
|
||||
label: t("jobs.actions.removefromproduction"),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
key: "addtoproduction",
|
||||
disabled: !job.converted,
|
||||
onClick: () => AddToProduction(client, job.id, refetch),
|
||||
label: t("jobs.actions.addtoproduction"),
|
||||
},
|
||||
]),
|
||||
{
|
||||
key: "togglesuspend",
|
||||
onClick: handleSuspend,
|
||||
label: job.suspended
|
||||
}}
|
||||
>
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="cccontract" disabled={jobRO || !job.converted}>
|
||||
<Link
|
||||
to={{
|
||||
pathname: "/manage/courtesycars/contracts/new",
|
||||
state: { jobId: job.id },
|
||||
}}
|
||||
>
|
||||
{t("menus.jobsactions.newcccontract")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
{job.inproduction ? (
|
||||
<Menu.Item
|
||||
key="addtoproduction"
|
||||
disabled={!job.converted}
|
||||
onClick={() => AddToProduction(client, job.id, refetch, true)}
|
||||
>
|
||||
{t("jobs.actions.removefromproduction")}
|
||||
</Menu.Item>
|
||||
) : (
|
||||
<Menu.Item
|
||||
key="addtoproduction"
|
||||
disabled={!job.converted}
|
||||
onClick={() => AddToProduction(client, job.id, refetch)}
|
||||
>
|
||||
{t("jobs.actions.addtoproduction")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key="togglesuspend" onClick={handleSuspend}>
|
||||
{job.suspended
|
||||
? t("production.actions.unsuspend")
|
||||
: t("production.actions.suspend"),
|
||||
},
|
||||
{
|
||||
key: "toggleAlert",
|
||||
onClick: handleAlertToggle,
|
||||
label:
|
||||
job.production_vars && job.production_vars.alert
|
||||
? t("production.labels.alertoff")
|
||||
: t("production.labels.alerton"),
|
||||
},
|
||||
{
|
||||
key: "dupe",
|
||||
label: t("menus.jobsactions.duplicate"),
|
||||
children: [
|
||||
{
|
||||
key: "dupewithlines",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={() =>
|
||||
DuplicateJob(
|
||||
client,
|
||||
job.id,
|
||||
{
|
||||
defaultOpenStatus:
|
||||
bodyshop.md_ro_statuses.default_imported,
|
||||
},
|
||||
(newJobId) => {
|
||||
history.push(`/manage/jobs/${newJobId}`);
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.duplicated"),
|
||||
});
|
||||
},
|
||||
true
|
||||
)
|
||||
: t("production.actions.suspend")}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="toggleAlert" onClick={handleAlertToggle}>
|
||||
{job.production_vars && job.production_vars.alert
|
||||
? t("production.labels.alertoff")
|
||||
: t("production.labels.alerton")}
|
||||
</Menu.Item>
|
||||
<Menu.SubMenu key="dupe" title={t("menus.jobsactions.duplicate")}>
|
||||
<Menu.Item>
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={() =>
|
||||
DuplicateJob(
|
||||
client,
|
||||
job.id,
|
||||
{ defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
|
||||
(newJobId) => {
|
||||
history.push(`/manage/jobs/${newJobId}`);
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.duplicated"),
|
||||
});
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.duplicate")}
|
||||
</Popconfirm>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={() =>
|
||||
DuplicateJob(
|
||||
client,
|
||||
job.id,
|
||||
{ defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
|
||||
(newJobId) => {
|
||||
history.push(`/manage/jobs/${newJobId}`);
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.duplicated"),
|
||||
});
|
||||
}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.duplicate")}
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "dupewithoutlines",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={() =>
|
||||
DuplicateJob(
|
||||
client,
|
||||
job.id,
|
||||
{
|
||||
defaultOpenStatus:
|
||||
bodyshop.md_ro_statuses.default_imported,
|
||||
},
|
||||
(newJobId) => {
|
||||
history.push(`/manage/jobs/${newJobId}`);
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.duplicated"),
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.duplicatenolines")}
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "postbills",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.postbills"),
|
||||
onClick: () => {
|
||||
)
|
||||
}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.duplicatenolines")}
|
||||
</Popconfirm>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
|
||||
<Menu.Item
|
||||
key="postbills"
|
||||
disabled={!job.converted}
|
||||
onClick={() => {
|
||||
logImEXEvent("job_header_enter_bills");
|
||||
|
||||
setBillEnterContext({
|
||||
@@ -351,13 +322,14 @@ export function JobsDetailHeaderActions({
|
||||
job: job,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "addtopartsqueue",
|
||||
disabled: !job.converted || !jobInProduction || jobRO,
|
||||
label: t("jobs.actions.addtopartsqueue"),
|
||||
onClick: async () => {
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.postbills")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="addtopartsqueue"
|
||||
disabled={!job.converted || !jobInProduction || jobRO}
|
||||
onClick={async () => {
|
||||
const result = await updateJob({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
@@ -376,12 +348,12 @@ export function JobsDetailHeaderActions({
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
disabled: !jobInPostProduction,
|
||||
key: "closejob",
|
||||
label: !jobInPostProduction ? (
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.addtopartsqueue")}
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled={!jobInPostProduction} key="closejob">
|
||||
{!jobInPostProduction ? (
|
||||
t("menus.jobsactions.closejob")
|
||||
) : (
|
||||
<Link
|
||||
@@ -391,27 +363,23 @@ export function JobsDetailHeaderActions({
|
||||
>
|
||||
{t("menus.jobsactions.closejob")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "admin",
|
||||
label: (
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/jobs/${job.id}/admin`,
|
||||
}}
|
||||
>
|
||||
{t("menus.jobsactions.admin")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
JobsDetailHeaderActionsExportcustdataComponent({ bodyshop, job }),
|
||||
JobsDetaiLheaderCsi({ job, bodyshop }),
|
||||
{
|
||||
key: "jobcosting",
|
||||
label: t("jobs.labels.jobcosting"),
|
||||
disabled: !job.converted,
|
||||
onClick: () => {
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="admin">
|
||||
<Link
|
||||
to={{
|
||||
pathname: `/manage/jobs/${job.id}/admin`,
|
||||
}}
|
||||
>
|
||||
{t("menus.jobsactions.admin")}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<JobsDetailHeaderActionsExportcustdataComponent job={job} />
|
||||
<JobsDetaiLheaderCsi job={job} />
|
||||
<Menu.Item
|
||||
key="jobcosting"
|
||||
disabled={!job.converted}
|
||||
onClick={() => {
|
||||
logImEXEvent("job_header_job_costing");
|
||||
setJobCostingContext({
|
||||
actions: { refetch: refetch },
|
||||
@@ -419,108 +387,96 @@ export function JobsDetailHeaderActions({
|
||||
jobId: job.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
...(job && !job.converted
|
||||
? [
|
||||
{
|
||||
key: "deletejob",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deleteconfirm")}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={async () => {
|
||||
//delete the job.
|
||||
const result = await deleteJob({
|
||||
variables: { id: job.id },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("jobs.labels.jobcosting")}
|
||||
</Menu.Item>
|
||||
{job && !job.converted && (
|
||||
<Menu.Item>
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deleteconfirm")}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={async () => {
|
||||
//delete the job.
|
||||
const result = await deleteJob({ variables: { id: job.id } });
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.delete"),
|
||||
});
|
||||
//go back to jobs list.
|
||||
history.push(`/manage/`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.deleted", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.deletejob")}
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
///////HEADER ADD EVENT ITEM
|
||||
JobsDetailHeaderActionsAddevent({ jobid: job.id, bodyshop }),
|
||||
...(!jobRO && job.converted
|
||||
? [
|
||||
{
|
||||
key: "voidjob",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.voidjob")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={async () => {
|
||||
//delete the job.
|
||||
const result = await voidJob({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: {
|
||||
status: bodyshop.md_ro_statuses.default_void,
|
||||
voided: true,
|
||||
scheduled_in: null,
|
||||
scheduled_completion: null,
|
||||
inproduction: false,
|
||||
},
|
||||
note: [
|
||||
{
|
||||
jobid: job.id,
|
||||
created_by: currentUser.email,
|
||||
audit: true,
|
||||
text: t("jobs.labels.voidnote"),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.voided"),
|
||||
});
|
||||
//go back to jobs list.
|
||||
history.push(`/manage/`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.voiding", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.void")}
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.delete"),
|
||||
});
|
||||
//go back to jobs list.
|
||||
history.push(`/manage/`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.deleted", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.deletejob")}
|
||||
</Popconfirm>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<JobsDetailHeaderActionsAddevent jobid={job.id} />
|
||||
{!jobRO && job.converted && (
|
||||
<Menu.Item>
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.voidjob")}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onConfirm={async () => {
|
||||
//delete the job.
|
||||
const result = await voidJob({
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: {
|
||||
status: bodyshop.md_ro_statuses.default_void,
|
||||
voided: true,
|
||||
scheduled_in: null,
|
||||
scheduled_completion: null,
|
||||
inproduction: false,
|
||||
},
|
||||
note: [
|
||||
{
|
||||
jobid: job.id,
|
||||
created_by: currentUser.email,
|
||||
audit: true,
|
||||
text: t("jobs.labels.voidnote"),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.voided"),
|
||||
});
|
||||
//go back to jobs list.
|
||||
history.push(`/manage/`);
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.voiding", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
getPopupContainer={(trigger) => trigger.parentNode}
|
||||
>
|
||||
{t("menus.jobsactions.void")}
|
||||
</Popconfirm>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<Dropdown menu={statusmenu} trigger={["click"]} key="changestatus">
|
||||
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
|
||||
<Button>
|
||||
<span>{t("general.labels.actions")}</span>
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { notification } from "antd";
|
||||
import { Menu, notification } from "antd";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import React 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 {
|
||||
GET_CURRENT_QUESTIONSET_ID,
|
||||
@@ -13,14 +16,30 @@ import {
|
||||
openChatByPhone,
|
||||
setMessage,
|
||||
} from "../../redux/messaging/messaging.actions";
|
||||
import { store } from "../../redux/store";
|
||||
import i18n from "../../translations/i18n";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
export default function JobsDetailHeaderCsi({ bodyshop, job }) {
|
||||
const { t } = i18n;
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser'
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
setMessage: (text) => dispatch(setMessage(text)),
|
||||
});
|
||||
|
||||
export function JobsDetailHeaderCsi({
|
||||
setEmailOptions,
|
||||
bodyshop,
|
||||
job,
|
||||
openChatByPhone,
|
||||
setMessage,
|
||||
...props
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [insertCsi] = useMutation(INSERT_CSI);
|
||||
const client = useApolloClient();
|
||||
|
||||
@@ -78,35 +97,29 @@ export default function JobsDetailHeaderCsi({ bodyshop, job }) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "email")
|
||||
store.dispatch(
|
||||
setEmailOptions({
|
||||
jobid: job.id,
|
||||
messageOptions: {
|
||||
to: [job.ownr_ea],
|
||||
replyTo: bodyshop.email,
|
||||
setEmailOptions({
|
||||
jobid: job.id,
|
||||
messageOptions: {
|
||||
to: [job.ownr_ea],
|
||||
replyTo: bodyshop.email,
|
||||
},
|
||||
template: {
|
||||
name: TemplateList("job_special").csi_invitation_action.key,
|
||||
variables: {
|
||||
id: result.data.insert_csi.returning[0].id,
|
||||
},
|
||||
template: {
|
||||
name: TemplateList("job_special").csi_invitation_action.key,
|
||||
variables: {
|
||||
id: result.data.insert_csi.returning[0].id,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (e.key === "text") {
|
||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
||||
if (p && p.isValid()) {
|
||||
store.dispatch(
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id,
|
||||
})
|
||||
);
|
||||
store.dispatch(
|
||||
setMessage(
|
||||
`${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
|
||||
)
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id,
|
||||
});
|
||||
setMessage(
|
||||
`${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
|
||||
);
|
||||
} else {
|
||||
notification["error"]({
|
||||
@@ -127,35 +140,29 @@ export default function JobsDetailHeaderCsi({ bodyshop, job }) {
|
||||
}
|
||||
} else {
|
||||
if (e.key === "email")
|
||||
store.dispatch(
|
||||
setEmailOptions({
|
||||
jobid: job.id,
|
||||
messageOptions: {
|
||||
to: [job.ownr_ea],
|
||||
replyTo: bodyshop.email,
|
||||
setEmailOptions({
|
||||
jobid: job.id,
|
||||
messageOptions: {
|
||||
to: [job.ownr_ea],
|
||||
replyTo: bodyshop.email,
|
||||
},
|
||||
template: {
|
||||
name: TemplateList("job_special").csi_invitation_action.key,
|
||||
variables: {
|
||||
id: job.csiinvites[0].id,
|
||||
},
|
||||
template: {
|
||||
name: TemplateList("job_special").csi_invitation_action.key,
|
||||
variables: {
|
||||
id: job.csiinvites[0].id,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (e.key === "text") {
|
||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
||||
if (p && p.isValid()) {
|
||||
store.dispatch(
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id,
|
||||
})
|
||||
);
|
||||
store.dispatch(
|
||||
setMessage(
|
||||
`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`
|
||||
)
|
||||
openChatByPhone({
|
||||
phone_num: p.formatInternational(),
|
||||
jobid: job.id,
|
||||
});
|
||||
setMessage(
|
||||
`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`
|
||||
);
|
||||
} else {
|
||||
notification["error"]({
|
||||
@@ -173,51 +180,61 @@ export default function JobsDetailHeaderCsi({ bodyshop, job }) {
|
||||
}
|
||||
};
|
||||
|
||||
if (!HasFeatureAccess({ featureName: "csi", bodyshop })) return {};
|
||||
if (!HasFeatureAccess({ featureName: "csi", bodyshop })) return <></>;
|
||||
|
||||
return {
|
||||
key: "sendcsi",
|
||||
label: t("jobs.actions.sendcsi"),
|
||||
children: [
|
||||
{
|
||||
key: "email",
|
||||
onClick: handleCreateCsi,
|
||||
disabled: !!!job.ownr_ea,
|
||||
label: t("general.labels.email"),
|
||||
},
|
||||
{
|
||||
onClick: handleCreateCsi,
|
||||
key: "text",
|
||||
disabled: !!!job.ownr_ph1,
|
||||
label: t("general.labels.text"),
|
||||
},
|
||||
{
|
||||
key: "generate",
|
||||
onClick: handleCreateCsi,
|
||||
disabled: job.csiinvites && job.csiinvites.length > 0,
|
||||
label: t("jobs.actions.generatecsi"),
|
||||
},
|
||||
{ type: "divider" },
|
||||
...job.csiinvites.map((item, idx) => {
|
||||
return item.completedon
|
||||
? {
|
||||
key: idx,
|
||||
label: (
|
||||
<Link to={`/manage/shop/csi?responseid=${item.id}`}>
|
||||
<DateTimeFormatter>{item.completedon}</DateTimeFormatter>
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
: {
|
||||
key: idx,
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(
|
||||
`${window.location.protocol}//${window.location.host}/csi/${item.id}`
|
||||
);
|
||||
},
|
||||
label: t("general.actions.copylink"),
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
key="sendcsi"
|
||||
title={t("jobs.actions.sendcsi")}
|
||||
disabled={!job.converted}
|
||||
{...props}
|
||||
>
|
||||
<Menu.Item
|
||||
onClick={handleCreateCsi}
|
||||
key="email"
|
||||
disabled={!!!job.ownr_ea}
|
||||
>
|
||||
{t("general.labels.email")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={handleCreateCsi}
|
||||
key="text"
|
||||
disabled={!!!job.ownr_ph1}
|
||||
>
|
||||
{t("general.labels.text")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={handleCreateCsi}
|
||||
key="generate"
|
||||
disabled={job.csiinvites && job.csiinvites.length > 0}
|
||||
>
|
||||
{t("jobs.actions.generatecsi")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
{job.csiinvites.map((item, idx) => {
|
||||
return item.completedon ? (
|
||||
<Menu.Item key={idx}>
|
||||
<Link to={`/manage/shop/csi?responseid=${item.id}`}>
|
||||
<DateTimeFormatter>{item.completedon}</DateTimeFormatter>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
) : (
|
||||
<Menu.Item
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${window.location.protocol}//${window.location.host}/csi/${item.id}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("general.actions.copylink")}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsDetailHeaderCsi);
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { notification } from "antd";
|
||||
import { Menu, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import i18next from "i18next";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
export default function JobsDetailHeaderActionexportCustomerData({
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function JobsDetailHeaderActionexportCustomerData({
|
||||
bodyshop,
|
||||
job,
|
||||
...props
|
||||
}) {
|
||||
const { t } = i18next;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleExportCustData = async (e) => {
|
||||
logImEXEvent("job_export_cust_data");
|
||||
@@ -90,20 +100,18 @@ export default function JobsDetailHeaderActionexportCustomerData({
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onClick: handleExportCustData,
|
||||
key: "exportcustdata",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.exportcustdata"),
|
||||
};
|
||||
// (
|
||||
// <Menu.Item
|
||||
// {...props}
|
||||
// onClick={handleExportCustData}
|
||||
// key="exportcustdata"
|
||||
// disabled={!job.converted}
|
||||
// >
|
||||
// {t("jobs.actions.exportcustdata")}
|
||||
// </Menu.Item>
|
||||
// );
|
||||
return (
|
||||
<Menu.Item
|
||||
{...props}
|
||||
onClick={handleExportCustData}
|
||||
key="exportcustdata"
|
||||
disabled={!job.converted}
|
||||
>
|
||||
{t("jobs.actions.exportcustdata")}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsDetailHeaderActionexportCustomerData);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -18,16 +18,20 @@ export function JobsDetailRatesChangeButton({ disabled, form, bodyshop }) {
|
||||
form.setFieldsValue({ ...rate, labor_rate_desc: rate.rate_label });
|
||||
};
|
||||
|
||||
const menu = {
|
||||
onClick: handleClick,
|
||||
items: bodyshop.md_labor_rates.map((rate, idx) => ({
|
||||
key: idx,
|
||||
value: rate,
|
||||
label: rate.rate_label,
|
||||
})),
|
||||
};
|
||||
const menu = (
|
||||
<div>
|
||||
<Menu onClick={handleClick}>
|
||||
{bodyshop.md_labor_rates.map((rate, idx) => (
|
||||
<Menu.Item value={rate} key={idx}>
|
||||
{rate.rate_label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu} disabled={disabled}>
|
||||
<Dropdown overlay={menu} disabled={disabled}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href=" #"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
import formatBytes from "../../utils/formatbytes";
|
||||
//import yauzl from "yauzl";
|
||||
import yauzl from "yauzl";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
@@ -36,7 +36,7 @@ export function JobsDocumentsDownloadButton({
|
||||
);
|
||||
const imagesToDownload = [
|
||||
...galleryImages.images.filter((image) => image.isSelected),
|
||||
...galleryImages.other.filter((image) => image.isSelected),
|
||||
...galleryImages.other.filter((image) => image.isSelected),
|
||||
];
|
||||
|
||||
function downloadProgress(progressEvent) {
|
||||
@@ -69,42 +69,44 @@ export function JobsDocumentsDownloadButton({
|
||||
setDownload(null);
|
||||
if (Direct_Media_Download.treatment === "on") {
|
||||
try {
|
||||
// const parentDir = await window.showDirectoryPicker({
|
||||
// id: "media",
|
||||
// startIn: "downloads",
|
||||
// });
|
||||
// const directory = await parentDir.getDirectoryHandle(identifier, {
|
||||
// create: true,
|
||||
// });
|
||||
// yauzl.fromBuffer(
|
||||
// Buffer.from(theDownloadedZip.data),
|
||||
// {},
|
||||
// (err, zipFile) => {
|
||||
// if (err) throw err;
|
||||
// zipFile.on("entry", (entry) => {
|
||||
// zipFile.openReadStream(entry, async (readErr, readStream) => {
|
||||
// if (readErr) {
|
||||
// zipFile.close();
|
||||
// throw readErr;
|
||||
// }
|
||||
// if (err) throw err;
|
||||
// let fileSystemHandle = await directory.getFileHandle(
|
||||
// entry.fileName,
|
||||
// {
|
||||
// create: true,
|
||||
// }
|
||||
// );
|
||||
// const writable = await fileSystemHandle.createWritable();
|
||||
// readStream.on("data", async function (chunk) {
|
||||
// await writable.write(chunk);
|
||||
// });
|
||||
// readStream.on("end", async function () {
|
||||
// await writable.close();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// );
|
||||
const parentDir = await window.showDirectoryPicker({
|
||||
id: "media",
|
||||
startIn: "downloads",
|
||||
});
|
||||
|
||||
const directory = await parentDir.getDirectoryHandle(identifier, {
|
||||
create: true,
|
||||
});
|
||||
|
||||
yauzl.fromBuffer(
|
||||
Buffer.from(theDownloadedZip.data),
|
||||
{},
|
||||
(err, zipFile) => {
|
||||
if (err) throw err;
|
||||
zipFile.on("entry", (entry) => {
|
||||
zipFile.openReadStream(entry, async (readErr, readStream) => {
|
||||
if (readErr) {
|
||||
zipFile.close();
|
||||
throw readErr;
|
||||
}
|
||||
if (err) throw err;
|
||||
let fileSystemHandle = await directory.getFileHandle(
|
||||
entry.fileName,
|
||||
{
|
||||
create: true,
|
||||
}
|
||||
);
|
||||
const writable = await fileSystemHandle.createWritable();
|
||||
readStream.on("data", async function (chunk) {
|
||||
await writable.write(chunk);
|
||||
});
|
||||
readStream.on("end", async function () {
|
||||
await writable.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
standardMediaDownload(theDownloadedZip.data);
|
||||
@@ -121,7 +123,7 @@ export function JobsDocumentsDownloadButton({
|
||||
a.click();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -159,7 +159,7 @@ export function JobsDocumentsGalleryReassign({
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popContent} open={visible}>
|
||||
<Popover content={popContent} visible={visible}>
|
||||
<Button
|
||||
disabled={selectedImages.length < 1}
|
||||
onClick={() => setVisible(true)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Col, Row, Space } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import Gallery from "react-grid-gallery";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
|
||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||
@@ -10,8 +10,7 @@ import JobsDocumentsDownloadButton from "./jobs-document-gallery.download.compon
|
||||
import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.component";
|
||||
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
|
||||
import JobsDocumentsGallerySelectAllComponent from "./jobs-documents-gallery.selectall.component";
|
||||
import Lightbox from "react-image-lightbox";
|
||||
import "react-image-lightbox/style.css";
|
||||
|
||||
function JobsDocumentsComponent({
|
||||
data,
|
||||
jobId,
|
||||
@@ -24,7 +23,11 @@ function JobsDocumentsComponent({
|
||||
}) {
|
||||
const [galleryImages, setgalleryImages] = useState({ images: [], other: [] });
|
||||
const { t } = useTranslation();
|
||||
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const onCurrentImageChange = (index) => {
|
||||
setIndex(index);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let documents = data.reduce(
|
||||
@@ -32,12 +35,10 @@ function JobsDocumentsComponent({
|
||||
const fileType = DetermineFileType(value.type);
|
||||
if (value.type.startsWith("image")) {
|
||||
acc.images.push({
|
||||
// src: GenerateSrcUrl(value),
|
||||
// thumbnail: GenerateThumbUrl(value),
|
||||
fullsize: GenerateSrcUrl(value),
|
||||
src: GenerateThumbUrl(value),
|
||||
height: 225,
|
||||
width: 225,
|
||||
src: GenerateSrcUrl(value),
|
||||
thumbnail: GenerateThumbUrl(value),
|
||||
thumbnailHeight: 225,
|
||||
thumbnailWidth: 225,
|
||||
isSelected: false,
|
||||
key: value.key,
|
||||
extension: value.extension,
|
||||
@@ -61,8 +62,8 @@ function JobsDocumentsComponent({
|
||||
const fileName = value.key.split("/").pop();
|
||||
acc.other.push({
|
||||
source: GenerateSrcUrl(value),
|
||||
//src: "",
|
||||
src: thumb,
|
||||
src: "",
|
||||
thumbnail: thumb,
|
||||
tags: [
|
||||
{
|
||||
value: fileName,
|
||||
@@ -84,8 +85,8 @@ function JobsDocumentsComponent({
|
||||
]
|
||||
: []),
|
||||
],
|
||||
height: 225,
|
||||
width: 225,
|
||||
thumbnailHeight: 225,
|
||||
thumbnailWidth: 225,
|
||||
isSelected: false,
|
||||
|
||||
extension: value.extension,
|
||||
@@ -147,15 +148,35 @@ function JobsDocumentsComponent({
|
||||
<Card title={t("jobs.labels.documents-images")}>
|
||||
<Gallery
|
||||
images={galleryImages.images}
|
||||
onClick={(index, item) => {
|
||||
setModalState({ open: true, index: index });
|
||||
// window.open(
|
||||
// item.fullsize,
|
||||
// "_blank",
|
||||
// "toolbar=0,location=0,menubar=0"
|
||||
// );
|
||||
backdropClosesModal={true}
|
||||
currentImageWillChange={onCurrentImageChange}
|
||||
customControls={[
|
||||
<Button
|
||||
key="edit-button"
|
||||
style={{
|
||||
float: "right",
|
||||
zIndex: "5",
|
||||
}}
|
||||
onClick={() => {
|
||||
const newWindow = window.open(
|
||||
`${window.location.protocol}//${window.location.host}/edit?documentId=${galleryImages.images[index].id}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
if (newWindow) newWindow.opener = null;
|
||||
}}
|
||||
>
|
||||
<EditFilled />
|
||||
</Button>,
|
||||
]}
|
||||
onClickImage={(props) => {
|
||||
window.open(
|
||||
props.target.src,
|
||||
"_blank",
|
||||
"toolbar=0,location=0,menubar=0"
|
||||
);
|
||||
}}
|
||||
onSelect={(index, image) => {
|
||||
onSelectImage={(index, image) => {
|
||||
setgalleryImages({
|
||||
...galleryImages,
|
||||
images: galleryImages.images.map((g, idx) =>
|
||||
@@ -170,6 +191,8 @@ function JobsDocumentsComponent({
|
||||
<Card title={t("jobs.labels.documents-other")}>
|
||||
<Gallery
|
||||
images={galleryImages.other}
|
||||
backdropClosesModal={true}
|
||||
enableLightbox={false}
|
||||
thumbnailStyle={() => {
|
||||
return {
|
||||
backgroundImage: <FileExcelFilled />,
|
||||
@@ -178,14 +201,14 @@ function JobsDocumentsComponent({
|
||||
cursor: "pointer",
|
||||
};
|
||||
}}
|
||||
onClick={(index) => {
|
||||
onClickThumbnail={(index) => {
|
||||
window.open(
|
||||
galleryImages.other[index].source,
|
||||
"_blank",
|
||||
"toolbar=0,location=0,menubar=0"
|
||||
);
|
||||
}}
|
||||
onSelect={(index) => {
|
||||
onSelectImage={(index) => {
|
||||
setgalleryImages({
|
||||
...galleryImages,
|
||||
other: galleryImages.other.map((g, idx) =>
|
||||
@@ -197,53 +220,6 @@ function JobsDocumentsComponent({
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{modalState.open && (
|
||||
<Lightbox
|
||||
toolbarButtons={[
|
||||
<EditFilled
|
||||
onClick={() => {
|
||||
const newWindow = window.open(
|
||||
`${window.location.protocol}//${
|
||||
window.location.host
|
||||
}/edit?documentId=${
|
||||
galleryImages.images[modalState.index].id
|
||||
}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
if (newWindow) newWindow.opener = null;
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
mainSrc={galleryImages.images[modalState.index].fullsize}
|
||||
nextSrc={
|
||||
galleryImages.images[
|
||||
(modalState.index + 1) % galleryImages.images.length
|
||||
].fullsize
|
||||
}
|
||||
prevSrc={
|
||||
galleryImages.images[
|
||||
(modalState.index + galleryImages.images.length - 1) %
|
||||
galleryImages.images.length
|
||||
].fullsize
|
||||
}
|
||||
onCloseRequest={() => setModalState({ open: false, index: 0 })}
|
||||
onMovePrevRequest={() =>
|
||||
setModalState({
|
||||
...modalState,
|
||||
index:
|
||||
(modalState.index + galleryImages.images.length - 1) %
|
||||
galleryImages.images.length,
|
||||
})
|
||||
}
|
||||
onMoveNextRequest={() =>
|
||||
setModalState({
|
||||
...modalState,
|
||||
index: (modalState.index + 1) % galleryImages.images.length,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SyncOutlined, FileExcelFilled } from "@ant-design/icons";
|
||||
import { Alert, Button, Card, Space } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import React, { useEffect } from "react";
|
||||
import Gallery from "react-grid-gallery";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -18,8 +18,6 @@ import JobsDocumentsLocalDeleteButton from "./jobs-documents-local-gallery.delet
|
||||
import JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.download";
|
||||
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
|
||||
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
|
||||
import Lightbox from "react-image-lightbox";
|
||||
import "react-image-lightbox/style.css";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -51,7 +49,6 @@ export function JobsDocumentsLocalGallery({
|
||||
vendorid,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [modalState, setModalState] = useState({ open: false, index: 0 });
|
||||
useEffect(() => {
|
||||
if (job) {
|
||||
if (invoice_number) {
|
||||
@@ -61,7 +58,6 @@ export function JobsDocumentsLocalGallery({
|
||||
}
|
||||
}
|
||||
}, [job, invoice_number, getJobMedia, getBillMedia]);
|
||||
|
||||
let optimized;
|
||||
const jobMedia =
|
||||
allMedia && allMedia[job.id]
|
||||
@@ -74,20 +70,12 @@ export function JobsDocumentsLocalGallery({
|
||||
) {
|
||||
acc.images.push({
|
||||
...val,
|
||||
fullsize: val.src,
|
||||
src: val.thumbnail,
|
||||
height: val.thumbnailHeight,
|
||||
width: val.thumbnailWidth,
|
||||
...(val.optimized && { src: val.optimized, fullsize: val.src }),
|
||||
});
|
||||
if (val.optimized) optimized = true;
|
||||
} else {
|
||||
acc.other.push({
|
||||
...val,
|
||||
fullsize: val.src,
|
||||
src: val.thumbnail,
|
||||
height: val.thumbnailHeight,
|
||||
width: val.thumbnailWidth,
|
||||
tags: [{ value: val.filename, title: val.filename }],
|
||||
});
|
||||
}
|
||||
@@ -132,7 +120,8 @@ export function JobsDocumentsLocalGallery({
|
||||
<Card title={t("jobs.labels.documents-images")}>
|
||||
<Gallery
|
||||
images={jobMedia.images}
|
||||
onSelect={(index, image) => {
|
||||
backdropClosesModal={true}
|
||||
onSelectImage={(index, image) => {
|
||||
toggleMediaSelected({ jobid: job.id, filename: image.filename });
|
||||
}}
|
||||
{...(optimized && {
|
||||
@@ -144,23 +133,24 @@ export function JobsDocumentsLocalGallery({
|
||||
/>,
|
||||
],
|
||||
})}
|
||||
onClick={(index, item) => {
|
||||
setModalState({ open: true, index: index });
|
||||
// const media = allMedia[job.id].find(
|
||||
// (m) => m.optimized === item.src
|
||||
// );
|
||||
onClickImage={(props) => {
|
||||
const media = allMedia[job.id].find(
|
||||
(m) => m.optimized === props.target.src
|
||||
);
|
||||
|
||||
// window.open(
|
||||
// media ? media.fullsize : item.fullsize,
|
||||
// "_blank",
|
||||
// "toolbar=0,location=0,menubar=0"
|
||||
// );
|
||||
window.open(
|
||||
media ? media.src : props.target.src,
|
||||
"_blank",
|
||||
"toolbar=0,location=0,menubar=0"
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Card title={t("jobs.labels.documents-other")}>
|
||||
<Gallery
|
||||
images={jobMedia.other}
|
||||
backdropClosesModal={true}
|
||||
enableLightbox={false}
|
||||
thumbnailStyle={() => {
|
||||
return {
|
||||
backgroundImage: <FileExcelFilled />,
|
||||
@@ -169,48 +159,18 @@ export function JobsDocumentsLocalGallery({
|
||||
cursor: "pointer",
|
||||
};
|
||||
}}
|
||||
onClick={(index) => {
|
||||
onClickThumbnail={(index) => {
|
||||
window.open(
|
||||
jobMedia.other[index].fullsize,
|
||||
jobMedia.other[index].src,
|
||||
"_blank",
|
||||
"toolbar=0,location=0,menubar=0"
|
||||
);
|
||||
}}
|
||||
onSelect={(index, image) => {
|
||||
onSelectImage={(index, image) => {
|
||||
toggleMediaSelected({ jobid: job.id, filename: image.filename });
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
{modalState.open && (
|
||||
<Lightbox
|
||||
mainSrc={jobMedia.images[modalState.index].fullsize}
|
||||
nextSrc={
|
||||
jobMedia.images[(modalState.index + 1) % jobMedia.images.length]
|
||||
.fullsize
|
||||
}
|
||||
prevSrc={
|
||||
jobMedia.images[
|
||||
(modalState.index + jobMedia.images.length - 1) %
|
||||
jobMedia.images.length
|
||||
].fullsize
|
||||
}
|
||||
onCloseRequest={() => setModalState({ open: false, index: 0 })}
|
||||
onMovePrevRequest={() =>
|
||||
setModalState({
|
||||
...modalState,
|
||||
index:
|
||||
(modalState.index + jobMedia.images.length - 1) %
|
||||
jobMedia.images.length,
|
||||
})
|
||||
}
|
||||
onMoveNextRequest={() =>
|
||||
setModalState({
|
||||
...modalState,
|
||||
index: (modalState.index + 1) % jobMedia.images.length,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import Gallery from "react-grid-gallery";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -38,7 +38,7 @@ function JobDocumentsLocalGalleryExternal({
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (jobId) {
|
||||
if ( jobId) {
|
||||
getJobMedia(jobId);
|
||||
}
|
||||
}, [jobId, getJobMedia]);
|
||||
@@ -52,7 +52,7 @@ function JobDocumentsLocalGalleryExternal({
|
||||
val.type.mime &&
|
||||
val.type.mime.startsWith("image")
|
||||
) {
|
||||
acc.push({ ...val, src: val.thumbnail });
|
||||
acc.push(val);
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
@@ -65,6 +65,7 @@ function JobDocumentsLocalGalleryExternal({
|
||||
<div className="clearfix">
|
||||
<Gallery
|
||||
images={galleryImages}
|
||||
backdropClosesModal={true}
|
||||
onSelectImage={(index, image) => {
|
||||
setgalleryImages(
|
||||
galleryImages.map((g, idx) =>
|
||||
|
||||
@@ -83,7 +83,7 @@ export function JobsDocumentsLocalGalleryReassign({
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={popContent} open={visible}>
|
||||
<Popover content={popContent} visible={visible}>
|
||||
<Button
|
||||
//disabled={selectedImages.length < 1}
|
||||
onClick={() => setVisible(true)}
|
||||
|
||||
@@ -171,8 +171,8 @@ export function LaborAllocationsAdjustmentEdit({
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={visible}
|
||||
onOpenChange={(vis) => setVisible(vis)}
|
||||
visible={visible}
|
||||
onVisibleChange={(vis) => setVisible(vis)}
|
||||
content={overlay}
|
||||
trigger="click"
|
||||
>
|
||||
|
||||
@@ -117,7 +117,7 @@ export function NoteUpsertModalContainer({
|
||||
return (
|
||||
<Modal
|
||||
title={existingNote ? t("notes.actions.edit") : t("notes.actions.new")}
|
||||
open={visible}
|
||||
visible={visible}
|
||||
okText={t("general.actions.save")}
|
||||
onOk={() => {
|
||||
form.submit();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Dropdown } from "antd";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -21,21 +21,27 @@ export function NotesPresetButton({ bodyshop, form }) {
|
||||
form.setFieldsValue({ text: item.text });
|
||||
};
|
||||
|
||||
const menu = {
|
||||
style: {
|
||||
columnCount: Math.floor(bodyshop.md_notes_presets.length / 10) + 1,
|
||||
},
|
||||
items: bodyshop.md_notes_presets.map((i, idx) => ({
|
||||
key: idx,
|
||||
style: { breakInside: "avoid" },
|
||||
onClick: () => handleSelect(i),
|
||||
label: i.label,
|
||||
})),
|
||||
};
|
||||
const menu = (
|
||||
<Menu
|
||||
style={{
|
||||
columnCount: Math.floor(bodyshop.md_notes_presets.length / 10) + 1,
|
||||
}}
|
||||
>
|
||||
{bodyshop.md_notes_presets.map((i, idx) => (
|
||||
<Menu.Item
|
||||
onClick={() => handleSelect(i)}
|
||||
key={idx}
|
||||
style={{ breakInside: "avoid" }}
|
||||
>
|
||||
{i.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown trigger={["click"]} menu={menu}>
|
||||
<Dropdown trigger={["click"]} overlay={menu}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href="# "
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { Button, Form, notification } from "antd";
|
||||
import { Button, Form, notification, PageHeader } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -76,7 +76,7 @@ export function PartsOrderBackorderEta({
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
content={popContent}
|
||||
open={visibility}
|
||||
visible={visibility}
|
||||
disabled={disabled}
|
||||
>
|
||||
<DateFormatter>{backordered_eta}</DateFormatter>
|
||||
|
||||
@@ -92,7 +92,7 @@ export function PartsOrderLineBackorderButton({
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
content={popContent}
|
||||
open={visibility}
|
||||
visible={visibility}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button loading={loading} onClick={handlePopover}>
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Drawer,
|
||||
Grid,
|
||||
Input,
|
||||
PageHeader,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Table,
|
||||
} from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -461,7 +471,7 @@ export function PartsOrderListTableComponent({
|
||||
<Drawer
|
||||
placement="right"
|
||||
onClose={() => handleOnRowClick(null)}
|
||||
open={selectedpartsorder}
|
||||
visible={selectedpartsorder}
|
||||
closable
|
||||
width={drawerPercentage}
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user