Compare commits

...

95 Commits

Author SHA1 Message Date
Patrick Fic
677c649ad8 IO-1612 Disable popover on PTO 2022-01-07 10:06:02 -08:00
Patrick Fic
adc48f50d7 IO-1622 Resolve CDK Export missing option 2022-01-07 10:01:09 -08:00
Patrick Fic
05fb2cd7a1 Further scheduling updates. 2022-01-07 09:54:43 -08:00
Patrick Fic
72c1ec74a5 IO-1623 Production priority column width increase. 2022-01-06 14:26:33 -08:00
Patrick Fic
ade92d86aa IO-347 Manual appointments. 2022-01-06 10:13:49 -08:00
Patrick Fic
fbd9fce230 IO-1604 Move inoice final note to notes screen. 2022-01-05 19:13:14 -08:00
Patrick Fic
172860b280 IO-1618 Prevent PAE posting of bills. 2022-01-05 13:56:15 -08:00
Patrick Fic
50926547b3 IO-1612 Add employee vacation tracking. 2022-01-05 11:45:37 -08:00
Patrick Fic
6457acc61e IO-1613 add suspended days to jobs. 2022-01-04 12:33:20 -08:00
Patrick Fic
fc023367cc IO-1608 Remove first name from parts order. 2022-01-03 13:05:47 -08:00
Patrick Fic
e06fded73c IO-1604 Add invoice final note to job close. 2022-01-03 10:49:01 -08:00
Patrick Fic
ee6b91d6ce IO-1591 Key tag label. 2022-01-03 09:16:22 -08:00
Patrick Fic
db69d2bf23 IO-1608 add name to parts order. 2022-01-03 09:15:46 -08:00
Patrick Fic
03ee20e2b2 IO-1591 Scheduling Modal hours showing as null. 2022-01-03 09:06:35 -08:00
Patrick Fic
0e2be2012e Merged in release/2021-12-31 (pull request #332)
Release/2021 12 31
2022-01-01 16:06:10 +00:00
Patrick Fic
93ea09fb19 Merge branch 'hotfix/2021-12-29' into release/2021-12/31 2021-12-29 22:03:47 -08:00
Patrick Fic
4d836a87ac Merged in hotfix/2021-12-29 (pull request #330)
Expose in service date for CDK.

Approved-by: Patrick Fic
2021-12-30 05:56:00 +00:00
Patrick Fic
ebde2516b7 Expose in service date for CDK. 2021-12-29 21:55:02 -08:00
Patrick Fic
817d1c99c7 Merged in hotfix/2021-12-29 (pull request #328)
Cherrypick CDK Fixes.

Approved-by: Patrick Fic
2021-12-30 04:06:46 +00:00
Patrick Fic
b29903c644 Cherrypick CDK Fixes. 2021-12-29 20:05:26 -08:00
Patrick Fic
454f69b511 IO-1605 Refactor smart scheduling. 2021-12-29 17:50:17 -06:00
Patrick Fic
e0d74aecb3 Updated CDK job export to pick vehicle with ID. 2021-12-29 14:23:37 -08:00
Patrick Fic
072650cf94 Updates to CDK job export for existing customers & incorrect phone. 2021-12-28 15:05:25 -08:00
Patrick Fic
fb92aae8cd Merged in hotfix/2021-12-28 (pull request #324)
hotfix/2021-12-28

Approved-by: Patrick Fic
2021-12-28 22:17:25 +00:00
Patrick Fic
93e2bfdc96 Merged in hotfix/2021-12-28 (pull request #322)
Resolve CDK generic customer.

Approved-by: Patrick Fic
2021-12-28 19:24:52 +00:00
Patrick Fic
04e3f2f44f Resolve CDK generic customer. 2021-12-28 11:24:24 -08:00
Patrick Fic
31c81543ad Update home page. 2021-12-28 10:39:13 -08:00
Patrick Fic
c27e8b9194 IO-1574 Add ignore blocked days to scoreboard. 2021-12-28 10:26:41 -08:00
Patrick Fic
1218deeee7 IO-1600 Number of rental days on contract list. 2021-12-28 10:06:01 -08:00
Patrick Fic
6422b13c1a IO-1257 Accept special characters on upload. 2021-12-28 09:54:31 -08:00
Patrick Fic
5ef1530d37 IO-1542 Automatically set next contact date on intake. 2021-12-28 09:42:23 -08:00
Patrick Fic
4a46bdb9e8 IO-1578 Add employee sorting to prod board. 2021-12-28 09:17:16 -08:00
Patrick Fic
8a42a995f7 IO-1591 added labels to print center. 2021-12-27 13:12:45 -08:00
Patrick Fic
ce513bb178 IO-1602 Add customer portions to CDK posting screen. 2021-12-27 12:40:52 -08:00
Patrick Fic
d0fe352d95 IO-1584 Updated Bill Posting WIP. 2021-12-27 12:25:32 -08:00
Patrick Fic
ffa3c54867 Merged in release/2021-12-23 (pull request #319)
Release/2021 12 23
2021-12-27 16:23:46 +00:00
Patrick Fic
38f190b8a5 Merged in release/2021-12-23 (pull request #318)
release/2021-12-23

Approved-by: Patrick Fic
2021-12-24 17:43:45 +00:00
Patrick Fic
f2c7c9b7ad Remove CI warning. 2021-12-24 09:43:24 -08:00
Patrick Fic
72e0dd9834 Merged in release/2021-12-23 (pull request #317)
IO-1590 Production by Technician IO-1585 File access api

Approved-by: Patrick Fic
2021-12-24 17:32:10 +00:00
Patrick Fic
b67b4c993e IO-1585 Remove unused function. 2021-12-24 09:31:50 -08:00
Patrick Fic
7abc3976e8 IO-1590 Production by Technician IO-1585 File access api 2021-12-24 09:30:26 -08:00
Patrick Fic
3f2f7c1182 Merged in release/2021-12-23 (pull request #316)
release/2021-12-23

Approved-by: Patrick Fic
2021-12-23 01:26:02 +00:00
Patrick Fic
74c6d32849 IO-1596 Add Envelope to print center. 2021-12-22 16:38:10 -08:00
Patrick Fic
76a00794f1 Improe chat response handling for archive/tagging. 2021-12-20 14:46:57 -08:00
Patrick Fic
60c9fcab2d Merged in release/2021-12-17 (pull request #315)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-17 23:54:19 +00:00
Patrick Fic
b8e7c8f341 Merged in release/2021-12-17 (pull request #314)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-17 19:09:27 +00:00
Patrick Fic
8d162f297a Allow negative job line split. 2021-12-17 11:01:57 -08:00
Patrick Fic
6f63fd9a5a Merged in release/2021-12-17 (pull request #313)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-17 17:56:25 +00:00
Patrick Fic
3247de6cf6 IO-1569 Change image selector color. 2021-12-17 09:55:12 -08:00
Patrick Fic
f42ef0749a IO-244 IOU updates. 2021-12-17 09:54:47 -08:00
Patrick Fic
7032975709 IO-1552 Autohouse Validation 2021-12-17 08:35:07 -08:00
Patrick Fic
7362d1b9db IO-1570 Update clear of line status. 2021-12-17 08:32:49 -08:00
Patrick Fic
6bee702770 Merged in release/2021-12-17 (pull request #312)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-16 17:05:18 +00:00
Patrick Fic
99c6ea30a9 Resolve unsub issue. 2021-12-16 09:04:15 -08:00
Patrick Fic
22829fb649 Merged in release/2021-12-17 (pull request #311)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-16 01:07:30 +00:00
Patrick Fic
0ba5553f77 Update autohouse query timeline. 2021-12-15 14:38:31 -08:00
Patrick Fic
e46d5b833b IO-244 Resolve IOU 2021-12-15 14:34:24 -08:00
Patrick Fic
1b8a76d5e0 IO-1552 Autohouse Validation Updates 2021-12-15 14:31:16 -08:00
Patrick Fic
20d448c6eb Merged in release/2021-12-17 (pull request #310)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-15 21:35:32 +00:00
Patrick Fic
2e2c780671 Regenerated lock file. 2021-12-15 13:35:08 -08:00
Patrick Fic
6a24143a2b Merged in release/2021-12-17 (pull request #309)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-15 21:32:07 +00:00
Patrick Fic
f6adfb60e4 Remove package lock for CI. 2021-12-15 13:31:47 -08:00
Patrick Fic
447bc45466 Merged in release/2021-12-17 (pull request #308)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-15 21:29:08 +00:00
Patrick Fic
fa878f8155 Cypress testing items. 2021-12-15 13:28:39 -08:00
Patrick Fic
0738b9e1c8 Updated CORS for Test. 2021-12-15 13:23:41 -08:00
Patrick Fic
3945afc0dd CDK updates. 2021-12-15 13:23:00 -08:00
Patrick Fic
5b439e1949 IO-1570 Allow clear of parts status. 2021-12-14 16:11:16 -08:00
Patrick Fic
70094dad99 Merged in release/2021-12-17 (pull request #307)
Re-add Craco & Cypress

Approved-by: Patrick Fic
2021-12-15 00:00:52 +00:00
Patrick Fic
f6080a1d38 Re-add Craco & Cypress 2021-12-14 16:00:31 -08:00
Patrick Fic
5b6d3d2f71 Merged in release/2021-12-17 (pull request #306)
Circle CI Update

Approved-by: Patrick Fic
2021-12-14 22:57:43 +00:00
Patrick Fic
25f06ce713 Circle CI Update 2021-12-14 14:57:23 -08:00
Patrick Fic
1e9aa912f2 Merged in release/2021-12-17 (pull request #302)
release/2021-12-17

Approved-by: Patrick Fic
2021-12-14 21:19:54 +00:00
Patrick Fic
127351249f Undo updated node version for server. 2021-12-14 13:15:14 -08:00
Patrick Fic
94c4b07318 Updated apollo fetch policies to always be network only. 2021-12-14 13:14:21 -08:00
Patrick Fic
920c8f6910 IO-1523 Long production view name. 2021-12-14 10:28:41 -08:00
Patrick Fic
8691ff05e1 IO-1554 Paginate active jobs list. 2021-12-14 10:20:20 -08:00
Patrick Fic
ca846b02e0 IO-1459 DMS Export marks bills as exported. 2021-12-14 09:54:46 -08:00
Patrick Fic
91e70750d6 IO-244 IOU Bug Fixes 2021-12-14 09:48:11 -08:00
Patrick Fic
273456d1fd IO-1560 Remove chrome auto populate for field. 2021-12-13 14:36:47 -08:00
Patrick Fic
3f5b5f6911 Merged in release/2021-12-17 (pull request #301)
Release/2021 12 17
2021-12-13 22:34:16 +00:00
Patrick Fic
3c9f66c95c IO-1524 Resolve CC form create issue with decimals. 2021-12-13 13:54:07 -08:00
Patrick Fic
e052a0d232 IO-1568 Force render manual job creation panels. 2021-12-13 13:26:47 -08:00
Patrick Fic
a933a1d587 IO-1572 Search bill lines by act price. 2021-12-13 13:22:02 -08:00
Patrick Fic
28cc092bb7 IO-1551 Include outbound fcm to update timestamps. 2021-12-13 12:58:56 -08:00
Patrick Fic
3b756beba6 IO-244 IOU updates to remove act price. 2021-12-13 12:32:10 -08:00
Patrick Fic
f01cff1c6d Merged in release/2021-12-10 (pull request #300)
Release/2021 12 10
2021-12-10 21:51:05 +00:00
Patrick Fic
1ae942988c Merged in release/2021-12-10 (pull request #299)
release/2021-12-10

Approved-by: Patrick Fic
2021-12-10 03:53:02 +00:00
Patrick Fic
4b5129e9cb Merged in release/2021-12-10 (pull request #298)
release/2021-12-10

Approved-by: Patrick Fic
2021-12-09 22:23:54 +00:00
Patrick Fic
e624e257ff Merged in release/2021-12-10 (pull request #297)
release/2021-12-10

Approved-by: Patrick Fic
2021-12-09 02:08:36 +00:00
Patrick Fic
358365072d Merged in release/2021-12-10 (pull request #296)
release/2021-12-10

Approved-by: Patrick Fic
2021-12-09 01:40:21 +00:00
Patrick Fic
eba50e3f9c Merged in release/2021-12-10 (pull request #295)
release/2021-12-10

Approved-by: Patrick Fic
2021-12-09 01:06:34 +00:00
Patrick Fic
5f47a2cb6c Resolve Report Center Issue.
(cherry picked from commit 76ea8ca2ed)
2021-12-08 15:01:46 -08:00
Patrick Fic
b37d8ae29d Merged in release/2021-12-10 (pull request #293)
release/2021-12-10

Approved-by: Patrick Fic
2021-12-08 21:18:27 +00:00
Patrick Fic
fd621e49c4 Merged in release/2021-12-10 (pull request #291)
release/2021-12-10

Approved-by: Patrick Fic
2021-12-08 17:26:21 +00:00
Patrick Fic
7522c4707e Merged in release/2021-12-10 (pull request #290)
release/2021-12-10

Approved-by: Patrick Fic
2021-12-07 22:14:58 +00:00
213 changed files with 7665 additions and 2295 deletions

View File

@@ -16,5 +16,6 @@
"rules": {
"no-console": "off"
},
"settings": {}
"settings": {},
"plugins": ["cypress"]
}

12
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"xml.fileAssociations": [
{
"pattern": "**/Test.xml",
"systemId": "file:///Users/pfic/Downloads/IMEX.xsd"
},
{
"pattern": "**/IMEX.xml",
"systemId": "logs/IMEX.xsd"
}
]
}

View File

@@ -44,7 +44,10 @@ const JobsList = (props) => (
);
const JobsFilter = (props) => {
const { loading, error, data } = useQuery(QUERY_ALL_SHOPS);
const { loading, error, data } = useQuery(QUERY_ALL_SHOPS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
if (loading) return <CircularProgress />;
if (error) return JSON.stringify(error);

View File

@@ -500,6 +500,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>end</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>note</name>
<definition_loaded>false</definition_loaded>
@@ -521,6 +542,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>start</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>time</name>
<definition_loaded>false</definition_loaded>
@@ -673,6 +715,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dataconsistency</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>history</name>
<definition_loaded>false</definition_loaded>
@@ -715,6 +778,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>manualevent</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>noarrivingjobs</name>
<definition_loaded>false</definition_loaded>
@@ -4000,6 +4084,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ignoreblockeddays</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>inhousevendorid</name>
<definition_loaded>false</definition_loaded>
@@ -4045,6 +4150,27 @@
<folder_node>
<name>intake</name>
<children>
<concept_node>
<name>next_contact_hours</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>templates</name>
<definition_loaded>false</definition_loaded>
@@ -9990,6 +10116,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>length</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>localtax</name>
<definition_loaded>false</definition_loaded>
@@ -13120,6 +13267,27 @@
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>addvacation</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>new</name>
<definition_loaded>false</definition_loaded>
@@ -13508,6 +13676,74 @@
</translation>
</translations>
</concept_node>
<folder_node>
<name>vacation</name>
<children>
<concept_node>
<name>end</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>length</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>start</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>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
@@ -13534,6 +13770,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>endmustbeafterstart</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>flat_rate</name>
<definition_loaded>false</definition_loaded>
@@ -14010,6 +14267,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>print</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>refresh</name>
<definition_loaded>false</definition_loaded>
@@ -20474,6 +20752,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>inservicedate</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>journal</name>
<definition_loaded>false</definition_loaded>
@@ -21326,6 +21625,27 @@
</concept_node>
</children>
</folder_node>
<concept_node>
<name>invoice_final_note</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>kmin</name>
<definition_loaded>false</definition_loaded>
@@ -26778,6 +27098,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>suspended</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>suspense</name>
<definition_loaded>false</definition_loaded>
@@ -33713,6 +34054,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>envelope_return_address</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>estimate</name>
<definition_loaded>false</definition_loaded>
@@ -33860,6 +34222,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>folder_label_multiple</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>glass_express_checklist</name>
<definition_loaded>false</definition_loaded>
@@ -34049,6 +34432,74 @@
</translation>
</translations>
</concept_node>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>count</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>labels</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>position</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>
</children>
</folder_node>
<concept_node>
<name>mpi_animal_checklist</name>
<definition_loaded>false</definition_loaded>
@@ -35328,6 +35779,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>suspend</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>unsuspend</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>
</children>
</folder_node>
<folder_node>
@@ -37619,6 +38112,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_by_technician</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>purchases_by_cost_center_detail</name>
<definition_loaded>false</definition_loaded>

8
client/cypress.json Normal file
View File

@@ -0,0 +1,8 @@
{
"baseUrl": "http://localhost:3000",
"experimentalStudio": true,
"env": {
"FIREBASE_USERNAME": "cypress@imex.test",
"FIREBASE_PASSWORD": "cypress"
}
}

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,5 @@
{
"id": 8739,
"name": "Jane",
"email": "jane@example.com"
}

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,23 @@
/// <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 ==== */
});
});

View File

@@ -0,0 +1,143 @@
/// <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')
})
})
})

View File

@@ -0,0 +1,299 @@
/// <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 })
})
})

View File

@@ -0,0 +1,39 @@
/// <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)
})
})

View File

@@ -0,0 +1,177 @@
/// <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-elements-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)
})
})
})
})

View File

@@ -0,0 +1,97 @@
/// <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)
})
})
})
})

View File

@@ -0,0 +1,77 @@
/// <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')
})
})

View File

@@ -0,0 +1,202 @@
/// <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'])
})
})

View File

@@ -0,0 +1,88 @@
/// <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')
})
})
})

View File

@@ -0,0 +1,52 @@
/// <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')
})
})
})

View File

@@ -0,0 +1,32 @@
/// <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')
})
})

View File

@@ -0,0 +1,104 @@
/// <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')
})
})

View File

@@ -0,0 +1,56 @@
/// <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
},
})
})
})

View File

@@ -0,0 +1,163 @@
/// <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)
})
})

View File

@@ -0,0 +1,114 @@
/// <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()
})
})
})

View File

@@ -0,0 +1,205 @@
/// <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))
})
})

View File

@@ -0,0 +1,121 @@
/// <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)
})
})

View File

@@ -0,0 +1,110 @@
/// <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
})
})
})
})

View File

@@ -0,0 +1,59 @@
/// <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)
})
})

View File

@@ -0,0 +1,31 @@
/// <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])
})
})

View File

@@ -0,0 +1,22 @@
/// <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')
})
})

View File

@@ -0,0 +1,22 @@
/// <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
}

View File

@@ -0,0 +1,27 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import "@testing-library/cypress/add-commands";

View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
"types": ["cypress"]
},
"include": ["**/*.*"]
}

View File

@@ -1,24 +1,20 @@
{
"name": "bodyshop",
"version": "0.1.1",
"version": "0.2.1",
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@apollo/client": "^3.4.17",
"@apollo/client": "^3.5.6",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.0",
"@fingerprintjs/fingerprintjs": "^3.3.0",
"@openreplay/tracker": "^3.4.7",
"@openreplay/tracker-assist": "^3.4.4",
"@openreplay/tracker-graphql": "^3.0.0",
"@openreplay/tracker-redux": "^3.0.0",
"@sentry/react": "^6.14.3",
"@sentry/tracing": "^6.14.3",
"@craco/craco": "^6.4.3",
"@fingerprintjs/fingerprintjs": "^3.3.1",
"@sentry/react": "^6.16.1",
"@sentry/tracing": "^6.16.1",
"@splitsoftware/splitio-react": "^1.3.0",
"@stripe/react-stripe-js": "^1.6.0",
"@stripe/stripe-js": "^1.21.1",
"@stripe/react-stripe-js": "^1.7.0",
"@stripe/stripe-js": "^1.22.0",
"@tanem/react-nprogress": "^3.0.82",
"antd": "^4.16.13",
"antd": "^4.17.4",
"apollo-link-logger": "^2.0.0",
"axios": "^0.24.0",
"craco-less": "^1.20.0",
@@ -27,79 +23,80 @@
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^9.4.1",
"graphql": "^16.0.1",
"i18next": "^21.4.2",
"firebase": "^9.6.1",
"graphql": "^16.2.0",
"i18next": "^21.6.3",
"i18next-browser-languagedetector": "^6.1.2",
"jsoneditor": "^9.5.7",
"jsoneditor": "^9.5.8",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.9.42",
"logrocket": "^2.1.1",
"markerjs2": "^2.17.0",
"libphonenumber-js": "^1.9.44",
"logrocket": "^2.1.2",
"markerjs2": "^2.17.2",
"moment-business-days": "^1.2.0",
"phone": "^3.1.9",
"phone": "^3.1.10",
"preval.macro": "^5.0.0",
"prop-types": "^15.7.2",
"query-string": "^7.0.1",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^17.0.1",
"react-big-calendar": "^0.38.1",
"react": "^17.0.2",
"react-big-calendar": "^0.38.2",
"react-color": "^2.19.3",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.1",
"react-dom": "^17.0.2",
"react-drag-listview": "^0.1.8",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.3.0",
"react-i18next": "^11.14.2",
"react-i18next": "^11.15.1",
"react-icons": "^4.3.1",
"react-number-format": "^4.8.0",
"react-number-format": "^4.9.0",
"react-redux": "^7.2.6",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0",
"react-scripts": "^4.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3",
"recharts": "^2.1.6",
"recharts": "^2.1.8",
"redux": "^4.1.2",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.2",
"reselect": "^4.1.2",
"sass": "^1.43.4",
"socket.io-client": "^4.3.2",
"reselect": "^4.1.5",
"sass": "^1.45.0",
"socket.io-client": "^4.4.0",
"styled-components": "^5.3.3",
"subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^2.1.2",
"workbox-background-sync": "^6.3.0",
"workbox-broadcast-update": "^6.3.0",
"workbox-cacheable-response": "^6.3.0",
"workbox-core": "^6.3.0",
"workbox-expiration": "^6.3.0",
"workbox-google-analytics": "^6.3.0",
"workbox-navigation-preload": "^6.3.0",
"workbox-precaching": "^6.3.0",
"workbox-range-requests": "^6.3.0",
"workbox-routing": "^6.3.0",
"workbox-strategies": "^6.3.0",
"workbox-streams": "^6.3.0"
"workbox-background-sync": "^6.4.2",
"workbox-broadcast-update": "^6.4.2",
"workbox-cacheable-response": "^6.4.2",
"workbox-core": "^6.4.2",
"workbox-expiration": "^6.4.2",
"workbox-google-analytics": "^6.4.2",
"workbox-navigation-preload": "^6.4.2",
"workbox-precaching": "^6.4.2",
"workbox-range-requests": "^6.4.2",
"workbox-routing": "^6.4.2",
"workbox-strategies": "^6.4.2",
"workbox-streams": "^6.4.2",
"yauzl": "^2.10.0"
},
"scripts": {
"postinstall": "patch-package",
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco 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` react-scripts build",
"test": "craco test",
"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 ."
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
"react-app/jest",
"plugin:cypress/recommended"
]
},
"browserslist": {
@@ -114,9 +111,15 @@
"last 1 safari version"
]
},
"resolutions": {
"react-error-overlay": "6.0.9"
},
"devDependencies": {
"@sentry/webpack-plugin": "^1.18.3",
"patch-package": "^6.4.7",
"@testing-library/cypress": "^8.0.2",
"cypress": "^9.1.1",
"eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.9",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.2"
}

View File

@@ -134,3 +134,11 @@
background: unset;
}
}
.ReactGridGallery_tile-icon-bar {
div {
svg {
fill: #1890ff;
}
}
}

View File

@@ -110,13 +110,6 @@ export function AccountingReceivablesTableComponent({
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) => {
return record.clm_no ? (
<span>{record.clm_no}</span>
) : (
t("general.labels.unknown")
);
},
},
{
title: t("jobs.fields.clm_total"),
@@ -126,11 +119,7 @@ export function AccountingReceivablesTableComponent({
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => {
return record.clm_total ? (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
) : (
t("general.labels.unknown")
);
return <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>;
},
},
{

View File

@@ -9,6 +9,7 @@ export default function AuditTrailListContainer({ recordId }) {
const { loading, error, data } = useQuery(QUERY_AUDIT_TRAIL, {
variables: { id: recordId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
logImEXEvent("audittrail_view", { recordId });

View File

@@ -77,6 +77,8 @@ export function BillDetailEditcontainer({
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: { billid: search.billid },
skip: !!!search.billid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleSave = () => {

View File

@@ -0,0 +1,135 @@
import { Form, Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
import BillFormItemsExtendedFormItem from "./bill-form-lines.extended.formitem.component";
export default function BillFormLinesExtended({
lineData,
discount,
form,
responsibilityCenters,
disabled,
}) {
const [search, setSearch] = useState("");
const { t } = useTranslation();
const columns = [
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
width: "10%",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
width: "10%",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
width: "10%",
filters: [
{
text: t("jobs.labels.partsfilter"),
value: ["PAN", "PAP", "PAL", "PAA", "PAS", "PASL"],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN", "PAP"],
},
{
text: t("joblines.fields.part_types.PAL"),
value: ["PAL"],
},
{
text: t("joblines.fields.part_types.PAA"),
value: ["PAA"],
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS", "PASL"],
},
],
onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
width: "10%",
sorter: (a, b) => a.act_price - b.act_price,
shouldCellUpdate: false,
render: (text, record) => (
<>
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
{record.part_qty ? `(x ${record.part_qty})` : null}
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
<span
style={{ marginLeft: ".2rem" }}
>{`(${record.prt_dsmk_p}%)`}</span>
) : (
<></>
)}
</>
),
},
{
title: t("billlines.fields.posting"),
dataIndex: "posting",
key: "posting",
render: (text, record, index) => (
<Form.Item noStyle name={["billlineskeys", record.id]}>
<BillFormItemsExtendedFormItem
form={form}
record={record}
index={index}
responsibilityCenters={responsibilityCenters}
discount={discount}
/>
</Form.Item>
),
},
];
const data =
search === ""
? lineData
: lineData.filter(
(l) =>
(l.line_desc &&
l.line_desc.toLowerCase().includes(search.toLowerCase())) ||
(l.oem_partno &&
l.oem_partno.toLowerCase().includes(search.toLowerCase())) ||
(l.act_price &&
l.act_price.toString().startsWith(search.toString()))
);
return (
<Form.Item noStyle name="billlineskeys">
<button onClick={() => console.log(form.getFieldsValue())}>form</button>
<Input onChange={(e) => setSearch(e.target.value)} allowClear />
<Table
pagination={false}
size="small"
columns={columns}
rowKey="id"
dataSource={data}
/>
</Form.Item>
);
}

View File

@@ -0,0 +1,288 @@
import React from "react";
import {
PlusCircleFilled,
MinusCircleFilled,
WarningOutlined,
} from "@ant-design/icons";
import { Form, Button, InputNumber, Input, Select, Switch, Space } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import CiecaSelect from "../../utils/Ciecaselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(BillFormItemsExtendedFormItem);
export function BillFormItemsExtendedFormItem({
value,
bodyshop,
form,
record,
index,
disabled,
responsibilityCenters,
discount,
}) {
// const { billlineskeys } = form.getFieldsValue("billlineskeys");
const { t } = useTranslation();
if (!value)
return (
<Button
onClick={() => {
const values = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
...values,
billlineskeys: {
...(values.billlineskeys || {}),
[record.id]: {
joblineid: record.id,
line_desc: record.line_desc,
quantity: record.part_qty || 1,
actual_price: record.act_price,
cost_center: record.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? record.part_type
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[record.part_type] ||
null)
: null,
},
},
});
}}
>
<PlusCircleFilled />
</Button>
);
return (
<Space wrap>
<Form.Item
label={t("billlines.fields.line_desc")}
name={["billlineskeys", record.id, "line_desc"]}
>
<Input disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.quantity")}
name={["billlineskeys", record.id, "quantity"]}
>
<InputNumber precision={0} min={0} disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.actual_price")}
name={["billlineskeys", record.id, "actual_price"]}
>
<CurrencyInput
min={0}
disabled={disabled}
onBlur={(e) => {
const { billlineskeys } = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
billlineskeys: {
...billlineskeys,
[record.id]: {
...billlineskeys[billlineskeys],
actual_cost: !!billlineskeys[billlineskeys].actual_cost
? billlineskeys[billlineskeys].actual_cost
: Math.round(
(parseFloat(e.target.value) * (1 - discount) +
Number.EPSILON) *
100
) / 100,
},
},
});
}}
/>
</Form.Item>
<Form.Item
label={t("billlines.fields.actual_cost")}
name={["billlineskeys", record.id, "actual_cost"]}
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const line = value;
if (!!!line) return null;
const lineDiscount = (
1 -
Math.round((line.actual_cost / line.actual_price) * 100) / 100
).toPrecision(2);
if (lineDiscount - discount === 0) return <div />;
return <WarningOutlined style={{ color: "red" }} />;
}}
</Form.Item>
<Form.Item
label={t("billlines.fields.cost_center")}
name={["billlineskeys", record.id, "cost_center"]}
>
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
? CiecaSelect(true, false)
: responsibilityCenters.costs.map((item) => (
<Select.Option key={item.name}>{item.name}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("billlines.fields.location")}
name={["billlineskeys", record.id, "location"]}
>
<Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("billlines.fields.deductedfromlbr")}
name={["billlineskeys", record.id, "deductedfromlbr"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
{() => {
if (
form.getFieldsValue("billlineskeys").billlineskeys[record.id]
.deductedfromlbr
)
return (
<div>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"billlineskeys",
record.id,
"lbr_adjustment",
"mod_lbr_ty",
]}
>
<Select allowClear>
<Select.Option value="LAA">
{t("joblines.fields.lbr_types.LAA")}
</Select.Option>
<Select.Option value="LAB">
{t("joblines.fields.lbr_types.LAB")}
</Select.Option>
<Select.Option value="LAD">
{t("joblines.fields.lbr_types.LAD")}
</Select.Option>
<Select.Option value="LAE">
{t("joblines.fields.lbr_types.LAE")}
</Select.Option>
<Select.Option value="LAF">
{t("joblines.fields.lbr_types.LAF")}
</Select.Option>
<Select.Option value="LAG">
{t("joblines.fields.lbr_types.LAG")}
</Select.Option>
<Select.Option value="LAM">
{t("joblines.fields.lbr_types.LAM")}
</Select.Option>
<Select.Option value="LAR">
{t("joblines.fields.lbr_types.LAR")}
</Select.Option>
<Select.Option value="LAS">
{t("joblines.fields.lbr_types.LAS")}
</Select.Option>
<Select.Option value="LAU">
{t("joblines.fields.lbr_types.LAU")}
</Select.Option>
<Select.Option value="LA1">
{t("joblines.fields.lbr_types.LA1")}
</Select.Option>
<Select.Option value="LA2">
{t("joblines.fields.lbr_types.LA2")}
</Select.Option>
<Select.Option value="LA3">
{t("joblines.fields.lbr_types.LA3")}
</Select.Option>
<Select.Option value="LA4">
{t("joblines.fields.lbr_types.LA4")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
label={t("jobs.labels.adjustmentrate")}
name={["billlineskeys", record.id, "lbr_adjustment", "rate"]}
initialValue={bodyshop.default_adjustment_rate}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber precision={2} min={0.01} />
</Form.Item>
</div>
);
return <></>;
}}
</Form.Item>
<Form.Item
label={t("billlines.fields.federal_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "federal"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.state_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "state"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Form.Item
label={t("billlines.fields.local_tax_applicable")}
name={["billlineskeys", record.id, "applicable_taxes", "local"]}
valuePropName="checked"
>
<Switch disabled={disabled} />
</Form.Item>
<Button
onClick={() => {
const values = form.getFieldsValue("billlineskeys");
form.setFieldsValue({
...values,
billlineskeys: {
...(values.billlineskeys || {}),
[record.id]: null,
},
});
}}
>
<MinusCircleFilled />
</Button>
</Space>
);
}

View File

@@ -28,6 +28,8 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility";
import { useTreatments } from "@splitsoftware/splitio-react";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -49,7 +51,11 @@ export function BillFormComponent({
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const { Extended_Bill_Posting } = useTreatments(
["Extended_Bill_Posting"],
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
};
@@ -357,13 +363,24 @@ export function BillFormComponent({
</Form.Item>
</LayoutFormRow>
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
{Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
) : (
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
)}
<Form.Item
name="upload"

View File

@@ -18,7 +18,10 @@ export function BillFormContainer({
disabled,
disableInvNumber,
}) {
const { data: VendorAutoCompleteData } = useQuery(SEARCH_VENDOR_AUTOCOMPLETE);
const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE,
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
);
const [loadLines, { data: lineData }] = useLazyQuery(
GET_JOB_LINES_TO_ENTER_BILL

View File

@@ -7,7 +7,7 @@ import {
Select,
Space,
Switch,
Table
Table,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -74,7 +74,9 @@ export function BillEnterModalLinesComponent({
actual_price: opt.cost,
cost_center: opt.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? opt.part_type
? opt.part_type !== "PAE"
? opt.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[
opt.part_type

View File

@@ -20,7 +20,11 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
.toLowerCase()
.includes(inputValue.toLowerCase())) ||
(option.oem_partno &&
option.oem_partno.toLowerCase().includes(inputValue.toLowerCase()))
option.oem_partno
.toLowerCase()
.includes(inputValue.toLowerCase())) ||
(option.act_price &&
option.act_price.toString().startsWith(inputValue.toString()))
);
}}
notFoundContent={"Removed."}
@@ -40,13 +44,14 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
line_desc={item.line_desc}
part_qty={item.part_qty}
oem_partno={item.oem_partno}
act_price={item.act_price}
style={{
...(item.removed ? { textDecoration: "line-through" } : {}),
}}
>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}`}
}${item.act_price ? ` - $${item.act_price}` : ``}`}
</Option>
))
: null}

View File

@@ -12,7 +12,10 @@ export default function BillsVendorsList() {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS);
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { t } = useTranslation();

View File

@@ -13,6 +13,7 @@ export default function ChatArchiveButton({ conversation }) {
await updateConversation({
variables: { id: conversation.id, archived: !conversation.archived },
refetchQueries: ["CONVERSATION_LIST_QUERY"],
});
setLoading(false);

View File

@@ -16,6 +16,16 @@ export default function ChatConversationTitleTags({ jobConversations }) {
conversationId: convId,
jobId: jobId,
},
update(cache) {
cache.modify({
id: cache.identify({ id: convId, __typename: "conversations" }),
fields: {
job_conversations(ex) {
return ex.filter((e) => e.jobid !== jobId);
},
},
});
},
});
logImEXEvent("messaging_remove_job_tag", {
conversationId: convId,

View File

@@ -25,6 +25,8 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
data: convoData,
} = useQuery(GET_CONVERSATION_DETAILS, {
variables: { conversationId: selectedConversation },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { loading, error, data } = useSubscription(

View File

@@ -27,12 +27,15 @@ export function ChatMediaSelector({
const [visible, setVisible] = useState(false);
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
jobId:
conversation.job_conversations[0] &&
conversation.job_conversations[0].jobid,
},
fetchPolicy: "network-only",
skip:
!visible ||
!conversation.job_conversations ||

View File

@@ -38,6 +38,8 @@ export function ChatPopupComponent({
const { t } = useTranslation();
const [pollInterval, setpollInterval] = useState(0);
const { loading, data, refetch, called } = useQuery(CONVERSATION_LIST_QUERY, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
...(pollInterval > 0 ? { pollInterval } : {}),
});

View File

@@ -55,6 +55,7 @@ function ChatSendMessageComponent({
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id,
selectedMedia: selectedImages,
imexshopid: bodyshop.imexshopid,
});
setSelectedMedia(
selectedMedia.map((i) => {

View File

@@ -8,6 +8,8 @@ import ContractCarsComponent from "./contract-cars.component";
export default function ContractCarsContainer({ selectedCarState, form }) {
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, {
variables: { today: moment().format("YYYY-MM-DD") },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [selectedCar, setSelectedCar] = selectedCarState;

View File

@@ -53,7 +53,7 @@ export function ContractConvertToRo({
const billingLines = [];
if (contractLength > 0)
billingLines.push({
manual_line:true,
manual_line: true,
unq_seq: 1,
line_no: 1,
line_ref: 1,
@@ -71,7 +71,7 @@ export function ContractConvertToRo({
contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength;
if (mileageDiff > 0) {
billingLines.push({
manual_line:true,
manual_line: true,
unq_seq: 2,
line_no: 2,
line_ref: 2,
@@ -88,7 +88,7 @@ export function ContractConvertToRo({
if (values.refuelqty > 0) {
billingLines.push({
manual_line:true,
manual_line: true,
unq_seq: 3,
line_no: 3,
line_ref: 3,
@@ -104,7 +104,7 @@ export function ContractConvertToRo({
}
if (values.applyCleanupCharge) {
billingLines.push({
manual_line:true,
manual_line: true,
unq_seq: 4,
line_no: 4,
line_ref: 4,
@@ -121,7 +121,7 @@ export function ContractConvertToRo({
if (contract.damagewaiver) {
//Add for cleanup fee.
billingLines.push({
manual_line:true,
manual_line: true,
unq_seq: 5,
line_no: 5,
line_ref: 5,
@@ -153,10 +153,10 @@ export function ContractConvertToRo({
ownr_co_nm: contract.job.owner.ownr_co_nm,
ownr_ph1: contract.job.owner.ownr_ph1,
ownr_ea: contract.job.owner.ownr_ea,
v_model_desc: contract.job.vehicle.v_model_desc,
v_model_yr: contract.job.vehicle.v_model_yr,
v_make_desc: contract.job.vehicle.v_make_desc,
v_vin: contract.job.vehicle.v_vin,
v_model_desc: contract.job.vehicle && contract.job.vehicle.v_model_desc,
v_model_yr: contract.job.vehicle && contract.job.vehicle.v_model_yr,
v_make_desc: contract.job.vehicle && contract.job.vehicle.v_make_desc,
v_vin: contract.job.vehicle && contract.job.vehicle.v_vin,
status: bodyshop.md_ro_statuses.default_completed,
notes: {
data: [

View File

@@ -16,6 +16,8 @@ export function ContractJobsContainer({ selectedJobState, bodyshop }) {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [selectedJob, setSelectedJob] = selectedJobState;

View File

@@ -11,6 +11,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import moment from "moment";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
@@ -86,7 +87,9 @@ export function ContractsList({
} ${record.courtesycar.make} ${record.courtesycar.model}${
record.courtesycar.plate ? ` (${record.courtesycar.plate})` : ""
}${
record.courtesycar.fleetnumber ? ` (${record.courtesycar.fleetnumber})` : ""
record.courtesycar.fleetnumber
? ` (${record.courtesycar.fleetnumber})`
: ""
}`}</Link>
),
},
@@ -133,6 +136,19 @@ export function ContractsList({
<DateTimeFormatter>{record.actualreturn}</DateTimeFormatter>
),
},
{
title: t("contracts.fields.length"),
dataIndex: "length",
key: "length",
render: (text, record) =>
(record.actualreturn &&
record.start &&
`${moment(record.actualreturn)
.diff(moment(record.start), "days", true)
.toFixed(1)} days`) ||
"",
},
];
const handleTableChange = (pagination, filters, sorter) => {

View File

@@ -118,7 +118,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
},
]}
>
<InputNumber />
<InputNumber precision={0} />
</Form.Item>
<Form.Item
label={t("courtesycars.fields.fleetnumber")}

View File

@@ -31,7 +31,7 @@ export default function CourtesyCarReturnModalComponent() {
},
]}
>
<InputNumber />
<InputNumber precision={0} />
</Form.Item>
<Form.Item
label={t("contracts.fields.fuelin")}

View File

@@ -19,6 +19,8 @@ export default function CsiResponseFormContainer() {
id: responseid,
},
skip: !!!responseid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
useEffect(() => {

View File

@@ -59,7 +59,8 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
});
const { loading, error, data, refetch } = useQuery(
createDashboardQuery(state)
createDashboardQuery(state),
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
);
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);

View File

@@ -45,7 +45,7 @@ export function DmsCustomerSelector({ bodyshop }) {
const onUseGeneric = () => {
setVisible(false);
socket.emit(
`${dmsType}selected-customer`,
`${dmsType}-selected-customer`,
bodyshop.cdk_configuration.generic_customer_number
);
setSelectedCustomer(null);

View File

@@ -2,6 +2,7 @@ import { DeleteFilled, DownOutlined } from "@ant-design/icons";
import {
Button,
Card,
DatePicker,
Divider,
Dropdown,
Form,
@@ -24,6 +25,7 @@ import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -80,8 +82,10 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
initialValues={{
story: t("jobs.labels.dms.defaultstory", {
ro_number: job.ro_number,
area_of_damage: job.area_of_damage && job.area_of_damage.impact1,
area_of_damage:
(job.area_of_damage && job.area_of_damage.impact1) || "UNKNOWN",
}).substr(0, 239),
inservicedate: moment("2019-01-01"),
}}
>
<LayoutFormRow grow>
@@ -154,6 +158,12 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
>
<Input disabled />
</Form.Item>
<Form.Item
name="inservicedate"
label={t("jobs.fields.dms.inservicedate")}
>
<DatePicker format="MM/DD/YYYY" />
</Form.Item>
</LayoutFormRow>
<Space>
<DmsCdkMakes form={form} socket={socket} job={job} />
@@ -174,6 +184,22 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
</Form.Item>
<Divider />
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.fields.ded_amt")}
value={Dinero(
job.job_totals.totals.custPayable.deductible
).toFormat()}
/>
<Statistic
title={t("jobs.labels.total_cust_payable")}
value={Dinero(job.job_totals.totals.custPayable.total).toFormat()}
/>
<Statistic
title={t("jobs.labels.net_repairs")}
value={Dinero(job.job_totals.totals.net_repairs).toFormat()}
/>
</Space>
<Form.List name={["payers"]}>
{(fields, { add, remove }) => {
return (

View File

@@ -29,6 +29,7 @@ export function DocumentEditorContainer({ setBodyshop }) {
data: dataShop,
} = useQuery(QUERY_BODYSHOP, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
useEffect(() => {
@@ -38,6 +39,8 @@ export function DocumentEditorContainer({ setBodyshop }) {
const { loading, error, data } = useQuery(GET_DOCUMENT_BY_PK, {
variables: { documentId },
skip: !documentId,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
if (loading || loadingShop) return <LoadingSpinner />;

View File

@@ -21,9 +21,9 @@ export const handleUpload = (ev, context) => {
const fileName = ev.file.name || ev.filename;
let key = `${bodyshop.id}/${jobId}/${fileName.replace(
/\.[^/.]+$/,
""
let key = `${bodyshop.id}/${jobId}/${replaceAccents(fileName).replace(
/[^A-Z0-9]+/gi,
"_"
)}-${new Date().getTime()}`;
let extension = fileName.split(".").pop();
uploadToCloudinary(
@@ -187,3 +187,33 @@ export function DetermineFileType(filetype) {
return "auto";
}
function replaceAccents(str) {
// Verifies if the String has accents and replace them
if (str.search(/[\xC0-\xFF]/g) > -1) {
str = str
.replace(/[\xC0-\xC5]/g, "A")
.replace(/[\xC6]/g, "AE")
.replace(/[\xC7]/g, "C")
.replace(/[\xC8-\xCB]/g, "E")
.replace(/[\xCC-\xCF]/g, "I")
.replace(/[\xD0]/g, "D")
.replace(/[\xD1]/g, "N")
.replace(/[\xD2-\xD6\xD8]/g, "O")
.replace(/[\xD9-\xDC]/g, "U")
.replace(/[\xDD]/g, "Y")
.replace(/[\xDE]/g, "P")
.replace(/[\xE0-\xE5]/g, "a")
.replace(/[\xE6]/g, "ae")
.replace(/[\xE7]/g, "c")
.replace(/[\xE8-\xEB]/g, "e")
.replace(/[\xEC-\xEF]/g, "i")
.replace(/[\xF1]/g, "n")
.replace(/[\xF2-\xF6\xF8]/g, "o")
.replace(/[\xF9-\xFC]/g, "u")
.replace(/[\xFE]/g, "p")
.replace(/[\xFD\xFF]/g, "y");
}
return str;
}

View File

@@ -34,6 +34,8 @@ export function EmailDocumentsComponent({
jobId: emailConfig.jobid,
},
skip: !emailConfig.jobid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
return (

View File

@@ -1,5 +1,5 @@
import { useLazyQuery } from "@apollo/client";
import { AutoComplete, Divider, Input, Space } from "antd";
import { AutoComplete, Divider, Space } from "antd";
import _ from "lodash";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -11,8 +11,7 @@ import AlertComponent from "../alert/alert.component";
export default function GlobalSearch() {
const { t } = useTranslation();
const [callSearch, { loading, error, data }] =
useLazyQuery(GLOBAL_SEARCH_QUERY);
const [callSearch, { error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "") callSearch(v);
@@ -172,8 +171,7 @@ export default function GlobalSearch() {
options={options}
onSearch={handleSearch}
placeholder={t("general.labels.globalsearch")}
>
<Input.Search loading={loading} />
</AutoComplete>
allowClear
></AutoComplete>
);
}

View File

@@ -27,6 +27,8 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR,
{
skip: !isModalVisible,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
}
);

View File

@@ -27,6 +27,7 @@ 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 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";
@@ -66,7 +67,10 @@ export function ScheduleEventComponent({
const popoverContent = (
<div style={{ maxWidth: "40vw" }}>
{!event.isintake ? (
<strong>{event.title}</strong>
<Space>
<strong>{event.title}</strong>
<ScheduleEventColor event={event} />
</Space>
) : (
<Space>
<strong>{`${(event.job && event.job.ownr_fn) || ""} ${
@@ -118,7 +122,9 @@ export function ScheduleEventComponent({
</DataLabel>
<ScheduleEventNote event={event} />
</div>
) : null}
) : (
<div>{event.note || ""}</div>
)}
<Divider />
<Space wrap>
{event.job ? (
@@ -140,84 +146,89 @@ export function ScheduleEventComponent({
{t("appointments.actions.preview")}
</Button>
) : null}
<Dropdown
overlay={
<Menu>
<Menu.Item
onClick={() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id },
},
{
to: event.job && event.job.ownr_ea,
subject: Template.subject,
},
"e",
event.job && event.job.id
);
}}
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({
phone_num: p.formatInternational(),
jobid: event.job.id,
});
setMessage(
t("appointments.labels.reminder", {
shopname: bodyshop.shopname,
date: moment(event.start).format("MM/DD/YYYY"),
time: moment(event.start).format("HH:mm a"),
})
{event.job ? (
<Dropdown
overlay={
<Menu>
<Menu.Item
onClick={() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id },
},
{
to: event.job && event.job.ownr_ea,
subject: Template.subject,
},
"e",
event.job && event.job.id
);
setVisible(false);
} else {
notification["error"]({
message: t("messaging.error.invalidphone"),
});
}
}}
disabled={event.arrived || !bodyshop.messagingservicesid}
>
{t("general.labels.sms")}
</Menu.Item>
</Menu>
}
>
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
}}
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({
phone_num: p.formatInternational(),
jobid: event.job.id,
});
setMessage(
t("appointments.labels.reminder", {
shopname: bodyshop.shopname,
date: moment(event.start).format("MM/DD/YYYY"),
time: moment(event.start).format("HH:mm a"),
})
);
setVisible(false);
} else {
notification["error"]({
message: t("messaging.error.invalidphone"),
});
}
}}
disabled={event.arrived || !bodyshop.messagingservicesid}
>
{t("general.labels.sms")}
</Menu.Item>
</Menu>
}
>
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
) : null}
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
{t("appointments.actions.cancel")}
</Button>
<Button
disabled={event.arrived}
onClick={() => {
setVisible(false);
setScheduleContext({
actions: { refetch: refetch },
context: {
jobId: event.job.id,
job: event.job,
previousEvent: event.id,
color: event.color,
alt_transport: event.job && event.job.alt_transport,
note: event.note,
},
});
}}
>
{t("appointments.actions.reschedule")}
</Button>
{event.isintake ? (
<Button
disabled={event.arrived}
onClick={() => {
setVisible(false);
setScheduleContext({
actions: { refetch: refetch },
context: {
jobId: event.job.id,
job: event.job,
previousEvent: event.id,
color: event.color,
alt_transport: event.job && event.job.alt_transport,
note: event.note,
},
});
}}
>
{t("appointments.actions.reschedule")}
</Button>
) : (
<ScheduleManualEvent event={event} />
)}
{event.isintake ? (
<Link
to={{
@@ -235,7 +246,13 @@ export function ScheduleEventComponent({
);
const RegularEvent = event.isintake ? (
<div style={{ display: "flex", flexWrap: "wrap" }}>
<div
style={{
display: "flex",
flexWrap: "wrap",
height: "100%",
}}
>
<Space>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
@@ -262,7 +279,7 @@ export function ScheduleEventComponent({
)}
</div>
) : (
<div>
<div style={{ height: "100%", width: "100%" }}>
<strong>{`${event.title || ""}`}</strong>
</div>
);
@@ -270,7 +287,7 @@ export function ScheduleEventComponent({
return (
<Popover
visible={visible}
onVisibleChange={(vis) => setVisible(vis)}
onVisibleChange={(vis) => !event.vacation && setVisible(vis)}
trigger="click"
content={event.block ? blockContent : popoverContent}
style={{ height: "100%", width: "100%" }}

View File

@@ -10,6 +10,8 @@ export default function JobAuditTrail({ jobId }) {
const { loading, data } = useQuery(QUERY_AUDIT_TRAIL, {
variables: { jobid: jobId },
skip: !jobId,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const columns = [

View File

@@ -79,9 +79,19 @@ export function JobChecklistForm({
...(type === "intake" && {
scheduled_completion: values.scheduled_completion,
}),
...(type === "intake" &&
bodyshop.intakechecklist &&
bodyshop.intakechecklist.next_contact_hours &&
bodyshop.intakechecklist.next_contact_hours > 0 && {
date_next_contact: moment().add(
bodyshop.intakechecklist.next_contact_hours,
"hours"
),
}),
...(type === "deliver" && {
actual_completion: values.actual_completion,
}),
[(type === "intake" && "intakechecklist") ||
(type === "deliver" && "deliverchecklist")]: {
...values,

View File

@@ -21,12 +21,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU);
export function JobCreateIOU({
bodyshop,
currentUser,
jobid,
selectedJobLines,
}) {
export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const client = useApolloClient();
@@ -44,7 +39,7 @@ export function JobCreateIOU({
//Query all of the job details to recreate.
const iouId = await CreateIouForJob(
client,
jobid,
job.id,
{
status: bodyshop.md_ro_statuses.default_open,
bodyshopid: bodyshop.id,
@@ -63,7 +58,7 @@ export function JobCreateIOU({
variables: { ids: selectedJobLinesIds },
update(cache) {
cache.modify({
id: cache.identify(jobid),
id: cache.identify(job.id),
fields: {
joblines(existingJobLines, { readField }) {
return existingJobLines.map((a) => {
@@ -83,10 +78,15 @@ export function JobCreateIOU({
<Popconfirm
title={t("jobs.labels.createiouwarning")}
onConfirm={handleCreateIou}
disabled={
!selectedJobLines || selectedJobLines.length === 0 || !job.converted
}
>
<Button
loading={loading}
disabled={!selectedJobLines || selectedJobLines.length === 0}
disabled={
!selectedJobLines || selectedJobLines.length === 0 || !job.converted
}
>
{t("jobs.actions.createiou")}
</Button>

View File

@@ -54,6 +54,8 @@ export function JobDetailCards({ setPrintCenterContext }) {
const { loading, error, data, refetch } = useQuery(QUERY_JOB_CARD_DETAILS, {
variables: { id: selected },
skip: !selected,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { t } = useTranslation();

View File

@@ -428,7 +428,7 @@ export function JobLinesComponent({
>
{t("joblines.actions.new")}
</Button>
<JobCreateIOU jobid={job.id} selectedJobLines={selectedLines} />
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {

View File

@@ -1,10 +1,10 @@
import React, { useState, useEffect } from "react";
import { Input, notification, Space } from "antd";
import { FieldTimeOutlined } from "@ant-design/icons";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import { useMutation } from "@apollo/client";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import { Input, notification } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
export default function JobLineNotePopup({ jobline, disabled }) {
const [editing, setEditing] = useState(false);
@@ -56,17 +56,23 @@ export default function JobLineNotePopup({ jobline, disabled }) {
</div>
);
return (
<div
style={{ width: "100%", minHeight: "2rem", cursor: "pointer" }}
onClick={() => !disabled && setEditing(true)}
>
{jobline.ioucreated && (
<Space>
<FieldTimeOutlined />
{t("joblines.labels.ioucreated")}
</Space>
)}
{jobline.notes}
<div>
<div
style={{
width: "100%",
...(jobline.ioucreated ? {} : { minHeight: "2rem" }),
cursor: "pointer",
}}
onClick={() => !disabled && setEditing(true)}
>
{jobline.ioucreated && (
<div>
<FieldTimeOutlined style={{ margin: 0, padding: 0 }} />
{t("joblines.labels.ioucreated")}
</div>
)}
{jobline.notes || null}
</div>
</div>
);
}

View File

@@ -56,10 +56,12 @@ export function JobLineStatusPopup({ bodyshop, jobline, disabled }) {
<LoadingSpinner loading={loading}>
<Select
autoFocus
allowClear
dropdownMatchSelectWidth={100}
value={status}
onSelect={handleChange}
onBlur={handleSave}
onClear={() => handleChange(null)}
>
{Object.values(bodyshop.md_order_statuses).map((s, idx) => (
<Select.Option key={idx} value={s}>

View File

@@ -4,7 +4,24 @@ import { useTranslation } from "react-i18next";
import InputCurrency from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JoblinesPreset from "../job-lines-preset-button/job-lines-preset-button.component";
export default function JobLinesUpsertModalComponent({
import { useTreatments } from "@splitsoftware/splitio-react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobLinesUpsertModalComponent);
export function JobLinesUpsertModalComponent({
bodyshop,
visible,
jobLine,
handleCancel,
@@ -18,6 +35,12 @@ export default function JobLinesUpsertModalComponent({
form.resetFields();
}, [visible, form]);
const { Allow_Negative_Jobline_Price } = useTreatments(
["Allow_Negative_Jobline_Price"],
{},
bodyshop.imexshopid
);
return (
<Modal
title={
@@ -213,7 +236,10 @@ export default function JobLinesUpsertModalComponent({
}),
]}
>
<InputCurrency precision={2} min={0} />
<InputCurrency
precision={2}
min={Allow_Negative_Jobline_Price.treatment === "on" ? null : 0}
/>
</Form.Item>
<Form.Item
label={t("joblines.fields.prt_dsmk_p")}

View File

@@ -30,6 +30,8 @@ function JobReconciliationModalContainer({
const { loading, error, data } = useQuery(GET_JOB_RECONCILIATION_BY_PK, {
variables: { id: job && job.id },
skip: !(job && job.id) || !visible,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const handleCancel = () => {

View File

@@ -55,6 +55,7 @@ export function JobsAvailableContainer({
}) {
const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_JOBS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { clm_no, availableJobId } = queryString.parse(useLocation().search);
const history = useHistory();

View File

@@ -31,6 +31,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Collapse.Panel
key="insurance"
header={t("menus.jobsdetail.insurance")}
forceRender
>
<LayoutFormRow>
<Form.Item label={t("jobs.fields.ins_co_id")} name="ins_co_id">
@@ -145,7 +146,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item>
</LayoutFormRow>
</Collapse.Panel>
<Collapse.Panel key="claim" header={t("menus.jobsdetail.claimdetail")}>
<Collapse.Panel forceRender key="claim" header={t("menus.jobsdetail.claimdetail")}>
<LayoutFormRow>
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
<Input />
@@ -192,7 +193,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item>
</LayoutFormRow>
</Collapse.Panel>
<Collapse.Panel
<Collapse.Panel forceRender
key="financial"
header={t("menus.jobsdetail.financials")}
>

View File

@@ -10,6 +10,8 @@ export default function JobsCreateOwnerContainer() {
const { loading, error, data } = useQuery(QUERY_SEARCH_OWNER_BY_IDX, {
variables: { search: `%${state.owner.search}%` },
skip: !state.owner.search,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
if (error) return <AlertComponent message={error.message} type="error" />;

View File

@@ -10,6 +10,8 @@ export default function JobsCreateVehicleInfoContainer({ form }) {
const { loading, error, data } = useQuery(SEARCH_VEHICLES, {
variables: { search: `%${state.vehicle.search}%` },
skip: !state.vehicle.search,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
if (error) return <AlertComponent message={error.message} type="error" />;

View File

@@ -89,6 +89,19 @@ export function JobsDetailHeaderActions({
});
};
const handleSuspend = (e) => {
logImEXEvent("production_toggle_alert");
//e.stopPropagation();
updateJob({
variables: {
jobId: job.id,
job: {
suspended: !job.suspended,
},
},
});
};
const statusmenu = (
<Menu key="popovermenu">
<Menu.Item
@@ -199,7 +212,11 @@ export function JobsDetailHeaderActions({
{t("jobs.actions.addtoproduction")}
</Menu.Item>
)}
<Menu.Item key="togglesuspend" onClick={handleSuspend}>
{job.suspended
? t("production.actions.unsuspend")
: t("production.actions.suspend")}
</Menu.Item>
<Menu.Item key="toggleAlert" onClick={handleAlertToggle}>
{job.production_vars && job.production_vars.alert
? t("production.labels.alertoff")

View File

@@ -108,6 +108,11 @@ export async function CreateIouForJob(
_tempLines.forEach((line) => {
delete line.id;
delete line.__typename;
line.oem_partno = `${line.oem_partno ? `${line.oem_partno} - ` : ``}IOU $${
(line.act_price && line.act_price.toFixed(2)) || 0
}/${line.mod_lb_hrs || 0}hrs`;
line.act_price = 0;
line.mod_lb_hrs = 0;
line.manual_line = true;
});

View File

@@ -1,5 +1,9 @@
import { Card, Col, Row, Space, Tag } from "antd";
import { WarningFilled, ExclamationCircleFilled } from "@ant-design/icons";
import {
WarningFilled,
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -68,6 +72,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{t("jobs.labels.inproduction")}
</Tag>
)}
{job.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{job.production_vars && job.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}

View File

@@ -8,6 +8,8 @@ export default function JobsDetailLaborContainer({ jobId, techConsole, job }) {
const { loading, error, data, refetch } = useQuery(GET_LINE_TICKET_BY_PK, {
variables: { id: jobId },
skip: !!!jobId,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
if (error) return <AlertComponent message={error.message} type="error" />;

View File

@@ -8,6 +8,8 @@ import JobsDetailPliComponent from "./jobs-detail-pli.component";
export default function JobsDetailPliContainer({ job }) {
const billsQuery = useQuery(QUERY_BILLS_BY_JOBID, {
variables: { jobid: job.id },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const search = queryString.parse(useLocation().search);

View File

@@ -100,7 +100,7 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
label={t("jobs.fields.state_tax_rate")}
name="state_tax_rate"
>
<InputNumber min={0} max={1} precision={2} disabled={jobRO} />
<InputNumber min={0} max={1} precision={2} disabled={jobRO} autoComplete="new-password"/>
</Form.Item>
<Form.Item
label={t("jobs.fields.local_tax_rate")}

View File

@@ -5,45 +5,124 @@ 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 { useTreatments } from "@splitsoftware/splitio-react";
export default function JobsDocumentsDownloadButton({
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsDownloadButton);
export function JobsDocumentsDownloadButton({
bodyshop,
galleryImages,
identifier,
}) {
const { t } = useTranslation();
const [download, setDownload] = useState(null);
const { Direct_Media_Download } = useTreatments(
["Direct_Media_Download"],
{},
bodyshop.imexshopid
);
const imagesToDownload = [
...galleryImages.images.filter((image) => image.isSelected),
// ...galleryImages.other.filter((image) => image.isSelected),
];
const handleDownload = () => {
logImEXEvent("jobs_documents_download");
axios
.post("/media/download", {
ids: imagesToDownload.map((_) => _.key),
})
.then((r) => {
// window.open(r.data);
downloadAs(
r.data,
`${identifier || "documents"}.zip`,
(progressEvent) => {
setDownload((currentDownloadState) => {
return {
downloaded: progressEvent.loaded || 0,
speed:
(progressEvent.loaded || 0) -
((currentDownloadState && currentDownloadState.downloaded) ||
0),
};
});
},
() => setDownload(null)
);
});
};
function downloadProgress(progressEvent) {
setDownload((currentDownloadState) => {
return {
downloaded: progressEvent.loaded || 0,
speed:
(progressEvent.loaded || 0) -
((currentDownloadState && currentDownloadState.downloaded) || 0),
};
});
}
const handleDownload = async () => {
logImEXEvent("jobs_documents_download");
const zipUrl = await axios({
url: "/media/download",
method: "POST",
//responseType: "arraybuffer", // Important
data: { ids: imagesToDownload.map((_) => _.key) },
});
const theDownloadedZip = await cleanAxios({
url: zipUrl.data,
method: "GET",
responseType: "arraybuffer",
onDownloadProgress: downloadProgress,
});
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();
});
});
});
}
);
} catch (e) {
console.log(e);
standardMediaDownload(theDownloadedZip.data);
}
} else {
standardMediaDownload(theDownloadedZip.data);
}
function standardMediaDownload(bufferData) {
const a = document.createElement("a");
const url = window.URL.createObjectURL(new Blob([bufferData]));
a.href = url;
a.download = `${identifier || "documents"}.zip`;
a.click();
}
};
return (
<>
<Button
@@ -63,25 +142,3 @@ export default function JobsDocumentsDownloadButton({
</>
);
}
const downloadAs = (url, name, onDownloadProgress, onCompleted) => {
cleanAxios
.get(url, {
headers: {
"Content-Type": "application/octet-stream",
},
responseType: "blob",
onDownloadProgress: onDownloadProgress,
})
.then((response) => {
onCompleted && onCompleted();
const a = document.createElement("a");
const url = window.URL.createObjectURL(response.data);
a.href = url;
a.download = name;
a.click();
})
.catch((err) => {
console.log("error", err);
});
};

View File

@@ -14,6 +14,7 @@ export default function JobsDocumentsContainer({
const { loading, error, data, refetch } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: { jobId: jobId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !!billId,
});

View File

@@ -33,6 +33,8 @@ export default connect(
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
},
skip: !modalProps.visible,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const modalSearch = modalSearchState[0];

View File

@@ -1,6 +1,10 @@
import { SyncOutlined, ExclamationCircleFilled } from "@ant-design/icons";
import {
SyncOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Input, Space, Table } from "antd";
import { Button, Card, Grid, Input, Space, Table } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -22,11 +26,15 @@ const mapStateToProps = createStructuredSelector({
export function JobsList({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [state, setState] = useState({
@@ -107,6 +115,9 @@ export function JobsList({ bodyshop }) {
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
</Space>
</Link>
),
@@ -115,6 +126,9 @@ export function JobsList({ bodyshop }) {
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
@@ -160,6 +174,8 @@ export function JobsList({ bodyshop }) {
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
@@ -204,6 +220,8 @@ export function JobsList({ bodyshop }) {
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
@@ -228,11 +246,15 @@ export function JobsList({ bodyshop }) {
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
responsive: ["md"],
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
@@ -251,6 +273,15 @@ export function JobsList({ bodyshop }) {
// },
];
const scrollMapper = {
xs: true,
sm: true,
md: true,
lg: "100%",
xl: "100%",
xxl: "100%",
};
return (
<Card
title={t("titles.bc.jobs-active")}
@@ -272,11 +303,13 @@ export function JobsList({ bodyshop }) {
>
<Table
loading={loading}
pagination={false}
pagination={{ defaultPageSize: 50 }}
columns={columns}
rowKey="id"
dataSource={jobs}
// scroll={{ x: true }}
scroll={{
x: selectedBreakpoint ? scrollMapper[selectedBreakpoint[0]] : "100%",
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);

View File

@@ -28,6 +28,7 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
const { loading, error, data, refetch } = useQuery(QUERY_NOTES_BY_JOB_PK, {
variables: { id: jobId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [deleteNote] = useMutation(DELETE_NOTE);

View File

@@ -5,7 +5,7 @@ import {
EyeInvisibleFilled,
WarningFilled,
} from "@ant-design/icons";
import { Button, Card, Space, Table } from "antd";
import { Button, Card, Form, Input, Space, Table } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -14,6 +14,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
import { setModalContext } from "../../redux/modals/modals.actions";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -131,32 +132,42 @@ export function JobNotesComponent({
];
return (
<Card
title={t("jobs.labels.notes")}
extra={
<Button
onClick={() => {
setNoteUpsertContext({
actions: { refetch: refetch },
context: {
jobId: jobId,
},
});
}}
<div>
<LayoutFormRow>
<Form.Item
label={t("jobs.fields.invoice_final_note")}
name="invoice_final_note"
>
{t("notes.actions.new")}
</Button>
}
>
<NoteUpsertModal />
<Input.TextArea disabled={jobRO} />
</Form.Item>
</LayoutFormRow>
<Card
title={t("jobs.labels.notes")}
extra={
<Button
onClick={() => {
setNoteUpsertContext({
actions: { refetch: refetch },
context: {
jobId: jobId,
},
});
}}
>
{t("notes.actions.new")}
</Button>
}
>
<NoteUpsertModal />
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data}
/>
</Card>
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data}
/>
</Card>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesComponent);

View File

@@ -12,6 +12,8 @@ export default function OwnersListContainer() {
const { loading, error, data, refetch } = useQuery(
QUERY_ALL_OWNERS_PAGINATED,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
search: search || "",
offset: page ? (page - 1) * 25 : 0,

View File

@@ -10,7 +10,7 @@ import {
PageHeader,
Popconfirm,
Space,
Table
Table,
} from "antd";
import queryString from "query-string";
import React, { useState } from "react";
@@ -168,7 +168,9 @@ export function PartsOrderListTableComponent({
cost_center: pol.jobline?.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? pol.jobline.part_type
? pol.jobline.part_type !== "PAE"
? pol.jobline.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[
pol.jobline.part_type

View File

@@ -77,6 +77,8 @@ export function PartsOrderModalContainer({
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS_FOR_ORDER, {
skip: !visible,
variables: { jobId: jobId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS);
@@ -248,8 +250,6 @@ export function PartsOrderModalContainer({
}),
});
}
} catch (error) {
console.log("Error OEC.", error);
notification["error"]({

View File

@@ -13,6 +13,8 @@ export default function PaymentFormTotalPayments({ jobid }) {
const { loading, error, data } = useQuery(QUERY_JOB_PAYMENT_TOTALS, {
variables: { id: jobid },
skip: !jobid,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
if (loading) return <LoadingSpinner />;

View File

@@ -31,6 +31,7 @@ function PhonebookFormContainer({ refetch, bodyshop }) {
const { loading, error, data } = useQuery(QUERY_PHONEBOOK_BY_ID, {
variables: { id: phonebookentry },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: !!!phonebookentry || phonebookentry === "new",
});

View File

@@ -0,0 +1,84 @@
import { Button, Card, Form, InputNumber, Popover } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(PrintCenterJobsLabels);
export function PrintCenterJobsLabels({ bodyshop, jobId }) {
const [isModalVisible, setIsModalVisible] = useState(false);
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [form] = Form.useForm();
const handleOk = () => {
form.submit();
setIsModalVisible(false);
};
const handleCancel = () => {
setIsModalVisible(false);
setLoading(false);
};
const handleFinish = async (values) => {
const { sendtype, ...restVals } = values;
setLoading(true);
await GenerateDocument(
{
name: TemplateList("job_special").folder_label_multiple.key,
variables: { id: jobId },
context: restVals,
},
{},
"p",
jobId
);
setLoading(false);
setIsModalVisible(false);
};
const content = (
<Card>
<Form
onFinish={handleFinish}
autoComplete={"off"}
layout="vertical"
form={form}
>
<Form.Item
label={t("printcenter.jobs.labels.position")}
name="position"
>
<InputNumber min={1} precision={0} />
</Form.Item>
<Form.Item label={t("printcenter.jobs.labels.count")} name="count">
<InputNumber min={1} precision={0} max={99} />
</Form.Item>
<Button type="primary" loading={loading} onClick={handleOk}>
{t("general.actions.print")}
</Button>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
</Form>
</Card>
);
return (
<Popover content={content} visible={isModalVisible}>
<Button onClick={() => setIsModalVisible(true)}>
{t("printcenter.jobs.labels.labels")}
</Button>
</Popover>
);
}

View File

@@ -9,6 +9,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants";
import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component";
import PrintCenterItem from "../print-center-item/print-center-item.component";
import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component";
import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component";
const mapStateToProps = createStructuredSelector({
printCenterModal: selectPrintCenter,
@@ -52,6 +53,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop }) {
<Card
extra={
<Space wrap>
<PrintCenterJobsLabels jobId={jobId} />
<Jobd3RdPartyModal jobId={jobId} />
<Input.Search
onChange={(e) => setSearch(e.target.value)}

View File

@@ -1,4 +1,8 @@
import { CalendarOutlined, EyeFilled } from "@ant-design/icons";
import {
CalendarOutlined,
EyeFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Col, Row, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -40,6 +44,9 @@ export default function ProductionBoardCard(
title={
<Space>
<ProductionAlert record={card} key="alert" />
{card.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
<span style={{ fontWeight: "bolder" }}>
{card.ro_number || t("general.labels.na")}
</span>

Some files were not shown because too many files have changed in this diff Show More